@co0ontty/wand 1.26.0 → 1.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +20 -4
- package/dist/config.js +99 -4
- package/dist/git-quick-commit.d.ts +3 -2
- package/dist/git-quick-commit.js +170 -133
- package/dist/server.js +214 -5
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +11 -0
- package/dist/structured-session-manager.d.ts +20 -0
- package/dist/structured-session-manager.js +192 -18
- package/dist/types.d.ts +25 -7
- package/dist/web-ui/content/scripts.js +366 -79
- package/dist/ws-broadcast.d.ts +10 -0
- package/dist/ws-broadcast.js +75 -0
- package/package.json +1 -1
|
@@ -123,6 +123,9 @@
|
|
|
123
123
|
sidebarPinned: (function() {
|
|
124
124
|
try { return localStorage.getItem("wand-sidebar-pinned") === "true"; } catch (e) { return false; }
|
|
125
125
|
})(),
|
|
126
|
+
sidebarCollapsed: (function() {
|
|
127
|
+
try { return localStorage.getItem("wand-sidebar-collapsed") === "true"; } catch (e) { return false; }
|
|
128
|
+
})(),
|
|
126
129
|
modalOpen: false,
|
|
127
130
|
presetValue: "",
|
|
128
131
|
cwdValue: "",
|
|
@@ -150,6 +153,11 @@
|
|
|
150
153
|
showInstallPrompt: false,
|
|
151
154
|
ws: null,
|
|
152
155
|
wsConnected: false,
|
|
156
|
+
// 上一次从服务器收到任意 WS 消息(包括 ping)的时间戳。心跳 stale 检测
|
|
157
|
+
// 用它来判断半开连接:长时间没消息 → forceReconnect。0 表示尚未连接过。
|
|
158
|
+
lastWsMessageAt: 0,
|
|
159
|
+
// 心跳检查 timer 句柄。每 10s 跑一次 evaluateWsHeartbeatStale()。
|
|
160
|
+
wsHeartbeatCheckTimer: null,
|
|
153
161
|
_updateBubbleShown: false,
|
|
154
162
|
notificationHistory: {},
|
|
155
163
|
delayedNotificationTimer: null,
|
|
@@ -199,15 +207,14 @@
|
|
|
199
207
|
// Whether the commit is also tagged is controlled by `makeTag` independently —
|
|
200
208
|
// that keeps "commit + tag, push later" possible.
|
|
201
209
|
quickCommitForm: { customMessage: "", makeTag: false, tag: "", primaryAction: "commit" },
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
|
|
210
|
+
// Which inline panel/dropdown is open. Only one can be open at a time, so a
|
|
211
|
+
// single field beats juggling three sibling booleans with mutual-exclusion code.
|
|
212
|
+
// Values: null | "action" | "push" | "tag-head".
|
|
213
|
+
quickCommitOpenMenu: null,
|
|
205
214
|
quickCommitTagHeadForm: { tag: "", push: false },
|
|
206
215
|
quickCommitTagHeadSubmitting: false,
|
|
207
216
|
quickCommitTagHeadGenerating: false,
|
|
208
217
|
quickCommitTagHeadError: "",
|
|
209
|
-
// Secondary "push only" controls.
|
|
210
|
-
quickCommitPushMenuOpen: false,
|
|
211
218
|
quickCommitPushing: false,
|
|
212
219
|
quickCommitPushError: "",
|
|
213
220
|
// Telegram 风格的"贴底"状态:true = 用户当前贴在底部,新消息会自然出现;
|
|
@@ -1027,6 +1034,11 @@
|
|
|
1027
1034
|
function syncOnForeground(reason, force) {
|
|
1028
1035
|
if (!state.config) return Promise.resolve();
|
|
1029
1036
|
if (document.hidden) return Promise.resolve();
|
|
1037
|
+
// 切回前台时立刻评估一次心跳 stale。setInterval 在 background 会被
|
|
1038
|
+
// 浏览器节流(最低 1Hz,部分浏览器更慢),所以如果挂了 1 分钟回来,
|
|
1039
|
+
// 不主动跑这一次的话要等到下一个 10s tick 才会发现,前 10s 会继续
|
|
1040
|
+
// 往一条死 socket 上推消息。
|
|
1041
|
+
evaluateWsHeartbeatStale();
|
|
1030
1042
|
// On Android resume the previous WS may still report OPEN/CONNECTING
|
|
1031
1043
|
// for a few seconds because the close frame hasn't been delivered
|
|
1032
1044
|
// yet (TCP keepalive / Doze suspended the network stack). Force a
|
|
@@ -1168,6 +1180,10 @@
|
|
|
1168
1180
|
if (_apkVersion) {
|
|
1169
1181
|
checkApkAutoUpdate();
|
|
1170
1182
|
}
|
|
1183
|
+
// macOS DMG auto-update check on startup
|
|
1184
|
+
if (_macAppVersion) {
|
|
1185
|
+
checkDmgAutoUpdate();
|
|
1186
|
+
}
|
|
1171
1187
|
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
1172
1188
|
loadClaudeHistory();
|
|
1173
1189
|
}
|
|
@@ -1412,6 +1428,9 @@
|
|
|
1412
1428
|
'<button id="sidebar-pin-btn" class="btn btn-ghost btn-sm sidebar-pin-toggle' + (state.sidebarPinned ? ' pinned' : '') + '" type="button" title="' + (state.sidebarPinned ? '取消固定侧栏' : '固定侧栏') + '">' +
|
|
1413
1429
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24z"/></svg>' +
|
|
1414
1430
|
'</button>' +
|
|
1431
|
+
'<button id="sidebar-collapse-btn" class="btn btn-ghost btn-sm sidebar-collapse-toggle" type="button" title="收起为窄条" aria-label="收起为窄条">' +
|
|
1432
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="14 6 8 12 14 18"/><line x1="4" y1="5" x2="4" y2="19"/></svg>' +
|
|
1433
|
+
'</button>' +
|
|
1415
1434
|
'<button id="close-drawer-button" class="btn btn-ghost btn-icon sidebar-close drawer-close-btn" type="button" aria-label="关闭菜单"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg></button>' +
|
|
1416
1435
|
'</div>' +
|
|
1417
1436
|
'</div>' +
|
|
@@ -1816,13 +1835,11 @@
|
|
|
1816
1835
|
tag: "",
|
|
1817
1836
|
primaryAction: readSavedPrimaryAction(),
|
|
1818
1837
|
};
|
|
1819
|
-
state.
|
|
1820
|
-
state.quickCommitTagHeadOpen = false;
|
|
1838
|
+
state.quickCommitOpenMenu = null;
|
|
1821
1839
|
state.quickCommitTagHeadForm = { tag: "", push: false };
|
|
1822
1840
|
state.quickCommitTagHeadSubmitting = false;
|
|
1823
1841
|
state.quickCommitTagHeadGenerating = false;
|
|
1824
1842
|
state.quickCommitTagHeadError = "";
|
|
1825
|
-
state.quickCommitPushMenuOpen = false;
|
|
1826
1843
|
state.quickCommitPushing = false;
|
|
1827
1844
|
state.quickCommitPushError = "";
|
|
1828
1845
|
closeWorktreeMergeModal();
|
|
@@ -1839,10 +1856,8 @@
|
|
|
1839
1856
|
quickCommitEscHandler = function(e) {
|
|
1840
1857
|
if (e.key === "Escape" && state.quickCommitOpen && !state.quickCommitSubmitting && !state.quickCommitTagHeadSubmitting && !state.quickCommitPushing) {
|
|
1841
1858
|
// First Esc closes any open dropdown; second closes the modal.
|
|
1842
|
-
if (state.
|
|
1843
|
-
state.
|
|
1844
|
-
state.quickCommitPushMenuOpen = false;
|
|
1845
|
-
state.quickCommitTagHeadOpen = false;
|
|
1859
|
+
if (state.quickCommitOpenMenu) {
|
|
1860
|
+
state.quickCommitOpenMenu = null;
|
|
1846
1861
|
rerenderQuickCommitModal();
|
|
1847
1862
|
return;
|
|
1848
1863
|
}
|
|
@@ -1853,18 +1868,16 @@
|
|
|
1853
1868
|
if (quickCommitDocClickHandler) document.removeEventListener("click", quickCommitDocClickHandler, true);
|
|
1854
1869
|
quickCommitDocClickHandler = function(e) {
|
|
1855
1870
|
if (!state.quickCommitOpen) return;
|
|
1871
|
+
// tag-head is an inline drawer, not a dropdown — clicking outside shouldn't close it.
|
|
1872
|
+
if (state.quickCommitOpenMenu !== "action" && state.quickCommitOpenMenu !== "push") return;
|
|
1856
1873
|
var modalEl = document.getElementById("quick-commit-modal");
|
|
1857
1874
|
if (!modalEl) return;
|
|
1858
|
-
var anyMenuOpen = state.quickCommitActionMenuOpen || state.quickCommitPushMenuOpen;
|
|
1859
|
-
if (!anyMenuOpen) return;
|
|
1860
|
-
// Click on the toggle itself? leave it to the toggle handler.
|
|
1861
1875
|
var t = e.target;
|
|
1862
1876
|
while (t && t !== modalEl) {
|
|
1863
1877
|
if (t.dataset && (t.dataset.qcDropdownToggle || t.dataset.qcDropdownMenu)) return;
|
|
1864
1878
|
t = t.parentNode;
|
|
1865
1879
|
}
|
|
1866
|
-
state.
|
|
1867
|
-
state.quickCommitPushMenuOpen = false;
|
|
1880
|
+
state.quickCommitOpenMenu = null;
|
|
1868
1881
|
rerenderQuickCommitModal();
|
|
1869
1882
|
};
|
|
1870
1883
|
document.addEventListener("click", quickCommitDocClickHandler, true);
|
|
@@ -1878,9 +1891,7 @@
|
|
|
1878
1891
|
state.quickCommitOpen = false;
|
|
1879
1892
|
state.quickCommitSubmitting = false;
|
|
1880
1893
|
state.quickCommitError = "";
|
|
1881
|
-
state.
|
|
1882
|
-
state.quickCommitPushMenuOpen = false;
|
|
1883
|
-
state.quickCommitTagHeadOpen = false;
|
|
1894
|
+
state.quickCommitOpenMenu = null;
|
|
1884
1895
|
var modal = document.getElementById("quick-commit-modal");
|
|
1885
1896
|
if (modal) modal.classList.add("hidden");
|
|
1886
1897
|
if (focusTrapHandler) {
|
|
@@ -1918,7 +1929,6 @@
|
|
|
1918
1929
|
var cancelBtn = document.getElementById("quick-commit-cancel-btn");
|
|
1919
1930
|
if (cancelBtn) cancelBtn.addEventListener("click", closeQuickCommitModal);
|
|
1920
1931
|
|
|
1921
|
-
// ── Section 1: commit form ──
|
|
1922
1932
|
var submitBtn = document.getElementById("quick-commit-submit-btn");
|
|
1923
1933
|
if (submitBtn) submitBtn.addEventListener("click", submitQuickCommit);
|
|
1924
1934
|
var aiBtn = document.getElementById("quick-commit-ai-btn");
|
|
@@ -1938,12 +1948,10 @@
|
|
|
1938
1948
|
state.quickCommitForm.tag = tagInput.value;
|
|
1939
1949
|
});
|
|
1940
1950
|
|
|
1941
|
-
// ── Primary action split-button (caret + dropdown) ──
|
|
1942
1951
|
var actionCaret = document.getElementById("quick-commit-action-caret");
|
|
1943
1952
|
if (actionCaret) actionCaret.addEventListener("click", function(e) {
|
|
1944
1953
|
e.stopPropagation();
|
|
1945
|
-
state.
|
|
1946
|
-
state.quickCommitPushMenuOpen = false;
|
|
1954
|
+
state.quickCommitOpenMenu = state.quickCommitOpenMenu === "action" ? null : "action";
|
|
1947
1955
|
rerenderQuickCommitModal();
|
|
1948
1956
|
});
|
|
1949
1957
|
var actionMenu = document.getElementById("quick-commit-action-menu");
|
|
@@ -1957,29 +1965,27 @@
|
|
|
1957
1965
|
state.quickCommitForm.primaryAction = value;
|
|
1958
1966
|
savePrimaryAction(value);
|
|
1959
1967
|
}
|
|
1960
|
-
state.
|
|
1968
|
+
state.quickCommitOpenMenu = null;
|
|
1961
1969
|
rerenderQuickCommitModal();
|
|
1962
1970
|
});
|
|
1963
1971
|
})(actionItems[i]);
|
|
1964
1972
|
}
|
|
1965
1973
|
}
|
|
1966
1974
|
|
|
1967
|
-
// ── Section 2: Tag HEAD inline panel ──
|
|
1968
1975
|
var tagHeadToggle = document.getElementById("quick-commit-tag-head-toggle");
|
|
1969
1976
|
if (tagHeadToggle) tagHeadToggle.addEventListener("click", function() {
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
}
|
|
1977
|
+
var willOpen = state.quickCommitOpenMenu !== "tag-head";
|
|
1978
|
+
state.quickCommitOpenMenu = willOpen ? "tag-head" : null;
|
|
1979
|
+
if (willOpen) state.quickCommitTagHeadError = "";
|
|
1974
1980
|
rerenderQuickCommitModal();
|
|
1975
|
-
if (
|
|
1981
|
+
if (willOpen) {
|
|
1976
1982
|
var inp = document.getElementById("quick-commit-tag-head-input");
|
|
1977
1983
|
if (inp) inp.focus();
|
|
1978
1984
|
}
|
|
1979
1985
|
});
|
|
1980
1986
|
var tagHeadCancel = document.getElementById("quick-commit-tag-head-cancel");
|
|
1981
1987
|
if (tagHeadCancel) tagHeadCancel.addEventListener("click", function() {
|
|
1982
|
-
state.
|
|
1988
|
+
if (state.quickCommitOpenMenu === "tag-head") state.quickCommitOpenMenu = null;
|
|
1983
1989
|
state.quickCommitTagHeadError = "";
|
|
1984
1990
|
rerenderQuickCommitModal();
|
|
1985
1991
|
});
|
|
@@ -2006,7 +2012,6 @@
|
|
|
2006
2012
|
submitTagHead(false);
|
|
2007
2013
|
});
|
|
2008
2014
|
|
|
2009
|
-
// ── Section 2: Push split-button ──
|
|
2010
2015
|
var pushBtn = document.getElementById("quick-commit-push-btn");
|
|
2011
2016
|
if (pushBtn) pushBtn.addEventListener("click", function() {
|
|
2012
2017
|
submitPushOnly({ pushCommits: true, pushTags: false });
|
|
@@ -2014,8 +2019,7 @@
|
|
|
2014
2019
|
var pushCaret = document.getElementById("quick-commit-push-caret");
|
|
2015
2020
|
if (pushCaret) pushCaret.addEventListener("click", function(e) {
|
|
2016
2021
|
e.stopPropagation();
|
|
2017
|
-
state.
|
|
2018
|
-
state.quickCommitActionMenuOpen = false;
|
|
2022
|
+
state.quickCommitOpenMenu = state.quickCommitOpenMenu === "push" ? null : "push";
|
|
2019
2023
|
rerenderQuickCommitModal();
|
|
2020
2024
|
});
|
|
2021
2025
|
var pushMenu = document.getElementById("quick-commit-push-menu");
|
|
@@ -2025,7 +2029,7 @@
|
|
|
2025
2029
|
(function(btn) {
|
|
2026
2030
|
btn.addEventListener("click", function() {
|
|
2027
2031
|
var value = btn.getAttribute("data-qc-push");
|
|
2028
|
-
state.
|
|
2032
|
+
state.quickCommitOpenMenu = null;
|
|
2029
2033
|
if (value === "commits") submitPushOnly({ pushCommits: true, pushTags: false });
|
|
2030
2034
|
else if (value === "tags") submitPushOnly({ pushCommits: false, pushTags: true });
|
|
2031
2035
|
else if (value === "both") submitPushOnly({ pushCommits: true, pushTags: true });
|
|
@@ -2119,7 +2123,11 @@
|
|
|
2119
2123
|
var data = result.data || {};
|
|
2120
2124
|
var hash = data.commit && data.commit.hash ? data.commit.hash.substring(0, 7) : "";
|
|
2121
2125
|
var tagName = data.tag && data.tag.name ? data.tag.name : "";
|
|
2122
|
-
var
|
|
2126
|
+
var subCommits = Array.isArray(data.submoduleCommits) ? data.submoduleCommits : [];
|
|
2127
|
+
var subPrefix = subCommits.length > 0
|
|
2128
|
+
? "已先提交 " + subCommits.length + " 个 submodule(" + subCommits.map(function(c) { return c.path; }).join("、") + "),"
|
|
2129
|
+
: "";
|
|
2130
|
+
var base = subPrefix + "已提交" + (hash ? " " + hash : "") + (tagName ? ",已打 tag " + tagName : "");
|
|
2123
2131
|
if (willPush && data.pushError) {
|
|
2124
2132
|
var msg = base + ";push 失败:" + data.pushError;
|
|
2125
2133
|
if (typeof showToast === "function") showToast(msg, "error");
|
|
@@ -2208,7 +2216,7 @@
|
|
|
2208
2216
|
} else {
|
|
2209
2217
|
if (typeof showToast === "function") showToast(base + (pushed ? ",已 push" : ""), "success");
|
|
2210
2218
|
}
|
|
2211
|
-
state.
|
|
2219
|
+
if (state.quickCommitOpenMenu === "tag-head") state.quickCommitOpenMenu = null;
|
|
2212
2220
|
state.quickCommitTagHeadForm = { tag: "", push: false };
|
|
2213
2221
|
if (state.selectedId) loadGitStatus(state.selectedId, { force: true }).then(function() {
|
|
2214
2222
|
if (state.quickCommitOpen) rerenderQuickCommitModal();
|
|
@@ -2223,7 +2231,6 @@
|
|
|
2223
2231
|
});
|
|
2224
2232
|
}
|
|
2225
2233
|
|
|
2226
|
-
// Push without committing.
|
|
2227
2234
|
function submitPushOnly(opts) {
|
|
2228
2235
|
if (!state.selectedId || state.quickCommitPushing) return;
|
|
2229
2236
|
var pushCommits = !!(opts && opts.pushCommits);
|
|
@@ -2267,7 +2274,6 @@
|
|
|
2267
2274
|
});
|
|
2268
2275
|
}
|
|
2269
2276
|
|
|
2270
|
-
// Build the file list portion of the modal.
|
|
2271
2277
|
function renderQuickCommitFileRows(files) {
|
|
2272
2278
|
var rows = files.map(function(item) {
|
|
2273
2279
|
var status = (item.status || " ").substring(0, 2);
|
|
@@ -2293,15 +2299,19 @@
|
|
|
2293
2299
|
return rows || '<div class="qc-empty">没有可提交的改动。</div>';
|
|
2294
2300
|
}
|
|
2295
2301
|
|
|
2296
|
-
|
|
2302
|
+
function isQuickCommitOpInFlight() {
|
|
2303
|
+
return state.quickCommitSubmitting || state.quickCommitTagHeadSubmitting || state.quickCommitPushing;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2297
2306
|
function renderQuickCommitPrimary(hasChanges) {
|
|
2298
2307
|
var f = state.quickCommitForm;
|
|
2299
2308
|
var label;
|
|
2300
2309
|
if (state.quickCommitSubmitting) label = "提交中…";
|
|
2301
2310
|
else if (f.primaryAction === "commit-push") label = "提交并 push";
|
|
2302
2311
|
else label = "仅提交";
|
|
2303
|
-
var disabled = !hasChanges ||
|
|
2304
|
-
var
|
|
2312
|
+
var disabled = !hasChanges || isQuickCommitOpInFlight();
|
|
2313
|
+
var menuOpen = state.quickCommitOpenMenu === "action";
|
|
2314
|
+
var caretActive = menuOpen ? " is-active" : "";
|
|
2305
2315
|
var menuItems = [
|
|
2306
2316
|
{ value: "commit", label: "仅提交", desc: "只 commit,不 push" },
|
|
2307
2317
|
{ value: "commit-push", label: "提交并 push", desc: "commit 后立即 push 到远端" }
|
|
@@ -2317,36 +2327,32 @@
|
|
|
2317
2327
|
'<button id="quick-commit-submit-btn" class="btn btn-primary qc-split-main" type="button"' + (disabled ? ' disabled' : '') + '>' +
|
|
2318
2328
|
escapeHtml(label) +
|
|
2319
2329
|
'</button>' +
|
|
2320
|
-
'<button id="quick-commit-action-caret" class="btn btn-primary qc-split-caret' + caretActive + '" type="button" data-qc-dropdown-toggle="action"' + (disabled ? ' disabled' : '') + ' aria-haspopup="menu" aria-expanded="' + (
|
|
2330
|
+
'<button id="quick-commit-action-caret" class="btn btn-primary qc-split-caret' + caretActive + '" type="button" data-qc-dropdown-toggle="action"' + (disabled ? ' disabled' : '') + ' aria-haspopup="menu" aria-expanded="' + (menuOpen ? 'true' : 'false') + '" aria-label="更多提交方式">' +
|
|
2321
2331
|
'<svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
|
|
2322
2332
|
'</button>' +
|
|
2323
|
-
(
|
|
2333
|
+
(menuOpen ?
|
|
2324
2334
|
'<div id="quick-commit-action-menu" class="qc-dropdown-menu" data-qc-dropdown-menu="action" role="menu">' + menuHtml + '</div>' : '') +
|
|
2325
2335
|
'</div>';
|
|
2326
2336
|
}
|
|
2327
2337
|
|
|
2328
|
-
// Status chips (ahead/behind/unpushed tags) under the HEAD card.
|
|
2329
2338
|
function renderQuickCommitStatusChips(s) {
|
|
2330
2339
|
var chips = [];
|
|
2340
|
+
var hasUpstream = !!s.upstream;
|
|
2331
2341
|
if (typeof s.ahead === "number" && s.ahead > 0) {
|
|
2332
2342
|
chips.push('<span class="qc-chip qc-chip--ahead" title="本地领先 ' + s.ahead + ' 个 commit">↑ ' + s.ahead + ' 待推送</span>');
|
|
2333
2343
|
}
|
|
2334
2344
|
if (typeof s.behind === "number" && s.behind > 0) {
|
|
2335
2345
|
chips.push('<span class="qc-chip qc-chip--behind" title="远端领先 ' + s.behind + ' 个 commit">↓ ' + s.behind + ' 待拉取</span>');
|
|
2336
2346
|
}
|
|
2337
|
-
if (
|
|
2338
|
-
chips.push('<span class="qc-chip qc-chip--tag" title="' + s.unpushedTagCount + ' 个本地 tag 未推送">🏷 ' + s.unpushedTagCount + ' 待推送</span>');
|
|
2339
|
-
}
|
|
2340
|
-
if (s.hasUpstream === false) {
|
|
2347
|
+
if (!hasUpstream) {
|
|
2341
2348
|
chips.push('<span class="qc-chip qc-chip--warn" title="当前分支没有 upstream,将首次推送时自动设置">无 upstream</span>');
|
|
2342
|
-
}
|
|
2343
|
-
if (!chips.length && s.hasUpstream !== false) {
|
|
2349
|
+
} else if (!chips.length) {
|
|
2344
2350
|
chips.push('<span class="qc-chip qc-chip--clean">与远端同步</span>');
|
|
2345
2351
|
}
|
|
2346
2352
|
return '<div class="qc-status-chips">' + chips.join("") + '</div>';
|
|
2347
2353
|
}
|
|
2348
2354
|
|
|
2349
|
-
// Inline "
|
|
2355
|
+
// Inline drawer used by the "为 HEAD 打 tag" toggle.
|
|
2350
2356
|
function renderQuickCommitTagHeadPanel() {
|
|
2351
2357
|
var thf = state.quickCommitTagHeadForm || { tag: "", push: false };
|
|
2352
2358
|
var submitting = state.quickCommitTagHeadSubmitting;
|
|
@@ -2368,22 +2374,23 @@
|
|
|
2368
2374
|
'</div>';
|
|
2369
2375
|
}
|
|
2370
2376
|
|
|
2371
|
-
// Push split-button on the secondary row.
|
|
2372
2377
|
function renderQuickCommitPushButton(s) {
|
|
2373
2378
|
var ahead = typeof s.ahead === "number" ? s.ahead : 0;
|
|
2374
|
-
var
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
var disabled =
|
|
2379
|
+
var hasUpstream = !!s.upstream;
|
|
2380
|
+
// Allow pushing commits if we're ahead, if upstream is missing (first push will set it up),
|
|
2381
|
+
// or if ahead is simply unknown — backend will surface real errors.
|
|
2382
|
+
var canPushCommits = ahead > 0 || !hasUpstream || typeof s.ahead !== "number";
|
|
2383
|
+
// Tag count isn't computed locally (would require a network probe). Let the user try.
|
|
2384
|
+
var canPushTags = true;
|
|
2385
|
+
var disabled = isQuickCommitOpInFlight() || (!canPushCommits && !canPushTags);
|
|
2381
2386
|
var mainLabel = state.quickCommitPushing ? "推送中…" : "推送";
|
|
2382
|
-
var
|
|
2383
|
-
var
|
|
2384
|
-
items
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
+
var menuOpen = state.quickCommitOpenMenu === "push";
|
|
2388
|
+
var caretActive = menuOpen ? " is-active" : "";
|
|
2389
|
+
var items = [
|
|
2390
|
+
{ value: "commits", label: "推送 commits", desc: ahead ? "推送 " + ahead + " 个 commit" : "推送当前分支", disabled: !canPushCommits },
|
|
2391
|
+
{ value: "tags", label: "推送 tags", desc: "推送所有本地 tag", disabled: !canPushTags },
|
|
2392
|
+
{ value: "both", label: "推送 commits 和 tags", desc: "二合一", disabled: !(canPushCommits || canPushTags) }
|
|
2393
|
+
];
|
|
2387
2394
|
var menuHtml = items.map(function(it) {
|
|
2388
2395
|
var dis = it.disabled ? " is-disabled" : "";
|
|
2389
2396
|
return '<button type="button" class="qc-dropdown-item' + dis + '" data-qc-push="' + it.value + '"' + (it.disabled ? ' disabled' : '') + '>' +
|
|
@@ -2395,10 +2402,10 @@
|
|
|
2395
2402
|
'<button id="quick-commit-push-btn" class="btn btn-secondary qc-split-main" type="button"' + (disabled ? ' disabled' : '') + '>' +
|
|
2396
2403
|
escapeHtml(mainLabel) +
|
|
2397
2404
|
'</button>' +
|
|
2398
|
-
'<button id="quick-commit-push-caret" class="btn btn-secondary qc-split-caret' + caretActive + '" type="button" data-qc-dropdown-toggle="push"' + (disabled ? ' disabled' : '') + ' aria-haspopup="menu" aria-expanded="' + (
|
|
2405
|
+
'<button id="quick-commit-push-caret" class="btn btn-secondary qc-split-caret' + caretActive + '" type="button" data-qc-dropdown-toggle="push"' + (disabled ? ' disabled' : '') + ' aria-haspopup="menu" aria-expanded="' + (menuOpen ? 'true' : 'false') + '" aria-label="更多推送方式">' +
|
|
2399
2406
|
'<svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true"><path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
|
|
2400
2407
|
'</button>' +
|
|
2401
|
-
(
|
|
2408
|
+
(menuOpen ?
|
|
2402
2409
|
'<div id="quick-commit-push-menu" class="qc-dropdown-menu qc-dropdown-menu--right" data-qc-dropdown-menu="push" role="menu">' + menuHtml + '</div>' : '') +
|
|
2403
2410
|
'</div>';
|
|
2404
2411
|
}
|
|
@@ -2467,11 +2474,11 @@
|
|
|
2467
2474
|
'<code class="qc-head-text">' + escapeHtml(headLine) + '</code>' +
|
|
2468
2475
|
'</div>' +
|
|
2469
2476
|
renderQuickCommitStatusChips(s) +
|
|
2470
|
-
(state.
|
|
2477
|
+
(state.quickCommitOpenMenu === "tag-head" ? renderQuickCommitTagHeadPanel() : '') +
|
|
2471
2478
|
'<div class="qc-section-actions qc-section-actions--secondary">' +
|
|
2472
2479
|
'<button id="quick-commit-tag-head-toggle" class="btn btn-ghost btn-sm" type="button"' + (state.quickCommitPushing ? ' disabled' : '') + '>' +
|
|
2473
2480
|
'<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" style="vertical-align:-2px;margin-right:4px;"><path d="M2 2h6.5l5 5-5.5 5.5L2 7.5V2z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><circle cx="5" cy="5" r="1" fill="currentColor"/></svg>' +
|
|
2474
|
-
(state.
|
|
2481
|
+
(state.quickCommitOpenMenu === "tag-head" ? '收起' : '为 HEAD 打 tag') +
|
|
2475
2482
|
'</button>' +
|
|
2476
2483
|
renderQuickCommitPushButton(s) +
|
|
2477
2484
|
'</div>' +
|
|
@@ -2640,6 +2647,40 @@
|
|
|
2640
2647
|
'</div>' +
|
|
2641
2648
|
'<p id="android-apk-message" class="hint hidden"></p>' +
|
|
2642
2649
|
'</div>' +
|
|
2650
|
+
'<div class="settings-update-section hidden" id="macos-dmg-section">' +
|
|
2651
|
+
'<div class="settings-section-head">' +
|
|
2652
|
+
'<span class="settings-section-icon">🖥️</span>' +
|
|
2653
|
+
'<div class="settings-section-head-text">' +
|
|
2654
|
+
'<h4 class="settings-section-heading">macOS App</h4>' +
|
|
2655
|
+
'<p class="settings-section-sub">原生客户端版本与 DMG 下载</p>' +
|
|
2656
|
+
'</div>' +
|
|
2657
|
+
'</div>' +
|
|
2658
|
+
'<div id="macos-dmg-current-row" class="settings-about-row hidden">' +
|
|
2659
|
+
'<span class="settings-label">当前版本</span>' +
|
|
2660
|
+
'<span class="settings-value" id="settings-macos-dmg-current">-</span>' +
|
|
2661
|
+
'</div>' +
|
|
2662
|
+
'<div id="macos-dmg-github-row" class="settings-about-row settings-about-row-action hidden">' +
|
|
2663
|
+
'<span class="settings-label">线上版本</span>' +
|
|
2664
|
+
'<span class="settings-value settings-value-flex" id="settings-macos-dmg-github">-</span>' +
|
|
2665
|
+
'<button id="download-github-dmg-btn" class="btn btn-secondary btn-sm hidden" type="button">下载</button>' +
|
|
2666
|
+
'</div>' +
|
|
2667
|
+
'<div id="macos-dmg-local-row" class="settings-about-row settings-about-row-action hidden">' +
|
|
2668
|
+
'<span class="settings-label">本地版本</span>' +
|
|
2669
|
+
'<span class="settings-value settings-value-flex" id="settings-macos-dmg-local">-</span>' +
|
|
2670
|
+
'<button id="download-local-dmg-btn" class="btn btn-secondary btn-sm hidden" type="button">下载</button>' +
|
|
2671
|
+
'</div>' +
|
|
2672
|
+
'<div id="macos-auto-update-row" class="settings-toggle-row hidden">' +
|
|
2673
|
+
'<div class="settings-toggle-text">' +
|
|
2674
|
+
'<span class="settings-toggle-title">自动更新</span>' +
|
|
2675
|
+
'<span class="settings-toggle-desc" id="macos-auto-update-hint">检测到新版 DMG 将自动下载并挂载。</span>' +
|
|
2676
|
+
'</div>' +
|
|
2677
|
+
'<label class="settings-switch">' +
|
|
2678
|
+
'<input type="checkbox" id="auto-update-dmg-toggle" class="switch-toggle">' +
|
|
2679
|
+
'<span class="switch-slider"></span>' +
|
|
2680
|
+
'</label>' +
|
|
2681
|
+
'</div>' +
|
|
2682
|
+
'<p id="macos-dmg-message" class="hint hidden"></p>' +
|
|
2683
|
+
'</div>' +
|
|
2643
2684
|
'<div class="settings-update-section" id="android-connect-section">' +
|
|
2644
2685
|
'<div class="settings-section-head">' +
|
|
2645
2686
|
'<span class="settings-section-icon">🔗</span>' +
|
|
@@ -5580,6 +5621,10 @@
|
|
|
5580
5621
|
if (autoUpdateApkToggle) autoUpdateApkToggle.addEventListener("change", function() {
|
|
5581
5622
|
toggleAutoUpdate("apk", autoUpdateApkToggle.checked);
|
|
5582
5623
|
});
|
|
5624
|
+
var autoUpdateDmgToggle = document.getElementById("auto-update-dmg-toggle");
|
|
5625
|
+
if (autoUpdateDmgToggle) autoUpdateDmgToggle.addEventListener("change", function() {
|
|
5626
|
+
toggleAutoUpdate("dmg", autoUpdateDmgToggle.checked);
|
|
5627
|
+
});
|
|
5583
5628
|
var copyConnectCodeBtn = document.getElementById("copy-connect-code-button");
|
|
5584
5629
|
if (copyConnectCodeBtn) copyConnectCodeBtn.addEventListener("click", function() {
|
|
5585
5630
|
var text = document.getElementById("android-connect-code");
|
|
@@ -7279,7 +7324,13 @@
|
|
|
7279
7324
|
var delta = normalizedOutput.slice(currentOutput.length);
|
|
7280
7325
|
if (delta) {
|
|
7281
7326
|
wandTerminalWrite(state.terminal, delta);
|
|
7282
|
-
|
|
7327
|
+
// 不在流式 chunk 路径触发 softResyncTerminal —— resync 会
|
|
7328
|
+
// resetTerminal() + 完整重放整段 buffer,重放期间所有 cursor-home
|
|
7329
|
+
// + 重画序列把"被覆盖的中间帧"反复塞进 main-screen scrollback,
|
|
7330
|
+
// 表现为 PTY 视图里同一段回答被画 N 遍(PC 端列宽大、Claude TUI
|
|
7331
|
+
// 重画序列密集时最严重)。响应结束的 thinking→idle 边界已经做了
|
|
7332
|
+
// 一次 softResync 兜底,足以清掉流式残留的错位光标 DOM;流式
|
|
7333
|
+
// 过程中持续重放只是纯粹的重复制造器。
|
|
7283
7334
|
wrote = true;
|
|
7284
7335
|
}
|
|
7285
7336
|
} else if (currentOutput && currentOutput.startsWith(normalizedOutput)) {
|
|
@@ -9222,6 +9273,8 @@
|
|
|
9222
9273
|
if (autoUpdateWebToggle) autoUpdateWebToggle.checked = !!autoUpdate.web;
|
|
9223
9274
|
var autoUpdateApkToggle = document.getElementById("auto-update-apk-toggle");
|
|
9224
9275
|
if (autoUpdateApkToggle) autoUpdateApkToggle.checked = !!autoUpdate.apk;
|
|
9276
|
+
var autoUpdateDmgToggle = document.getElementById("auto-update-dmg-toggle");
|
|
9277
|
+
if (autoUpdateDmgToggle) autoUpdateDmgToggle.checked = !!autoUpdate.dmg;
|
|
9225
9278
|
|
|
9226
9279
|
// ── Android APK version display ──
|
|
9227
9280
|
var apkSection = document.getElementById("android-apk-section");
|
|
@@ -9341,6 +9394,119 @@
|
|
|
9341
9394
|
}
|
|
9342
9395
|
}
|
|
9343
9396
|
|
|
9397
|
+
// ── macOS DMG version display ──
|
|
9398
|
+
var dmgSection = document.getElementById("macos-dmg-section");
|
|
9399
|
+
var dmgCurrentRow = document.getElementById("macos-dmg-current-row");
|
|
9400
|
+
var dmgCurrentEl = document.getElementById("settings-macos-dmg-current");
|
|
9401
|
+
var dmgGithubRow = document.getElementById("macos-dmg-github-row");
|
|
9402
|
+
var dmgGithubEl = document.getElementById("settings-macos-dmg-github");
|
|
9403
|
+
var dmgGithubBtn = document.getElementById("download-github-dmg-btn");
|
|
9404
|
+
var dmgLocalRow = document.getElementById("macos-dmg-local-row");
|
|
9405
|
+
var dmgLocalEl = document.getElementById("settings-macos-dmg-local");
|
|
9406
|
+
var dmgLocalBtn = document.getElementById("download-local-dmg-btn");
|
|
9407
|
+
var dmgMessageEl = document.getElementById("macos-dmg-message");
|
|
9408
|
+
var macosDmg = data.macosDmg || {};
|
|
9409
|
+
var isInMacApp = !!_macAppVersion;
|
|
9410
|
+
var hasDmgInfo = isInMacApp || !!macosDmg.github || !!macosDmg.local;
|
|
9411
|
+
if (dmgSection) {
|
|
9412
|
+
if (hasDmgInfo) dmgSection.classList.remove("hidden");
|
|
9413
|
+
else dmgSection.classList.add("hidden");
|
|
9414
|
+
}
|
|
9415
|
+
|
|
9416
|
+
if (isInMacApp) {
|
|
9417
|
+
// ── macOS 壳内:显示当前版本 + 线上 + 本地 + 下载安装按钮 ──
|
|
9418
|
+
if (dmgCurrentRow && dmgCurrentEl) {
|
|
9419
|
+
dmgCurrentEl.textContent = "v" + _macAppVersion;
|
|
9420
|
+
dmgCurrentRow.classList.remove("hidden");
|
|
9421
|
+
}
|
|
9422
|
+
if (macosDmg.github && dmgGithubRow && dmgGithubEl) {
|
|
9423
|
+
var dghLabel = macosDmg.github.version ? ("v" + macosDmg.github.version) : macosDmg.github.fileName;
|
|
9424
|
+
if (typeof macosDmg.github.size === "number") dghLabel += " · " + formatBytes(macosDmg.github.size);
|
|
9425
|
+
dmgGithubEl.textContent = dghLabel;
|
|
9426
|
+
dmgGithubRow.classList.remove("hidden");
|
|
9427
|
+
if (dmgGithubBtn) {
|
|
9428
|
+
dmgGithubBtn.textContent = "下载安装";
|
|
9429
|
+
dmgGithubBtn.classList.remove("hidden");
|
|
9430
|
+
dmgGithubBtn.onclick = function() {
|
|
9431
|
+
try {
|
|
9432
|
+
WandNative.downloadUpdate(macosDmg.github.downloadUrl, macosDmg.github.fileName || "wand-update.dmg", "github");
|
|
9433
|
+
} catch (e) {
|
|
9434
|
+
if (typeof window.wandAlert === "function") {
|
|
9435
|
+
window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
|
|
9436
|
+
} else if (typeof showToast === "function") {
|
|
9437
|
+
showToast("调用下载失败: " + e.message, "error");
|
|
9438
|
+
} else {
|
|
9439
|
+
alert("调用下载失败: " + e.message);
|
|
9440
|
+
}
|
|
9441
|
+
}
|
|
9442
|
+
};
|
|
9443
|
+
}
|
|
9444
|
+
}
|
|
9445
|
+
if (macosDmg.local && dmgLocalRow && dmgLocalEl) {
|
|
9446
|
+
var dlcLabel = macosDmg.local.version ? ("v" + macosDmg.local.version) : macosDmg.local.fileName;
|
|
9447
|
+
if (typeof macosDmg.local.size === "number") dlcLabel += " · " + formatBytes(macosDmg.local.size);
|
|
9448
|
+
dmgLocalEl.textContent = dlcLabel;
|
|
9449
|
+
dmgLocalRow.classList.remove("hidden");
|
|
9450
|
+
if (dmgLocalBtn) {
|
|
9451
|
+
dmgLocalBtn.textContent = "下载安装";
|
|
9452
|
+
dmgLocalBtn.classList.remove("hidden");
|
|
9453
|
+
dmgLocalBtn.onclick = function() {
|
|
9454
|
+
try {
|
|
9455
|
+
WandNative.downloadUpdate(macosDmg.local.downloadUrl, macosDmg.local.fileName || "wand-update.dmg", "local");
|
|
9456
|
+
} catch (e) {
|
|
9457
|
+
if (typeof window.wandAlert === "function") {
|
|
9458
|
+
window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
|
|
9459
|
+
} else if (typeof showToast === "function") {
|
|
9460
|
+
showToast("调用下载失败: " + e.message, "error");
|
|
9461
|
+
} else {
|
|
9462
|
+
alert("调用下载失败: " + e.message);
|
|
9463
|
+
}
|
|
9464
|
+
}
|
|
9465
|
+
};
|
|
9466
|
+
}
|
|
9467
|
+
}
|
|
9468
|
+
if (!macosDmg.github && !macosDmg.local && dmgMessageEl) {
|
|
9469
|
+
dmgMessageEl.textContent = "暂无可用更新";
|
|
9470
|
+
dmgMessageEl.classList.remove("hidden");
|
|
9471
|
+
}
|
|
9472
|
+
var dmgAutoRow = document.getElementById("macos-auto-update-row");
|
|
9473
|
+
var dmgAutoHint = document.getElementById("macos-auto-update-hint");
|
|
9474
|
+
if (dmgAutoRow) dmgAutoRow.classList.remove("hidden");
|
|
9475
|
+
if (dmgAutoHint) dmgAutoHint.classList.remove("hidden");
|
|
9476
|
+
} else {
|
|
9477
|
+
// ── 浏览器模式:仅展示下载入口 ──
|
|
9478
|
+
if (macosDmg.github && dmgGithubRow && dmgGithubEl) {
|
|
9479
|
+
var dghLabel2 = macosDmg.github.version ? ("v" + macosDmg.github.version) : macosDmg.github.fileName;
|
|
9480
|
+
if (typeof macosDmg.github.size === "number") dghLabel2 += " · " + formatBytes(macosDmg.github.size);
|
|
9481
|
+
dmgGithubEl.textContent = dghLabel2;
|
|
9482
|
+
dmgGithubRow.classList.remove("hidden");
|
|
9483
|
+
if (dmgGithubBtn) {
|
|
9484
|
+
dmgGithubBtn.textContent = "下载";
|
|
9485
|
+
dmgGithubBtn.classList.remove("hidden");
|
|
9486
|
+
dmgGithubBtn.onclick = function() {
|
|
9487
|
+
window.open(macosDmg.github.downloadUrl, "_blank");
|
|
9488
|
+
};
|
|
9489
|
+
}
|
|
9490
|
+
}
|
|
9491
|
+
if (macosDmg.local && dmgLocalRow && dmgLocalEl) {
|
|
9492
|
+
var dlcLabel2 = macosDmg.local.version ? ("v" + macosDmg.local.version) : macosDmg.local.fileName;
|
|
9493
|
+
if (typeof macosDmg.local.size === "number") dlcLabel2 += " · " + formatBytes(macosDmg.local.size);
|
|
9494
|
+
dmgLocalEl.textContent = dlcLabel2;
|
|
9495
|
+
dmgLocalRow.classList.remove("hidden");
|
|
9496
|
+
if (dmgLocalBtn) {
|
|
9497
|
+
dmgLocalBtn.textContent = "下载";
|
|
9498
|
+
dmgLocalBtn.classList.remove("hidden");
|
|
9499
|
+
dmgLocalBtn.onclick = function() {
|
|
9500
|
+
window.open(macosDmg.local.downloadUrl, "_self");
|
|
9501
|
+
};
|
|
9502
|
+
}
|
|
9503
|
+
}
|
|
9504
|
+
if (!macosDmg.github && !macosDmg.local && dmgMessageEl) {
|
|
9505
|
+
dmgMessageEl.textContent = "暂未提供";
|
|
9506
|
+
dmgMessageEl.classList.remove("hidden");
|
|
9507
|
+
}
|
|
9508
|
+
}
|
|
9509
|
+
|
|
9344
9510
|
// App connect code (encrypted)
|
|
9345
9511
|
var connectCodeEl = document.getElementById("android-connect-code");
|
|
9346
9512
|
var connectQrCanvas = document.getElementById("android-connect-qr");
|
|
@@ -9710,6 +9876,24 @@
|
|
|
9710
9876
|
.catch(function() {});
|
|
9711
9877
|
}
|
|
9712
9878
|
|
|
9879
|
+
function checkDmgAutoUpdate() {
|
|
9880
|
+
if (!_macAppVersion) return;
|
|
9881
|
+
fetch("/api/auto-update", { credentials: "same-origin" })
|
|
9882
|
+
.then(function(res) { return res.json(); })
|
|
9883
|
+
.then(function(autoData) {
|
|
9884
|
+
if (!autoData.dmg) return;
|
|
9885
|
+
return fetch("/api/macos-dmg-update?currentVersion=" + encodeURIComponent(_macAppVersion), { credentials: "same-origin" })
|
|
9886
|
+
.then(function(res) { return res.json(); })
|
|
9887
|
+
.then(function(data) {
|
|
9888
|
+
if (!data.updateAvailable || !data.downloadUrl) return;
|
|
9889
|
+
try {
|
|
9890
|
+
WandNative.downloadUpdate(data.downloadUrl, data.fileName || "wand-update.dmg", data.source || "local");
|
|
9891
|
+
} catch (_e) {}
|
|
9892
|
+
});
|
|
9893
|
+
})
|
|
9894
|
+
.catch(function() {});
|
|
9895
|
+
}
|
|
9896
|
+
|
|
9713
9897
|
function toggleAutoUpdate(type, enabled) {
|
|
9714
9898
|
var body = {};
|
|
9715
9899
|
body[type] = enabled;
|
|
@@ -9724,8 +9908,10 @@
|
|
|
9724
9908
|
// Sync toggle state with server response
|
|
9725
9909
|
var webToggle = document.getElementById("auto-update-web-toggle");
|
|
9726
9910
|
var apkToggle = document.getElementById("auto-update-apk-toggle");
|
|
9911
|
+
var dmgToggle = document.getElementById("auto-update-dmg-toggle");
|
|
9727
9912
|
if (webToggle) webToggle.checked = !!data.web;
|
|
9728
9913
|
if (apkToggle) apkToggle.checked = !!data.apk;
|
|
9914
|
+
if (dmgToggle) dmgToggle.checked = !!data.dmg;
|
|
9729
9915
|
})
|
|
9730
9916
|
.catch(function() {
|
|
9731
9917
|
// Revert toggle on failure
|
|
@@ -13870,6 +14056,35 @@
|
|
|
13870
14056
|
|
|
13871
14057
|
// Drop any in-flight socket and start a new one *now* — used by the
|
|
13872
14058
|
// Android resume bridge to recover from zombie connections (socket
|
|
14059
|
+
// 客户端 WS 心跳检测:每 10s 跑一次,看 lastWsMessageAt 距今多久。
|
|
14060
|
+
// 服务端每 20s 主动下推 {type:"ping"},所以 40s 没消息就明确是半开。
|
|
14061
|
+
// 浏览器在 background 时 setInterval 会被节流到 ~1Hz 或更慢,但
|
|
14062
|
+
// 我们也在 visibilitychange→visible 里做了一次主动评估,所以
|
|
14063
|
+
// 切回前台时不会拖很久才发现 stale。
|
|
14064
|
+
var WS_HEARTBEAT_CHECK_MS = 10_000;
|
|
14065
|
+
var WS_HEARTBEAT_STALE_MS = 40_000;
|
|
14066
|
+
function startWsHeartbeatCheck() {
|
|
14067
|
+
stopWsHeartbeatCheck();
|
|
14068
|
+
state.wsHeartbeatCheckTimer = setInterval(evaluateWsHeartbeatStale, WS_HEARTBEAT_CHECK_MS);
|
|
14069
|
+
}
|
|
14070
|
+
function stopWsHeartbeatCheck() {
|
|
14071
|
+
if (state.wsHeartbeatCheckTimer) {
|
|
14072
|
+
clearInterval(state.wsHeartbeatCheckTimer);
|
|
14073
|
+
state.wsHeartbeatCheckTimer = null;
|
|
14074
|
+
}
|
|
14075
|
+
}
|
|
14076
|
+
function evaluateWsHeartbeatStale() {
|
|
14077
|
+
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
|
|
14078
|
+
// 第一帧(包括 onopen)会刷新 lastWsMessageAt;如果还是 0 说明刚连上
|
|
14079
|
+
// 但服务端没下发任何东西(连 init 都没发出来)——交给下一轮检查。
|
|
14080
|
+
if (!state.lastWsMessageAt) return;
|
|
14081
|
+
var idle = Date.now() - state.lastWsMessageAt;
|
|
14082
|
+
if (idle > WS_HEARTBEAT_STALE_MS) {
|
|
14083
|
+
forceReconnectWebSocket("heartbeat-stale-" + Math.round(idle / 1000) + "s");
|
|
14084
|
+
}
|
|
14085
|
+
}
|
|
14086
|
+
|
|
14087
|
+
// Force a fresh WebSocket connection even if the existing one
|
|
13873
14088
|
// still says OPEN, but the TCP path was torn down by Doze). Skips
|
|
13874
14089
|
// the backoff timer; the caller has already decided this is urgent.
|
|
13875
14090
|
function forceReconnectWebSocket(reason) {
|
|
@@ -13880,6 +14095,13 @@
|
|
|
13880
14095
|
// reconnect path while we're already starting a fresh one.
|
|
13881
14096
|
try { stale.onclose = null; } catch (e) { /* ignore */ }
|
|
13882
14097
|
try { stale.onerror = null; } catch (e) { /* ignore */ }
|
|
14098
|
+
// 也清掉 onmessage:close() 是异步的,TCP RST/Close 帧到达之前,浏览器
|
|
14099
|
+
// socket 缓冲区里可能还有几条 in-flight 帧没派发。一旦它们在新 ws 已
|
|
14100
|
+
// open + init 之后再触发,老 ws 的 output 会被 handleWebSocketMessage
|
|
14101
|
+
// 当成"新增量"写进 wterm,造成"刚才那段又被画了一遍"。stale 心跳触发
|
|
14102
|
+
// 的 force reconnect 比 onclose 触发更早,老 ws 仍处于 OPEN,这个窗口
|
|
14103
|
+
// 更宽,必须显式 detach。
|
|
14104
|
+
try { stale.onmessage = null; } catch (e) { /* ignore */ }
|
|
13883
14105
|
try { stale.close(); } catch (e) { /* ignore */ }
|
|
13884
14106
|
state.ws = null;
|
|
13885
14107
|
}
|
|
@@ -13926,6 +14148,7 @@
|
|
|
13926
14148
|
ws.onopen = function() {
|
|
13927
14149
|
state.ws = ws;
|
|
13928
14150
|
state.wsConnected = true;
|
|
14151
|
+
state.lastWsMessageAt = Date.now();
|
|
13929
14152
|
// Reset backoff on a successful connect so the next disconnect
|
|
13930
14153
|
// starts the ladder from 500ms again.
|
|
13931
14154
|
state.wsReconnectAttempts = 0;
|
|
@@ -13933,6 +14156,9 @@
|
|
|
13933
14156
|
// Server's per-client output sequence counter restarts on every
|
|
13934
14157
|
// new socket; clear ours so the first init isn't treated as a gap.
|
|
13935
14158
|
state.lastSeqBySession = {};
|
|
14159
|
+
// 启动客户端心跳检测:每 10s 检查一次 lastWsMessageAt,超过 40s
|
|
14160
|
+
// 没收到任何消息(包括服务端 20s 一次的 ping)就视为半开连接。
|
|
14161
|
+
startWsHeartbeatCheck();
|
|
13936
14162
|
// Subscribe to current session if any
|
|
13937
14163
|
subscribeToSession(state.selectedId);
|
|
13938
14164
|
// Flush pending messages after reconnection
|
|
@@ -13947,8 +14173,20 @@
|
|
|
13947
14173
|
};
|
|
13948
14174
|
|
|
13949
14175
|
ws.onmessage = function(event) {
|
|
14176
|
+
// 任意服务端消息都说明连接活着,先刷新心跳计时。
|
|
14177
|
+
state.lastWsMessageAt = Date.now();
|
|
13950
14178
|
try {
|
|
13951
14179
|
var msg = JSON.parse(event.data);
|
|
14180
|
+
// 应用层 ping:立刻回 pong。同时也让服务端能算 RTT。
|
|
14181
|
+
// 这条消息处理完就 return,不进入下面的 sessionId 分发流程。
|
|
14182
|
+
if (msg && msg.type === "ping") {
|
|
14183
|
+
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
|
14184
|
+
try {
|
|
14185
|
+
state.ws.send(JSON.stringify({ type: "pong", t: msg.t }));
|
|
14186
|
+
} catch (sendErr) { /* ignore */ }
|
|
14187
|
+
}
|
|
14188
|
+
return;
|
|
14189
|
+
}
|
|
13952
14190
|
if (msg && msg.type === "resync_required" && msg.sessionId) {
|
|
13953
14191
|
// Server dropped some output events under backpressure and
|
|
13954
14192
|
// is asking us for a fresh snapshot. Send a resync so the
|
|
@@ -13995,6 +14233,7 @@
|
|
|
13995
14233
|
ws.onclose = function() {
|
|
13996
14234
|
state.ws = null;
|
|
13997
14235
|
state.wsConnected = false;
|
|
14236
|
+
stopWsHeartbeatCheck();
|
|
13998
14237
|
scheduleWsReconnect();
|
|
13999
14238
|
};
|
|
14000
14239
|
|
|
@@ -14128,7 +14367,9 @@
|
|
|
14128
14367
|
// 变化的视觉错位无法被自愈,直到用户手动改窗口才修。现在让
|
|
14129
14368
|
// wterm 内部 ResizeObserver 独占 cols 跟踪职责。
|
|
14130
14369
|
wandTerminalWrite(state.terminal, msg.data.chunk);
|
|
14131
|
-
|
|
14370
|
+
// 同 syncTerminalBuffer 的 delta 分支:流式 chunk 不再触发
|
|
14371
|
+
// softResyncTerminal,避免完整重放把 cursor-home + 重画的
|
|
14372
|
+
// "被覆盖中间帧"反复塞进 scrollback。thinking→idle 兜底就够了。
|
|
14132
14373
|
state.terminalSessionId = msg.sessionId;
|
|
14133
14374
|
if (msg.data.output) {
|
|
14134
14375
|
state.terminalOutput = clampClientTerminalOutput(normalizeTerminalOutput(msg.data.output));
|
|
@@ -14264,10 +14505,24 @@
|
|
|
14264
14505
|
renderChat(true);
|
|
14265
14506
|
updateTaskDisplay();
|
|
14266
14507
|
updateApprovalStats();
|
|
14267
|
-
//
|
|
14268
|
-
//
|
|
14269
|
-
//
|
|
14270
|
-
|
|
14508
|
+
// 服务端 ring buffer 在多数场景下是当前已渲染输出的严格 superset
|
|
14509
|
+
// (同一 PTY,buffer 只增不减)。这种情况下走 append delta 就够了,
|
|
14510
|
+
// 不应该强制 replace。replace 会触发 resetTerminal + 全量重放整段
|
|
14511
|
+
// output,wterm 把 ANSI cursor-home 重画的"中间帧"全部塞进
|
|
14512
|
+
// scrollback,造成"同一段回答在 PTY 视图里被画 N 遍"——尤其是
|
|
14513
|
+
// 锁屏 / 切回前台 / 心跳 stale 触发 reconnect 时,每次 init 都重放
|
|
14514
|
+
// 一次,累积重复非常显著。
|
|
14515
|
+
//
|
|
14516
|
+
// 只有真的不连续(会话切换、ring buffer 截断了头部、output 不是
|
|
14517
|
+
// currentOutput 的严格前缀延伸)才回退到 replace 的全量基线。
|
|
14518
|
+
var initOutput = msg.data.output || "";
|
|
14519
|
+
var sameTerminalSession = state.terminalSessionId === msg.sessionId;
|
|
14520
|
+
var currTerminalOutput = state.terminalOutput || "";
|
|
14521
|
+
var canAppendDelta = sameTerminalSession
|
|
14522
|
+
&& currTerminalOutput.length > 0
|
|
14523
|
+
&& initOutput.length >= currTerminalOutput.length
|
|
14524
|
+
&& initOutput.startsWith(currTerminalOutput);
|
|
14525
|
+
updateTerminalOutput(initOutput, msg.sessionId, canAppendDelta ? "append" : "replace");
|
|
14271
14526
|
// wterm 启动 cols=120,replace 写入可能落在错的列宽上;ResizeObserver
|
|
14272
14527
|
// 回调异步,用 fit-with-retry 兜一次确保按真实宽度重排。
|
|
14273
14528
|
ensureTerminalFitWithRetry("init");
|
|
@@ -17762,11 +18017,43 @@
|
|
|
17762
18017
|
|
|
17763
18018
|
// ── Browser Notification API ──
|
|
17764
18019
|
|
|
18020
|
+
// macOS WKWebView shim: Android shell injects a global WandNative via
|
|
18021
|
+
// addJavascriptInterface, but WKWebView only exposes
|
|
18022
|
+
// webkit.messageHandlers.<name>.postMessage(...). To keep call-sites
|
|
18023
|
+
// identical across platforms, we synthesize a WandNative-shaped object
|
|
18024
|
+
// when running inside the macOS shell.
|
|
18025
|
+
try {
|
|
18026
|
+
var _macUaMatch = navigator.userAgent.match(/WandPlatform\/macOS/);
|
|
18027
|
+
var _macHandler = (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.wandNative) || null;
|
|
18028
|
+
if (_macUaMatch && _macHandler && typeof window.WandNative === "undefined") {
|
|
18029
|
+
window.WandNative = {
|
|
18030
|
+
// Only downloadUpdate is wired for now; other Android-specific
|
|
18031
|
+
// methods (notifications, haptics, screen wake) intentionally
|
|
18032
|
+
// omitted so feature detection falls back to web APIs on macOS.
|
|
18033
|
+
downloadUpdate: function(url, fileName, source) {
|
|
18034
|
+
try {
|
|
18035
|
+
_macHandler.postMessage({
|
|
18036
|
+
type: "downloadUpdate",
|
|
18037
|
+
url: String(url || ""),
|
|
18038
|
+
fileName: String(fileName || "wand-update.dmg"),
|
|
18039
|
+
source: String(source || "local"),
|
|
18040
|
+
});
|
|
18041
|
+
} catch (_e) {}
|
|
18042
|
+
},
|
|
18043
|
+
};
|
|
18044
|
+
}
|
|
18045
|
+
} catch (_e) {}
|
|
18046
|
+
|
|
17765
18047
|
// Detect Android APK native bridge
|
|
17766
18048
|
var _hasNativeBridge = typeof WandNative !== "undefined" && typeof WandNative.sendNotification === "function";
|
|
17767
|
-
//
|
|
17768
|
-
|
|
17769
|
-
|
|
18049
|
+
// Extract WandApp/<version> from User-Agent (set by both Android and macOS shells).
|
|
18050
|
+
// We distinguish platforms by the additional WandPlatform/<name> token —
|
|
18051
|
+
// macOS UA ends with "WandApp/X WandPlatform/macOS".
|
|
18052
|
+
var _wandAppMatch = navigator.userAgent.match(/WandApp\/([^\s]+)/);
|
|
18053
|
+
var _isMacApp = /WandPlatform\/macOS/.test(navigator.userAgent);
|
|
18054
|
+
var _isAndroidApp = !!_wandAppMatch && !_isMacApp;
|
|
18055
|
+
var _apkVersion = (_wandAppMatch && _isAndroidApp) ? _wandAppMatch[1] : null;
|
|
18056
|
+
var _macAppVersion = (_wandAppMatch && _isMacApp) ? _wandAppMatch[1] : null;
|
|
17770
18057
|
|
|
17771
18058
|
function _vibrate(pattern) {
|
|
17772
18059
|
if (!_hasNativeBridge || typeof WandNative.vibrate !== "function") return;
|