@co0ontty/wand 1.26.0 → 1.29.0
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/config.js +24 -0
- 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/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 +375 -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>' +
|
|
@@ -1448,6 +1467,15 @@
|
|
|
1448
1467
|
'</button>' +
|
|
1449
1468
|
'</div>' +
|
|
1450
1469
|
'</div>' +
|
|
1470
|
+
'<div class="sidebar-rail" id="sidebar-rail" aria-hidden="true">' +
|
|
1471
|
+
'<button class="sidebar-rail-expand" id="sidebar-rail-expand" type="button" title="展开侧栏" aria-label="展开侧栏">' +
|
|
1472
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="10 6 16 12 10 18"/><line x1="20" y1="5" x2="20" y2="19"/></svg>' +
|
|
1473
|
+
'</button>' +
|
|
1474
|
+
'<div class="sidebar-rail-list" id="sidebar-rail-list">' + renderRailSessions() + '</div>' +
|
|
1475
|
+
'<button class="sidebar-rail-new" id="sidebar-rail-new" type="button" title="新会话" aria-label="新会话">' +
|
|
1476
|
+
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>' +
|
|
1477
|
+
'</button>' +
|
|
1478
|
+
'</div>' +
|
|
1451
1479
|
'</aside>' +
|
|
1452
1480
|
'<main class="main-content">' +
|
|
1453
1481
|
'<div class="main-header-row">' +
|
|
@@ -1816,13 +1844,11 @@
|
|
|
1816
1844
|
tag: "",
|
|
1817
1845
|
primaryAction: readSavedPrimaryAction(),
|
|
1818
1846
|
};
|
|
1819
|
-
state.
|
|
1820
|
-
state.quickCommitTagHeadOpen = false;
|
|
1847
|
+
state.quickCommitOpenMenu = null;
|
|
1821
1848
|
state.quickCommitTagHeadForm = { tag: "", push: false };
|
|
1822
1849
|
state.quickCommitTagHeadSubmitting = false;
|
|
1823
1850
|
state.quickCommitTagHeadGenerating = false;
|
|
1824
1851
|
state.quickCommitTagHeadError = "";
|
|
1825
|
-
state.quickCommitPushMenuOpen = false;
|
|
1826
1852
|
state.quickCommitPushing = false;
|
|
1827
1853
|
state.quickCommitPushError = "";
|
|
1828
1854
|
closeWorktreeMergeModal();
|
|
@@ -1839,10 +1865,8 @@
|
|
|
1839
1865
|
quickCommitEscHandler = function(e) {
|
|
1840
1866
|
if (e.key === "Escape" && state.quickCommitOpen && !state.quickCommitSubmitting && !state.quickCommitTagHeadSubmitting && !state.quickCommitPushing) {
|
|
1841
1867
|
// First Esc closes any open dropdown; second closes the modal.
|
|
1842
|
-
if (state.
|
|
1843
|
-
state.
|
|
1844
|
-
state.quickCommitPushMenuOpen = false;
|
|
1845
|
-
state.quickCommitTagHeadOpen = false;
|
|
1868
|
+
if (state.quickCommitOpenMenu) {
|
|
1869
|
+
state.quickCommitOpenMenu = null;
|
|
1846
1870
|
rerenderQuickCommitModal();
|
|
1847
1871
|
return;
|
|
1848
1872
|
}
|
|
@@ -1853,18 +1877,16 @@
|
|
|
1853
1877
|
if (quickCommitDocClickHandler) document.removeEventListener("click", quickCommitDocClickHandler, true);
|
|
1854
1878
|
quickCommitDocClickHandler = function(e) {
|
|
1855
1879
|
if (!state.quickCommitOpen) return;
|
|
1880
|
+
// tag-head is an inline drawer, not a dropdown — clicking outside shouldn't close it.
|
|
1881
|
+
if (state.quickCommitOpenMenu !== "action" && state.quickCommitOpenMenu !== "push") return;
|
|
1856
1882
|
var modalEl = document.getElementById("quick-commit-modal");
|
|
1857
1883
|
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
1884
|
var t = e.target;
|
|
1862
1885
|
while (t && t !== modalEl) {
|
|
1863
1886
|
if (t.dataset && (t.dataset.qcDropdownToggle || t.dataset.qcDropdownMenu)) return;
|
|
1864
1887
|
t = t.parentNode;
|
|
1865
1888
|
}
|
|
1866
|
-
state.
|
|
1867
|
-
state.quickCommitPushMenuOpen = false;
|
|
1889
|
+
state.quickCommitOpenMenu = null;
|
|
1868
1890
|
rerenderQuickCommitModal();
|
|
1869
1891
|
};
|
|
1870
1892
|
document.addEventListener("click", quickCommitDocClickHandler, true);
|
|
@@ -1878,9 +1900,7 @@
|
|
|
1878
1900
|
state.quickCommitOpen = false;
|
|
1879
1901
|
state.quickCommitSubmitting = false;
|
|
1880
1902
|
state.quickCommitError = "";
|
|
1881
|
-
state.
|
|
1882
|
-
state.quickCommitPushMenuOpen = false;
|
|
1883
|
-
state.quickCommitTagHeadOpen = false;
|
|
1903
|
+
state.quickCommitOpenMenu = null;
|
|
1884
1904
|
var modal = document.getElementById("quick-commit-modal");
|
|
1885
1905
|
if (modal) modal.classList.add("hidden");
|
|
1886
1906
|
if (focusTrapHandler) {
|
|
@@ -1918,7 +1938,6 @@
|
|
|
1918
1938
|
var cancelBtn = document.getElementById("quick-commit-cancel-btn");
|
|
1919
1939
|
if (cancelBtn) cancelBtn.addEventListener("click", closeQuickCommitModal);
|
|
1920
1940
|
|
|
1921
|
-
// ── Section 1: commit form ──
|
|
1922
1941
|
var submitBtn = document.getElementById("quick-commit-submit-btn");
|
|
1923
1942
|
if (submitBtn) submitBtn.addEventListener("click", submitQuickCommit);
|
|
1924
1943
|
var aiBtn = document.getElementById("quick-commit-ai-btn");
|
|
@@ -1938,12 +1957,10 @@
|
|
|
1938
1957
|
state.quickCommitForm.tag = tagInput.value;
|
|
1939
1958
|
});
|
|
1940
1959
|
|
|
1941
|
-
// ── Primary action split-button (caret + dropdown) ──
|
|
1942
1960
|
var actionCaret = document.getElementById("quick-commit-action-caret");
|
|
1943
1961
|
if (actionCaret) actionCaret.addEventListener("click", function(e) {
|
|
1944
1962
|
e.stopPropagation();
|
|
1945
|
-
state.
|
|
1946
|
-
state.quickCommitPushMenuOpen = false;
|
|
1963
|
+
state.quickCommitOpenMenu = state.quickCommitOpenMenu === "action" ? null : "action";
|
|
1947
1964
|
rerenderQuickCommitModal();
|
|
1948
1965
|
});
|
|
1949
1966
|
var actionMenu = document.getElementById("quick-commit-action-menu");
|
|
@@ -1957,29 +1974,27 @@
|
|
|
1957
1974
|
state.quickCommitForm.primaryAction = value;
|
|
1958
1975
|
savePrimaryAction(value);
|
|
1959
1976
|
}
|
|
1960
|
-
state.
|
|
1977
|
+
state.quickCommitOpenMenu = null;
|
|
1961
1978
|
rerenderQuickCommitModal();
|
|
1962
1979
|
});
|
|
1963
1980
|
})(actionItems[i]);
|
|
1964
1981
|
}
|
|
1965
1982
|
}
|
|
1966
1983
|
|
|
1967
|
-
// ── Section 2: Tag HEAD inline panel ──
|
|
1968
1984
|
var tagHeadToggle = document.getElementById("quick-commit-tag-head-toggle");
|
|
1969
1985
|
if (tagHeadToggle) tagHeadToggle.addEventListener("click", function() {
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
}
|
|
1986
|
+
var willOpen = state.quickCommitOpenMenu !== "tag-head";
|
|
1987
|
+
state.quickCommitOpenMenu = willOpen ? "tag-head" : null;
|
|
1988
|
+
if (willOpen) state.quickCommitTagHeadError = "";
|
|
1974
1989
|
rerenderQuickCommitModal();
|
|
1975
|
-
if (
|
|
1990
|
+
if (willOpen) {
|
|
1976
1991
|
var inp = document.getElementById("quick-commit-tag-head-input");
|
|
1977
1992
|
if (inp) inp.focus();
|
|
1978
1993
|
}
|
|
1979
1994
|
});
|
|
1980
1995
|
var tagHeadCancel = document.getElementById("quick-commit-tag-head-cancel");
|
|
1981
1996
|
if (tagHeadCancel) tagHeadCancel.addEventListener("click", function() {
|
|
1982
|
-
state.
|
|
1997
|
+
if (state.quickCommitOpenMenu === "tag-head") state.quickCommitOpenMenu = null;
|
|
1983
1998
|
state.quickCommitTagHeadError = "";
|
|
1984
1999
|
rerenderQuickCommitModal();
|
|
1985
2000
|
});
|
|
@@ -2006,7 +2021,6 @@
|
|
|
2006
2021
|
submitTagHead(false);
|
|
2007
2022
|
});
|
|
2008
2023
|
|
|
2009
|
-
// ── Section 2: Push split-button ──
|
|
2010
2024
|
var pushBtn = document.getElementById("quick-commit-push-btn");
|
|
2011
2025
|
if (pushBtn) pushBtn.addEventListener("click", function() {
|
|
2012
2026
|
submitPushOnly({ pushCommits: true, pushTags: false });
|
|
@@ -2014,8 +2028,7 @@
|
|
|
2014
2028
|
var pushCaret = document.getElementById("quick-commit-push-caret");
|
|
2015
2029
|
if (pushCaret) pushCaret.addEventListener("click", function(e) {
|
|
2016
2030
|
e.stopPropagation();
|
|
2017
|
-
state.
|
|
2018
|
-
state.quickCommitActionMenuOpen = false;
|
|
2031
|
+
state.quickCommitOpenMenu = state.quickCommitOpenMenu === "push" ? null : "push";
|
|
2019
2032
|
rerenderQuickCommitModal();
|
|
2020
2033
|
});
|
|
2021
2034
|
var pushMenu = document.getElementById("quick-commit-push-menu");
|
|
@@ -2025,7 +2038,7 @@
|
|
|
2025
2038
|
(function(btn) {
|
|
2026
2039
|
btn.addEventListener("click", function() {
|
|
2027
2040
|
var value = btn.getAttribute("data-qc-push");
|
|
2028
|
-
state.
|
|
2041
|
+
state.quickCommitOpenMenu = null;
|
|
2029
2042
|
if (value === "commits") submitPushOnly({ pushCommits: true, pushTags: false });
|
|
2030
2043
|
else if (value === "tags") submitPushOnly({ pushCommits: false, pushTags: true });
|
|
2031
2044
|
else if (value === "both") submitPushOnly({ pushCommits: true, pushTags: true });
|
|
@@ -2119,7 +2132,11 @@
|
|
|
2119
2132
|
var data = result.data || {};
|
|
2120
2133
|
var hash = data.commit && data.commit.hash ? data.commit.hash.substring(0, 7) : "";
|
|
2121
2134
|
var tagName = data.tag && data.tag.name ? data.tag.name : "";
|
|
2122
|
-
var
|
|
2135
|
+
var subCommits = Array.isArray(data.submoduleCommits) ? data.submoduleCommits : [];
|
|
2136
|
+
var subPrefix = subCommits.length > 0
|
|
2137
|
+
? "已先提交 " + subCommits.length + " 个 submodule(" + subCommits.map(function(c) { return c.path; }).join("、") + "),"
|
|
2138
|
+
: "";
|
|
2139
|
+
var base = subPrefix + "已提交" + (hash ? " " + hash : "") + (tagName ? ",已打 tag " + tagName : "");
|
|
2123
2140
|
if (willPush && data.pushError) {
|
|
2124
2141
|
var msg = base + ";push 失败:" + data.pushError;
|
|
2125
2142
|
if (typeof showToast === "function") showToast(msg, "error");
|
|
@@ -2208,7 +2225,7 @@
|
|
|
2208
2225
|
} else {
|
|
2209
2226
|
if (typeof showToast === "function") showToast(base + (pushed ? ",已 push" : ""), "success");
|
|
2210
2227
|
}
|
|
2211
|
-
state.
|
|
2228
|
+
if (state.quickCommitOpenMenu === "tag-head") state.quickCommitOpenMenu = null;
|
|
2212
2229
|
state.quickCommitTagHeadForm = { tag: "", push: false };
|
|
2213
2230
|
if (state.selectedId) loadGitStatus(state.selectedId, { force: true }).then(function() {
|
|
2214
2231
|
if (state.quickCommitOpen) rerenderQuickCommitModal();
|
|
@@ -2223,7 +2240,6 @@
|
|
|
2223
2240
|
});
|
|
2224
2241
|
}
|
|
2225
2242
|
|
|
2226
|
-
// Push without committing.
|
|
2227
2243
|
function submitPushOnly(opts) {
|
|
2228
2244
|
if (!state.selectedId || state.quickCommitPushing) return;
|
|
2229
2245
|
var pushCommits = !!(opts && opts.pushCommits);
|
|
@@ -2267,7 +2283,6 @@
|
|
|
2267
2283
|
});
|
|
2268
2284
|
}
|
|
2269
2285
|
|
|
2270
|
-
// Build the file list portion of the modal.
|
|
2271
2286
|
function renderQuickCommitFileRows(files) {
|
|
2272
2287
|
var rows = files.map(function(item) {
|
|
2273
2288
|
var status = (item.status || " ").substring(0, 2);
|
|
@@ -2293,15 +2308,19 @@
|
|
|
2293
2308
|
return rows || '<div class="qc-empty">没有可提交的改动。</div>';
|
|
2294
2309
|
}
|
|
2295
2310
|
|
|
2296
|
-
|
|
2311
|
+
function isQuickCommitOpInFlight() {
|
|
2312
|
+
return state.quickCommitSubmitting || state.quickCommitTagHeadSubmitting || state.quickCommitPushing;
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2297
2315
|
function renderQuickCommitPrimary(hasChanges) {
|
|
2298
2316
|
var f = state.quickCommitForm;
|
|
2299
2317
|
var label;
|
|
2300
2318
|
if (state.quickCommitSubmitting) label = "提交中…";
|
|
2301
2319
|
else if (f.primaryAction === "commit-push") label = "提交并 push";
|
|
2302
2320
|
else label = "仅提交";
|
|
2303
|
-
var disabled = !hasChanges ||
|
|
2304
|
-
var
|
|
2321
|
+
var disabled = !hasChanges || isQuickCommitOpInFlight();
|
|
2322
|
+
var menuOpen = state.quickCommitOpenMenu === "action";
|
|
2323
|
+
var caretActive = menuOpen ? " is-active" : "";
|
|
2305
2324
|
var menuItems = [
|
|
2306
2325
|
{ value: "commit", label: "仅提交", desc: "只 commit,不 push" },
|
|
2307
2326
|
{ value: "commit-push", label: "提交并 push", desc: "commit 后立即 push 到远端" }
|
|
@@ -2317,36 +2336,32 @@
|
|
|
2317
2336
|
'<button id="quick-commit-submit-btn" class="btn btn-primary qc-split-main" type="button"' + (disabled ? ' disabled' : '') + '>' +
|
|
2318
2337
|
escapeHtml(label) +
|
|
2319
2338
|
'</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="' + (
|
|
2339
|
+
'<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
2340
|
'<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
2341
|
'</button>' +
|
|
2323
|
-
(
|
|
2342
|
+
(menuOpen ?
|
|
2324
2343
|
'<div id="quick-commit-action-menu" class="qc-dropdown-menu" data-qc-dropdown-menu="action" role="menu">' + menuHtml + '</div>' : '') +
|
|
2325
2344
|
'</div>';
|
|
2326
2345
|
}
|
|
2327
2346
|
|
|
2328
|
-
// Status chips (ahead/behind/unpushed tags) under the HEAD card.
|
|
2329
2347
|
function renderQuickCommitStatusChips(s) {
|
|
2330
2348
|
var chips = [];
|
|
2349
|
+
var hasUpstream = !!s.upstream;
|
|
2331
2350
|
if (typeof s.ahead === "number" && s.ahead > 0) {
|
|
2332
2351
|
chips.push('<span class="qc-chip qc-chip--ahead" title="本地领先 ' + s.ahead + ' 个 commit">↑ ' + s.ahead + ' 待推送</span>');
|
|
2333
2352
|
}
|
|
2334
2353
|
if (typeof s.behind === "number" && s.behind > 0) {
|
|
2335
2354
|
chips.push('<span class="qc-chip qc-chip--behind" title="远端领先 ' + s.behind + ' 个 commit">↓ ' + s.behind + ' 待拉取</span>');
|
|
2336
2355
|
}
|
|
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) {
|
|
2356
|
+
if (!hasUpstream) {
|
|
2341
2357
|
chips.push('<span class="qc-chip qc-chip--warn" title="当前分支没有 upstream,将首次推送时自动设置">无 upstream</span>');
|
|
2342
|
-
}
|
|
2343
|
-
if (!chips.length && s.hasUpstream !== false) {
|
|
2358
|
+
} else if (!chips.length) {
|
|
2344
2359
|
chips.push('<span class="qc-chip qc-chip--clean">与远端同步</span>');
|
|
2345
2360
|
}
|
|
2346
2361
|
return '<div class="qc-status-chips">' + chips.join("") + '</div>';
|
|
2347
2362
|
}
|
|
2348
2363
|
|
|
2349
|
-
// Inline "
|
|
2364
|
+
// Inline drawer used by the "为 HEAD 打 tag" toggle.
|
|
2350
2365
|
function renderQuickCommitTagHeadPanel() {
|
|
2351
2366
|
var thf = state.quickCommitTagHeadForm || { tag: "", push: false };
|
|
2352
2367
|
var submitting = state.quickCommitTagHeadSubmitting;
|
|
@@ -2368,22 +2383,23 @@
|
|
|
2368
2383
|
'</div>';
|
|
2369
2384
|
}
|
|
2370
2385
|
|
|
2371
|
-
// Push split-button on the secondary row.
|
|
2372
2386
|
function renderQuickCommitPushButton(s) {
|
|
2373
2387
|
var ahead = typeof s.ahead === "number" ? s.ahead : 0;
|
|
2374
|
-
var
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
var disabled =
|
|
2388
|
+
var hasUpstream = !!s.upstream;
|
|
2389
|
+
// Allow pushing commits if we're ahead, if upstream is missing (first push will set it up),
|
|
2390
|
+
// or if ahead is simply unknown — backend will surface real errors.
|
|
2391
|
+
var canPushCommits = ahead > 0 || !hasUpstream || typeof s.ahead !== "number";
|
|
2392
|
+
// Tag count isn't computed locally (would require a network probe). Let the user try.
|
|
2393
|
+
var canPushTags = true;
|
|
2394
|
+
var disabled = isQuickCommitOpInFlight() || (!canPushCommits && !canPushTags);
|
|
2381
2395
|
var mainLabel = state.quickCommitPushing ? "推送中…" : "推送";
|
|
2382
|
-
var
|
|
2383
|
-
var
|
|
2384
|
-
items
|
|
2385
|
-
|
|
2386
|
-
|
|
2396
|
+
var menuOpen = state.quickCommitOpenMenu === "push";
|
|
2397
|
+
var caretActive = menuOpen ? " is-active" : "";
|
|
2398
|
+
var items = [
|
|
2399
|
+
{ value: "commits", label: "推送 commits", desc: ahead ? "推送 " + ahead + " 个 commit" : "推送当前分支", disabled: !canPushCommits },
|
|
2400
|
+
{ value: "tags", label: "推送 tags", desc: "推送所有本地 tag", disabled: !canPushTags },
|
|
2401
|
+
{ value: "both", label: "推送 commits 和 tags", desc: "二合一", disabled: !(canPushCommits || canPushTags) }
|
|
2402
|
+
];
|
|
2387
2403
|
var menuHtml = items.map(function(it) {
|
|
2388
2404
|
var dis = it.disabled ? " is-disabled" : "";
|
|
2389
2405
|
return '<button type="button" class="qc-dropdown-item' + dis + '" data-qc-push="' + it.value + '"' + (it.disabled ? ' disabled' : '') + '>' +
|
|
@@ -2395,10 +2411,10 @@
|
|
|
2395
2411
|
'<button id="quick-commit-push-btn" class="btn btn-secondary qc-split-main" type="button"' + (disabled ? ' disabled' : '') + '>' +
|
|
2396
2412
|
escapeHtml(mainLabel) +
|
|
2397
2413
|
'</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="' + (
|
|
2414
|
+
'<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
2415
|
'<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
2416
|
'</button>' +
|
|
2401
|
-
(
|
|
2417
|
+
(menuOpen ?
|
|
2402
2418
|
'<div id="quick-commit-push-menu" class="qc-dropdown-menu qc-dropdown-menu--right" data-qc-dropdown-menu="push" role="menu">' + menuHtml + '</div>' : '') +
|
|
2403
2419
|
'</div>';
|
|
2404
2420
|
}
|
|
@@ -2467,11 +2483,11 @@
|
|
|
2467
2483
|
'<code class="qc-head-text">' + escapeHtml(headLine) + '</code>' +
|
|
2468
2484
|
'</div>' +
|
|
2469
2485
|
renderQuickCommitStatusChips(s) +
|
|
2470
|
-
(state.
|
|
2486
|
+
(state.quickCommitOpenMenu === "tag-head" ? renderQuickCommitTagHeadPanel() : '') +
|
|
2471
2487
|
'<div class="qc-section-actions qc-section-actions--secondary">' +
|
|
2472
2488
|
'<button id="quick-commit-tag-head-toggle" class="btn btn-ghost btn-sm" type="button"' + (state.quickCommitPushing ? ' disabled' : '') + '>' +
|
|
2473
2489
|
'<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.
|
|
2490
|
+
(state.quickCommitOpenMenu === "tag-head" ? '收起' : '为 HEAD 打 tag') +
|
|
2475
2491
|
'</button>' +
|
|
2476
2492
|
renderQuickCommitPushButton(s) +
|
|
2477
2493
|
'</div>' +
|
|
@@ -2640,6 +2656,40 @@
|
|
|
2640
2656
|
'</div>' +
|
|
2641
2657
|
'<p id="android-apk-message" class="hint hidden"></p>' +
|
|
2642
2658
|
'</div>' +
|
|
2659
|
+
'<div class="settings-update-section hidden" id="macos-dmg-section">' +
|
|
2660
|
+
'<div class="settings-section-head">' +
|
|
2661
|
+
'<span class="settings-section-icon">🖥️</span>' +
|
|
2662
|
+
'<div class="settings-section-head-text">' +
|
|
2663
|
+
'<h4 class="settings-section-heading">macOS App</h4>' +
|
|
2664
|
+
'<p class="settings-section-sub">原生客户端版本与 DMG 下载</p>' +
|
|
2665
|
+
'</div>' +
|
|
2666
|
+
'</div>' +
|
|
2667
|
+
'<div id="macos-dmg-current-row" class="settings-about-row hidden">' +
|
|
2668
|
+
'<span class="settings-label">当前版本</span>' +
|
|
2669
|
+
'<span class="settings-value" id="settings-macos-dmg-current">-</span>' +
|
|
2670
|
+
'</div>' +
|
|
2671
|
+
'<div id="macos-dmg-github-row" class="settings-about-row settings-about-row-action hidden">' +
|
|
2672
|
+
'<span class="settings-label">线上版本</span>' +
|
|
2673
|
+
'<span class="settings-value settings-value-flex" id="settings-macos-dmg-github">-</span>' +
|
|
2674
|
+
'<button id="download-github-dmg-btn" class="btn btn-secondary btn-sm hidden" type="button">下载</button>' +
|
|
2675
|
+
'</div>' +
|
|
2676
|
+
'<div id="macos-dmg-local-row" class="settings-about-row settings-about-row-action hidden">' +
|
|
2677
|
+
'<span class="settings-label">本地版本</span>' +
|
|
2678
|
+
'<span class="settings-value settings-value-flex" id="settings-macos-dmg-local">-</span>' +
|
|
2679
|
+
'<button id="download-local-dmg-btn" class="btn btn-secondary btn-sm hidden" type="button">下载</button>' +
|
|
2680
|
+
'</div>' +
|
|
2681
|
+
'<div id="macos-auto-update-row" class="settings-toggle-row hidden">' +
|
|
2682
|
+
'<div class="settings-toggle-text">' +
|
|
2683
|
+
'<span class="settings-toggle-title">自动更新</span>' +
|
|
2684
|
+
'<span class="settings-toggle-desc" id="macos-auto-update-hint">检测到新版 DMG 将自动下载并挂载。</span>' +
|
|
2685
|
+
'</div>' +
|
|
2686
|
+
'<label class="settings-switch">' +
|
|
2687
|
+
'<input type="checkbox" id="auto-update-dmg-toggle" class="switch-toggle">' +
|
|
2688
|
+
'<span class="switch-slider"></span>' +
|
|
2689
|
+
'</label>' +
|
|
2690
|
+
'</div>' +
|
|
2691
|
+
'<p id="macos-dmg-message" class="hint hidden"></p>' +
|
|
2692
|
+
'</div>' +
|
|
2643
2693
|
'<div class="settings-update-section" id="android-connect-section">' +
|
|
2644
2694
|
'<div class="settings-section-head">' +
|
|
2645
2695
|
'<span class="settings-section-icon">🔗</span>' +
|
|
@@ -5580,6 +5630,10 @@
|
|
|
5580
5630
|
if (autoUpdateApkToggle) autoUpdateApkToggle.addEventListener("change", function() {
|
|
5581
5631
|
toggleAutoUpdate("apk", autoUpdateApkToggle.checked);
|
|
5582
5632
|
});
|
|
5633
|
+
var autoUpdateDmgToggle = document.getElementById("auto-update-dmg-toggle");
|
|
5634
|
+
if (autoUpdateDmgToggle) autoUpdateDmgToggle.addEventListener("change", function() {
|
|
5635
|
+
toggleAutoUpdate("dmg", autoUpdateDmgToggle.checked);
|
|
5636
|
+
});
|
|
5583
5637
|
var copyConnectCodeBtn = document.getElementById("copy-connect-code-button");
|
|
5584
5638
|
if (copyConnectCodeBtn) copyConnectCodeBtn.addEventListener("click", function() {
|
|
5585
5639
|
var text = document.getElementById("android-connect-code");
|
|
@@ -7279,7 +7333,13 @@
|
|
|
7279
7333
|
var delta = normalizedOutput.slice(currentOutput.length);
|
|
7280
7334
|
if (delta) {
|
|
7281
7335
|
wandTerminalWrite(state.terminal, delta);
|
|
7282
|
-
|
|
7336
|
+
// 不在流式 chunk 路径触发 softResyncTerminal —— resync 会
|
|
7337
|
+
// resetTerminal() + 完整重放整段 buffer,重放期间所有 cursor-home
|
|
7338
|
+
// + 重画序列把"被覆盖的中间帧"反复塞进 main-screen scrollback,
|
|
7339
|
+
// 表现为 PTY 视图里同一段回答被画 N 遍(PC 端列宽大、Claude TUI
|
|
7340
|
+
// 重画序列密集时最严重)。响应结束的 thinking→idle 边界已经做了
|
|
7341
|
+
// 一次 softResync 兜底,足以清掉流式残留的错位光标 DOM;流式
|
|
7342
|
+
// 过程中持续重放只是纯粹的重复制造器。
|
|
7283
7343
|
wrote = true;
|
|
7284
7344
|
}
|
|
7285
7345
|
} else if (currentOutput && currentOutput.startsWith(normalizedOutput)) {
|
|
@@ -9222,6 +9282,8 @@
|
|
|
9222
9282
|
if (autoUpdateWebToggle) autoUpdateWebToggle.checked = !!autoUpdate.web;
|
|
9223
9283
|
var autoUpdateApkToggle = document.getElementById("auto-update-apk-toggle");
|
|
9224
9284
|
if (autoUpdateApkToggle) autoUpdateApkToggle.checked = !!autoUpdate.apk;
|
|
9285
|
+
var autoUpdateDmgToggle = document.getElementById("auto-update-dmg-toggle");
|
|
9286
|
+
if (autoUpdateDmgToggle) autoUpdateDmgToggle.checked = !!autoUpdate.dmg;
|
|
9225
9287
|
|
|
9226
9288
|
// ── Android APK version display ──
|
|
9227
9289
|
var apkSection = document.getElementById("android-apk-section");
|
|
@@ -9341,6 +9403,119 @@
|
|
|
9341
9403
|
}
|
|
9342
9404
|
}
|
|
9343
9405
|
|
|
9406
|
+
// ── macOS DMG version display ──
|
|
9407
|
+
var dmgSection = document.getElementById("macos-dmg-section");
|
|
9408
|
+
var dmgCurrentRow = document.getElementById("macos-dmg-current-row");
|
|
9409
|
+
var dmgCurrentEl = document.getElementById("settings-macos-dmg-current");
|
|
9410
|
+
var dmgGithubRow = document.getElementById("macos-dmg-github-row");
|
|
9411
|
+
var dmgGithubEl = document.getElementById("settings-macos-dmg-github");
|
|
9412
|
+
var dmgGithubBtn = document.getElementById("download-github-dmg-btn");
|
|
9413
|
+
var dmgLocalRow = document.getElementById("macos-dmg-local-row");
|
|
9414
|
+
var dmgLocalEl = document.getElementById("settings-macos-dmg-local");
|
|
9415
|
+
var dmgLocalBtn = document.getElementById("download-local-dmg-btn");
|
|
9416
|
+
var dmgMessageEl = document.getElementById("macos-dmg-message");
|
|
9417
|
+
var macosDmg = data.macosDmg || {};
|
|
9418
|
+
var isInMacApp = !!_macAppVersion;
|
|
9419
|
+
var hasDmgInfo = isInMacApp || !!macosDmg.github || !!macosDmg.local;
|
|
9420
|
+
if (dmgSection) {
|
|
9421
|
+
if (hasDmgInfo) dmgSection.classList.remove("hidden");
|
|
9422
|
+
else dmgSection.classList.add("hidden");
|
|
9423
|
+
}
|
|
9424
|
+
|
|
9425
|
+
if (isInMacApp) {
|
|
9426
|
+
// ── macOS 壳内:显示当前版本 + 线上 + 本地 + 下载安装按钮 ──
|
|
9427
|
+
if (dmgCurrentRow && dmgCurrentEl) {
|
|
9428
|
+
dmgCurrentEl.textContent = "v" + _macAppVersion;
|
|
9429
|
+
dmgCurrentRow.classList.remove("hidden");
|
|
9430
|
+
}
|
|
9431
|
+
if (macosDmg.github && dmgGithubRow && dmgGithubEl) {
|
|
9432
|
+
var dghLabel = macosDmg.github.version ? ("v" + macosDmg.github.version) : macosDmg.github.fileName;
|
|
9433
|
+
if (typeof macosDmg.github.size === "number") dghLabel += " · " + formatBytes(macosDmg.github.size);
|
|
9434
|
+
dmgGithubEl.textContent = dghLabel;
|
|
9435
|
+
dmgGithubRow.classList.remove("hidden");
|
|
9436
|
+
if (dmgGithubBtn) {
|
|
9437
|
+
dmgGithubBtn.textContent = "下载安装";
|
|
9438
|
+
dmgGithubBtn.classList.remove("hidden");
|
|
9439
|
+
dmgGithubBtn.onclick = function() {
|
|
9440
|
+
try {
|
|
9441
|
+
WandNative.downloadUpdate(macosDmg.github.downloadUrl, macosDmg.github.fileName || "wand-update.dmg", "github");
|
|
9442
|
+
} catch (e) {
|
|
9443
|
+
if (typeof window.wandAlert === "function") {
|
|
9444
|
+
window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
|
|
9445
|
+
} else if (typeof showToast === "function") {
|
|
9446
|
+
showToast("调用下载失败: " + e.message, "error");
|
|
9447
|
+
} else {
|
|
9448
|
+
alert("调用下载失败: " + e.message);
|
|
9449
|
+
}
|
|
9450
|
+
}
|
|
9451
|
+
};
|
|
9452
|
+
}
|
|
9453
|
+
}
|
|
9454
|
+
if (macosDmg.local && dmgLocalRow && dmgLocalEl) {
|
|
9455
|
+
var dlcLabel = macosDmg.local.version ? ("v" + macosDmg.local.version) : macosDmg.local.fileName;
|
|
9456
|
+
if (typeof macosDmg.local.size === "number") dlcLabel += " · " + formatBytes(macosDmg.local.size);
|
|
9457
|
+
dmgLocalEl.textContent = dlcLabel;
|
|
9458
|
+
dmgLocalRow.classList.remove("hidden");
|
|
9459
|
+
if (dmgLocalBtn) {
|
|
9460
|
+
dmgLocalBtn.textContent = "下载安装";
|
|
9461
|
+
dmgLocalBtn.classList.remove("hidden");
|
|
9462
|
+
dmgLocalBtn.onclick = function() {
|
|
9463
|
+
try {
|
|
9464
|
+
WandNative.downloadUpdate(macosDmg.local.downloadUrl, macosDmg.local.fileName || "wand-update.dmg", "local");
|
|
9465
|
+
} catch (e) {
|
|
9466
|
+
if (typeof window.wandAlert === "function") {
|
|
9467
|
+
window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
|
|
9468
|
+
} else if (typeof showToast === "function") {
|
|
9469
|
+
showToast("调用下载失败: " + e.message, "error");
|
|
9470
|
+
} else {
|
|
9471
|
+
alert("调用下载失败: " + e.message);
|
|
9472
|
+
}
|
|
9473
|
+
}
|
|
9474
|
+
};
|
|
9475
|
+
}
|
|
9476
|
+
}
|
|
9477
|
+
if (!macosDmg.github && !macosDmg.local && dmgMessageEl) {
|
|
9478
|
+
dmgMessageEl.textContent = "暂无可用更新";
|
|
9479
|
+
dmgMessageEl.classList.remove("hidden");
|
|
9480
|
+
}
|
|
9481
|
+
var dmgAutoRow = document.getElementById("macos-auto-update-row");
|
|
9482
|
+
var dmgAutoHint = document.getElementById("macos-auto-update-hint");
|
|
9483
|
+
if (dmgAutoRow) dmgAutoRow.classList.remove("hidden");
|
|
9484
|
+
if (dmgAutoHint) dmgAutoHint.classList.remove("hidden");
|
|
9485
|
+
} else {
|
|
9486
|
+
// ── 浏览器模式:仅展示下载入口 ──
|
|
9487
|
+
if (macosDmg.github && dmgGithubRow && dmgGithubEl) {
|
|
9488
|
+
var dghLabel2 = macosDmg.github.version ? ("v" + macosDmg.github.version) : macosDmg.github.fileName;
|
|
9489
|
+
if (typeof macosDmg.github.size === "number") dghLabel2 += " · " + formatBytes(macosDmg.github.size);
|
|
9490
|
+
dmgGithubEl.textContent = dghLabel2;
|
|
9491
|
+
dmgGithubRow.classList.remove("hidden");
|
|
9492
|
+
if (dmgGithubBtn) {
|
|
9493
|
+
dmgGithubBtn.textContent = "下载";
|
|
9494
|
+
dmgGithubBtn.classList.remove("hidden");
|
|
9495
|
+
dmgGithubBtn.onclick = function() {
|
|
9496
|
+
window.open(macosDmg.github.downloadUrl, "_blank");
|
|
9497
|
+
};
|
|
9498
|
+
}
|
|
9499
|
+
}
|
|
9500
|
+
if (macosDmg.local && dmgLocalRow && dmgLocalEl) {
|
|
9501
|
+
var dlcLabel2 = macosDmg.local.version ? ("v" + macosDmg.local.version) : macosDmg.local.fileName;
|
|
9502
|
+
if (typeof macosDmg.local.size === "number") dlcLabel2 += " · " + formatBytes(macosDmg.local.size);
|
|
9503
|
+
dmgLocalEl.textContent = dlcLabel2;
|
|
9504
|
+
dmgLocalRow.classList.remove("hidden");
|
|
9505
|
+
if (dmgLocalBtn) {
|
|
9506
|
+
dmgLocalBtn.textContent = "下载";
|
|
9507
|
+
dmgLocalBtn.classList.remove("hidden");
|
|
9508
|
+
dmgLocalBtn.onclick = function() {
|
|
9509
|
+
window.open(macosDmg.local.downloadUrl, "_self");
|
|
9510
|
+
};
|
|
9511
|
+
}
|
|
9512
|
+
}
|
|
9513
|
+
if (!macosDmg.github && !macosDmg.local && dmgMessageEl) {
|
|
9514
|
+
dmgMessageEl.textContent = "暂未提供";
|
|
9515
|
+
dmgMessageEl.classList.remove("hidden");
|
|
9516
|
+
}
|
|
9517
|
+
}
|
|
9518
|
+
|
|
9344
9519
|
// App connect code (encrypted)
|
|
9345
9520
|
var connectCodeEl = document.getElementById("android-connect-code");
|
|
9346
9521
|
var connectQrCanvas = document.getElementById("android-connect-qr");
|
|
@@ -9710,6 +9885,24 @@
|
|
|
9710
9885
|
.catch(function() {});
|
|
9711
9886
|
}
|
|
9712
9887
|
|
|
9888
|
+
function checkDmgAutoUpdate() {
|
|
9889
|
+
if (!_macAppVersion) return;
|
|
9890
|
+
fetch("/api/auto-update", { credentials: "same-origin" })
|
|
9891
|
+
.then(function(res) { return res.json(); })
|
|
9892
|
+
.then(function(autoData) {
|
|
9893
|
+
if (!autoData.dmg) return;
|
|
9894
|
+
return fetch("/api/macos-dmg-update?currentVersion=" + encodeURIComponent(_macAppVersion), { credentials: "same-origin" })
|
|
9895
|
+
.then(function(res) { return res.json(); })
|
|
9896
|
+
.then(function(data) {
|
|
9897
|
+
if (!data.updateAvailable || !data.downloadUrl) return;
|
|
9898
|
+
try {
|
|
9899
|
+
WandNative.downloadUpdate(data.downloadUrl, data.fileName || "wand-update.dmg", data.source || "local");
|
|
9900
|
+
} catch (_e) {}
|
|
9901
|
+
});
|
|
9902
|
+
})
|
|
9903
|
+
.catch(function() {});
|
|
9904
|
+
}
|
|
9905
|
+
|
|
9713
9906
|
function toggleAutoUpdate(type, enabled) {
|
|
9714
9907
|
var body = {};
|
|
9715
9908
|
body[type] = enabled;
|
|
@@ -9724,8 +9917,10 @@
|
|
|
9724
9917
|
// Sync toggle state with server response
|
|
9725
9918
|
var webToggle = document.getElementById("auto-update-web-toggle");
|
|
9726
9919
|
var apkToggle = document.getElementById("auto-update-apk-toggle");
|
|
9920
|
+
var dmgToggle = document.getElementById("auto-update-dmg-toggle");
|
|
9727
9921
|
if (webToggle) webToggle.checked = !!data.web;
|
|
9728
9922
|
if (apkToggle) apkToggle.checked = !!data.apk;
|
|
9923
|
+
if (dmgToggle) dmgToggle.checked = !!data.dmg;
|
|
9729
9924
|
})
|
|
9730
9925
|
.catch(function() {
|
|
9731
9926
|
// Revert toggle on failure
|
|
@@ -13870,6 +14065,35 @@
|
|
|
13870
14065
|
|
|
13871
14066
|
// Drop any in-flight socket and start a new one *now* — used by the
|
|
13872
14067
|
// Android resume bridge to recover from zombie connections (socket
|
|
14068
|
+
// 客户端 WS 心跳检测:每 10s 跑一次,看 lastWsMessageAt 距今多久。
|
|
14069
|
+
// 服务端每 20s 主动下推 {type:"ping"},所以 40s 没消息就明确是半开。
|
|
14070
|
+
// 浏览器在 background 时 setInterval 会被节流到 ~1Hz 或更慢,但
|
|
14071
|
+
// 我们也在 visibilitychange→visible 里做了一次主动评估,所以
|
|
14072
|
+
// 切回前台时不会拖很久才发现 stale。
|
|
14073
|
+
var WS_HEARTBEAT_CHECK_MS = 10_000;
|
|
14074
|
+
var WS_HEARTBEAT_STALE_MS = 40_000;
|
|
14075
|
+
function startWsHeartbeatCheck() {
|
|
14076
|
+
stopWsHeartbeatCheck();
|
|
14077
|
+
state.wsHeartbeatCheckTimer = setInterval(evaluateWsHeartbeatStale, WS_HEARTBEAT_CHECK_MS);
|
|
14078
|
+
}
|
|
14079
|
+
function stopWsHeartbeatCheck() {
|
|
14080
|
+
if (state.wsHeartbeatCheckTimer) {
|
|
14081
|
+
clearInterval(state.wsHeartbeatCheckTimer);
|
|
14082
|
+
state.wsHeartbeatCheckTimer = null;
|
|
14083
|
+
}
|
|
14084
|
+
}
|
|
14085
|
+
function evaluateWsHeartbeatStale() {
|
|
14086
|
+
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
|
|
14087
|
+
// 第一帧(包括 onopen)会刷新 lastWsMessageAt;如果还是 0 说明刚连上
|
|
14088
|
+
// 但服务端没下发任何东西(连 init 都没发出来)——交给下一轮检查。
|
|
14089
|
+
if (!state.lastWsMessageAt) return;
|
|
14090
|
+
var idle = Date.now() - state.lastWsMessageAt;
|
|
14091
|
+
if (idle > WS_HEARTBEAT_STALE_MS) {
|
|
14092
|
+
forceReconnectWebSocket("heartbeat-stale-" + Math.round(idle / 1000) + "s");
|
|
14093
|
+
}
|
|
14094
|
+
}
|
|
14095
|
+
|
|
14096
|
+
// Force a fresh WebSocket connection even if the existing one
|
|
13873
14097
|
// still says OPEN, but the TCP path was torn down by Doze). Skips
|
|
13874
14098
|
// the backoff timer; the caller has already decided this is urgent.
|
|
13875
14099
|
function forceReconnectWebSocket(reason) {
|
|
@@ -13880,6 +14104,13 @@
|
|
|
13880
14104
|
// reconnect path while we're already starting a fresh one.
|
|
13881
14105
|
try { stale.onclose = null; } catch (e) { /* ignore */ }
|
|
13882
14106
|
try { stale.onerror = null; } catch (e) { /* ignore */ }
|
|
14107
|
+
// 也清掉 onmessage:close() 是异步的,TCP RST/Close 帧到达之前,浏览器
|
|
14108
|
+
// socket 缓冲区里可能还有几条 in-flight 帧没派发。一旦它们在新 ws 已
|
|
14109
|
+
// open + init 之后再触发,老 ws 的 output 会被 handleWebSocketMessage
|
|
14110
|
+
// 当成"新增量"写进 wterm,造成"刚才那段又被画了一遍"。stale 心跳触发
|
|
14111
|
+
// 的 force reconnect 比 onclose 触发更早,老 ws 仍处于 OPEN,这个窗口
|
|
14112
|
+
// 更宽,必须显式 detach。
|
|
14113
|
+
try { stale.onmessage = null; } catch (e) { /* ignore */ }
|
|
13883
14114
|
try { stale.close(); } catch (e) { /* ignore */ }
|
|
13884
14115
|
state.ws = null;
|
|
13885
14116
|
}
|
|
@@ -13926,6 +14157,7 @@
|
|
|
13926
14157
|
ws.onopen = function() {
|
|
13927
14158
|
state.ws = ws;
|
|
13928
14159
|
state.wsConnected = true;
|
|
14160
|
+
state.lastWsMessageAt = Date.now();
|
|
13929
14161
|
// Reset backoff on a successful connect so the next disconnect
|
|
13930
14162
|
// starts the ladder from 500ms again.
|
|
13931
14163
|
state.wsReconnectAttempts = 0;
|
|
@@ -13933,6 +14165,9 @@
|
|
|
13933
14165
|
// Server's per-client output sequence counter restarts on every
|
|
13934
14166
|
// new socket; clear ours so the first init isn't treated as a gap.
|
|
13935
14167
|
state.lastSeqBySession = {};
|
|
14168
|
+
// 启动客户端心跳检测:每 10s 检查一次 lastWsMessageAt,超过 40s
|
|
14169
|
+
// 没收到任何消息(包括服务端 20s 一次的 ping)就视为半开连接。
|
|
14170
|
+
startWsHeartbeatCheck();
|
|
13936
14171
|
// Subscribe to current session if any
|
|
13937
14172
|
subscribeToSession(state.selectedId);
|
|
13938
14173
|
// Flush pending messages after reconnection
|
|
@@ -13947,8 +14182,20 @@
|
|
|
13947
14182
|
};
|
|
13948
14183
|
|
|
13949
14184
|
ws.onmessage = function(event) {
|
|
14185
|
+
// 任意服务端消息都说明连接活着,先刷新心跳计时。
|
|
14186
|
+
state.lastWsMessageAt = Date.now();
|
|
13950
14187
|
try {
|
|
13951
14188
|
var msg = JSON.parse(event.data);
|
|
14189
|
+
// 应用层 ping:立刻回 pong。同时也让服务端能算 RTT。
|
|
14190
|
+
// 这条消息处理完就 return,不进入下面的 sessionId 分发流程。
|
|
14191
|
+
if (msg && msg.type === "ping") {
|
|
14192
|
+
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
|
14193
|
+
try {
|
|
14194
|
+
state.ws.send(JSON.stringify({ type: "pong", t: msg.t }));
|
|
14195
|
+
} catch (sendErr) { /* ignore */ }
|
|
14196
|
+
}
|
|
14197
|
+
return;
|
|
14198
|
+
}
|
|
13952
14199
|
if (msg && msg.type === "resync_required" && msg.sessionId) {
|
|
13953
14200
|
// Server dropped some output events under backpressure and
|
|
13954
14201
|
// is asking us for a fresh snapshot. Send a resync so the
|
|
@@ -13995,6 +14242,7 @@
|
|
|
13995
14242
|
ws.onclose = function() {
|
|
13996
14243
|
state.ws = null;
|
|
13997
14244
|
state.wsConnected = false;
|
|
14245
|
+
stopWsHeartbeatCheck();
|
|
13998
14246
|
scheduleWsReconnect();
|
|
13999
14247
|
};
|
|
14000
14248
|
|
|
@@ -14128,7 +14376,9 @@
|
|
|
14128
14376
|
// 变化的视觉错位无法被自愈,直到用户手动改窗口才修。现在让
|
|
14129
14377
|
// wterm 内部 ResizeObserver 独占 cols 跟踪职责。
|
|
14130
14378
|
wandTerminalWrite(state.terminal, msg.data.chunk);
|
|
14131
|
-
|
|
14379
|
+
// 同 syncTerminalBuffer 的 delta 分支:流式 chunk 不再触发
|
|
14380
|
+
// softResyncTerminal,避免完整重放把 cursor-home + 重画的
|
|
14381
|
+
// "被覆盖中间帧"反复塞进 scrollback。thinking→idle 兜底就够了。
|
|
14132
14382
|
state.terminalSessionId = msg.sessionId;
|
|
14133
14383
|
if (msg.data.output) {
|
|
14134
14384
|
state.terminalOutput = clampClientTerminalOutput(normalizeTerminalOutput(msg.data.output));
|
|
@@ -14264,10 +14514,24 @@
|
|
|
14264
14514
|
renderChat(true);
|
|
14265
14515
|
updateTaskDisplay();
|
|
14266
14516
|
updateApprovalStats();
|
|
14267
|
-
//
|
|
14268
|
-
//
|
|
14269
|
-
//
|
|
14270
|
-
|
|
14517
|
+
// 服务端 ring buffer 在多数场景下是当前已渲染输出的严格 superset
|
|
14518
|
+
// (同一 PTY,buffer 只增不减)。这种情况下走 append delta 就够了,
|
|
14519
|
+
// 不应该强制 replace。replace 会触发 resetTerminal + 全量重放整段
|
|
14520
|
+
// output,wterm 把 ANSI cursor-home 重画的"中间帧"全部塞进
|
|
14521
|
+
// scrollback,造成"同一段回答在 PTY 视图里被画 N 遍"——尤其是
|
|
14522
|
+
// 锁屏 / 切回前台 / 心跳 stale 触发 reconnect 时,每次 init 都重放
|
|
14523
|
+
// 一次,累积重复非常显著。
|
|
14524
|
+
//
|
|
14525
|
+
// 只有真的不连续(会话切换、ring buffer 截断了头部、output 不是
|
|
14526
|
+
// currentOutput 的严格前缀延伸)才回退到 replace 的全量基线。
|
|
14527
|
+
var initOutput = msg.data.output || "";
|
|
14528
|
+
var sameTerminalSession = state.terminalSessionId === msg.sessionId;
|
|
14529
|
+
var currTerminalOutput = state.terminalOutput || "";
|
|
14530
|
+
var canAppendDelta = sameTerminalSession
|
|
14531
|
+
&& currTerminalOutput.length > 0
|
|
14532
|
+
&& initOutput.length >= currTerminalOutput.length
|
|
14533
|
+
&& initOutput.startsWith(currTerminalOutput);
|
|
14534
|
+
updateTerminalOutput(initOutput, msg.sessionId, canAppendDelta ? "append" : "replace");
|
|
14271
14535
|
// wterm 启动 cols=120,replace 写入可能落在错的列宽上;ResizeObserver
|
|
14272
14536
|
// 回调异步,用 fit-with-retry 兜一次确保按真实宽度重排。
|
|
14273
14537
|
ensureTerminalFitWithRetry("init");
|
|
@@ -17762,11 +18026,43 @@
|
|
|
17762
18026
|
|
|
17763
18027
|
// ── Browser Notification API ──
|
|
17764
18028
|
|
|
18029
|
+
// macOS WKWebView shim: Android shell injects a global WandNative via
|
|
18030
|
+
// addJavascriptInterface, but WKWebView only exposes
|
|
18031
|
+
// webkit.messageHandlers.<name>.postMessage(...). To keep call-sites
|
|
18032
|
+
// identical across platforms, we synthesize a WandNative-shaped object
|
|
18033
|
+
// when running inside the macOS shell.
|
|
18034
|
+
try {
|
|
18035
|
+
var _macUaMatch = navigator.userAgent.match(/WandPlatform\/macOS/);
|
|
18036
|
+
var _macHandler = (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.wandNative) || null;
|
|
18037
|
+
if (_macUaMatch && _macHandler && typeof window.WandNative === "undefined") {
|
|
18038
|
+
window.WandNative = {
|
|
18039
|
+
// Only downloadUpdate is wired for now; other Android-specific
|
|
18040
|
+
// methods (notifications, haptics, screen wake) intentionally
|
|
18041
|
+
// omitted so feature detection falls back to web APIs on macOS.
|
|
18042
|
+
downloadUpdate: function(url, fileName, source) {
|
|
18043
|
+
try {
|
|
18044
|
+
_macHandler.postMessage({
|
|
18045
|
+
type: "downloadUpdate",
|
|
18046
|
+
url: String(url || ""),
|
|
18047
|
+
fileName: String(fileName || "wand-update.dmg"),
|
|
18048
|
+
source: String(source || "local"),
|
|
18049
|
+
});
|
|
18050
|
+
} catch (_e) {}
|
|
18051
|
+
},
|
|
18052
|
+
};
|
|
18053
|
+
}
|
|
18054
|
+
} catch (_e) {}
|
|
18055
|
+
|
|
17765
18056
|
// Detect Android APK native bridge
|
|
17766
18057
|
var _hasNativeBridge = typeof WandNative !== "undefined" && typeof WandNative.sendNotification === "function";
|
|
17767
|
-
//
|
|
17768
|
-
|
|
17769
|
-
|
|
18058
|
+
// Extract WandApp/<version> from User-Agent (set by both Android and macOS shells).
|
|
18059
|
+
// We distinguish platforms by the additional WandPlatform/<name> token —
|
|
18060
|
+
// macOS UA ends with "WandApp/X WandPlatform/macOS".
|
|
18061
|
+
var _wandAppMatch = navigator.userAgent.match(/WandApp\/([^\s]+)/);
|
|
18062
|
+
var _isMacApp = /WandPlatform\/macOS/.test(navigator.userAgent);
|
|
18063
|
+
var _isAndroidApp = !!_wandAppMatch && !_isMacApp;
|
|
18064
|
+
var _apkVersion = (_wandAppMatch && _isAndroidApp) ? _wandAppMatch[1] : null;
|
|
18065
|
+
var _macAppVersion = (_wandAppMatch && _isMacApp) ? _wandAppMatch[1] : null;
|
|
17770
18066
|
|
|
17771
18067
|
function _vibrate(pattern) {
|
|
17772
18068
|
if (!_hasNativeBridge || typeof WandNative.vibrate !== "function") return;
|