@co0ontty/wand 1.25.3 → 1.26.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/README.md +12 -0
- package/dist/git-quick-commit.d.ts +17 -1
- package/dist/git-quick-commit.js +248 -32
- package/dist/npm-update-utils.d.ts +51 -0
- package/dist/npm-update-utils.js +171 -0
- package/dist/server-session-routes.js +58 -1
- package/dist/server.js +14 -17
- package/dist/structured-session-manager.js +58 -2
- package/dist/tui/commands.d.ts +4 -0
- package/dist/tui/commands.js +16 -7
- package/dist/types.d.ts +32 -0
- package/dist/web-ui/content/scripts.js +634 -77
- package/dist/web-ui/content/styles.css +285 -0
- package/dist/ws-broadcast.js +36 -13
- package/package.json +1 -1
|
@@ -195,7 +195,21 @@
|
|
|
195
195
|
quickCommitSubmitting: false,
|
|
196
196
|
quickCommitGenerating: false,
|
|
197
197
|
quickCommitError: "",
|
|
198
|
-
|
|
198
|
+
// primaryAction: "commit" (commit only, no push) or "commit-push" (commit then push).
|
|
199
|
+
// Whether the commit is also tagged is controlled by `makeTag` independently —
|
|
200
|
+
// that keeps "commit + tag, push later" possible.
|
|
201
|
+
quickCommitForm: { customMessage: "", makeTag: false, tag: "", primaryAction: "commit" },
|
|
202
|
+
quickCommitActionMenuOpen: false,
|
|
203
|
+
// Secondary "tag the existing HEAD" panel (inline drawer).
|
|
204
|
+
quickCommitTagHeadOpen: false,
|
|
205
|
+
quickCommitTagHeadForm: { tag: "", push: false },
|
|
206
|
+
quickCommitTagHeadSubmitting: false,
|
|
207
|
+
quickCommitTagHeadGenerating: false,
|
|
208
|
+
quickCommitTagHeadError: "",
|
|
209
|
+
// Secondary "push only" controls.
|
|
210
|
+
quickCommitPushMenuOpen: false,
|
|
211
|
+
quickCommitPushing: false,
|
|
212
|
+
quickCommitPushError: "",
|
|
199
213
|
// Telegram 风格的"贴底"状态:true = 用户当前贴在底部,新消息会自然出现;
|
|
200
214
|
// false = 用户向上滚了,未读会累积到气泡里,不会自动滚他们的视图。
|
|
201
215
|
chatStickToBottom: true,
|
|
@@ -1777,13 +1791,40 @@
|
|
|
1777
1791
|
}
|
|
1778
1792
|
|
|
1779
1793
|
var quickCommitEscHandler = null;
|
|
1794
|
+
var quickCommitDocClickHandler = null;
|
|
1795
|
+
|
|
1796
|
+
// Restore the user's last "primary action" choice so the split button feels sticky.
|
|
1797
|
+
function readSavedPrimaryAction() {
|
|
1798
|
+
try {
|
|
1799
|
+
var v = localStorage.getItem("wand.quickCommit.primaryAction");
|
|
1800
|
+
if (v === "commit" || v === "commit-push") return v;
|
|
1801
|
+
} catch (e) { /* localStorage may be blocked */ }
|
|
1802
|
+
return "commit";
|
|
1803
|
+
}
|
|
1804
|
+
function savePrimaryAction(value) {
|
|
1805
|
+
try { localStorage.setItem("wand.quickCommit.primaryAction", value); } catch (e) { /* no-op */ }
|
|
1806
|
+
}
|
|
1780
1807
|
|
|
1781
1808
|
function openQuickCommitModal() {
|
|
1782
1809
|
if (!state.selectedId) return;
|
|
1783
1810
|
state.quickCommitOpen = true;
|
|
1784
1811
|
state.quickCommitSubmitting = false;
|
|
1785
1812
|
state.quickCommitError = "";
|
|
1786
|
-
state.quickCommitForm = {
|
|
1813
|
+
state.quickCommitForm = {
|
|
1814
|
+
customMessage: "",
|
|
1815
|
+
makeTag: false,
|
|
1816
|
+
tag: "",
|
|
1817
|
+
primaryAction: readSavedPrimaryAction(),
|
|
1818
|
+
};
|
|
1819
|
+
state.quickCommitActionMenuOpen = false;
|
|
1820
|
+
state.quickCommitTagHeadOpen = false;
|
|
1821
|
+
state.quickCommitTagHeadForm = { tag: "", push: false };
|
|
1822
|
+
state.quickCommitTagHeadSubmitting = false;
|
|
1823
|
+
state.quickCommitTagHeadGenerating = false;
|
|
1824
|
+
state.quickCommitTagHeadError = "";
|
|
1825
|
+
state.quickCommitPushMenuOpen = false;
|
|
1826
|
+
state.quickCommitPushing = false;
|
|
1827
|
+
state.quickCommitPushError = "";
|
|
1787
1828
|
closeWorktreeMergeModal();
|
|
1788
1829
|
closeSessionModal();
|
|
1789
1830
|
closeSettingsModal();
|
|
@@ -1796,11 +1837,37 @@
|
|
|
1796
1837
|
}
|
|
1797
1838
|
if (quickCommitEscHandler) document.removeEventListener("keydown", quickCommitEscHandler);
|
|
1798
1839
|
quickCommitEscHandler = function(e) {
|
|
1799
|
-
if (e.key === "Escape" && state.quickCommitOpen && !state.quickCommitSubmitting) {
|
|
1840
|
+
if (e.key === "Escape" && state.quickCommitOpen && !state.quickCommitSubmitting && !state.quickCommitTagHeadSubmitting && !state.quickCommitPushing) {
|
|
1841
|
+
// First Esc closes any open dropdown; second closes the modal.
|
|
1842
|
+
if (state.quickCommitActionMenuOpen || state.quickCommitPushMenuOpen || state.quickCommitTagHeadOpen) {
|
|
1843
|
+
state.quickCommitActionMenuOpen = false;
|
|
1844
|
+
state.quickCommitPushMenuOpen = false;
|
|
1845
|
+
state.quickCommitTagHeadOpen = false;
|
|
1846
|
+
rerenderQuickCommitModal();
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1800
1849
|
closeQuickCommitModal();
|
|
1801
1850
|
}
|
|
1802
1851
|
};
|
|
1803
1852
|
document.addEventListener("keydown", quickCommitEscHandler);
|
|
1853
|
+
if (quickCommitDocClickHandler) document.removeEventListener("click", quickCommitDocClickHandler, true);
|
|
1854
|
+
quickCommitDocClickHandler = function(e) {
|
|
1855
|
+
if (!state.quickCommitOpen) return;
|
|
1856
|
+
var modalEl = document.getElementById("quick-commit-modal");
|
|
1857
|
+
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
|
+
var t = e.target;
|
|
1862
|
+
while (t && t !== modalEl) {
|
|
1863
|
+
if (t.dataset && (t.dataset.qcDropdownToggle || t.dataset.qcDropdownMenu)) return;
|
|
1864
|
+
t = t.parentNode;
|
|
1865
|
+
}
|
|
1866
|
+
state.quickCommitActionMenuOpen = false;
|
|
1867
|
+
state.quickCommitPushMenuOpen = false;
|
|
1868
|
+
rerenderQuickCommitModal();
|
|
1869
|
+
};
|
|
1870
|
+
document.addEventListener("click", quickCommitDocClickHandler, true);
|
|
1804
1871
|
loadGitStatus(state.selectedId, { force: true }).then(function() {
|
|
1805
1872
|
if (!state.quickCommitOpen) return;
|
|
1806
1873
|
rerenderQuickCommitModal();
|
|
@@ -1811,6 +1878,9 @@
|
|
|
1811
1878
|
state.quickCommitOpen = false;
|
|
1812
1879
|
state.quickCommitSubmitting = false;
|
|
1813
1880
|
state.quickCommitError = "";
|
|
1881
|
+
state.quickCommitActionMenuOpen = false;
|
|
1882
|
+
state.quickCommitPushMenuOpen = false;
|
|
1883
|
+
state.quickCommitTagHeadOpen = false;
|
|
1814
1884
|
var modal = document.getElementById("quick-commit-modal");
|
|
1815
1885
|
if (modal) modal.classList.add("hidden");
|
|
1816
1886
|
if (focusTrapHandler) {
|
|
@@ -1821,6 +1891,10 @@
|
|
|
1821
1891
|
document.removeEventListener("keydown", quickCommitEscHandler);
|
|
1822
1892
|
quickCommitEscHandler = null;
|
|
1823
1893
|
}
|
|
1894
|
+
if (quickCommitDocClickHandler) {
|
|
1895
|
+
document.removeEventListener("click", quickCommitDocClickHandler, true);
|
|
1896
|
+
quickCommitDocClickHandler = null;
|
|
1897
|
+
}
|
|
1824
1898
|
if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
|
|
1825
1899
|
lastFocusedElement.focus();
|
|
1826
1900
|
}
|
|
@@ -1843,6 +1917,8 @@
|
|
|
1843
1917
|
if (closeBtn) closeBtn.addEventListener("click", closeQuickCommitModal);
|
|
1844
1918
|
var cancelBtn = document.getElementById("quick-commit-cancel-btn");
|
|
1845
1919
|
if (cancelBtn) cancelBtn.addEventListener("click", closeQuickCommitModal);
|
|
1920
|
+
|
|
1921
|
+
// ── Section 1: commit form ──
|
|
1846
1922
|
var submitBtn = document.getElementById("quick-commit-submit-btn");
|
|
1847
1923
|
if (submitBtn) submitBtn.addEventListener("click", submitQuickCommit);
|
|
1848
1924
|
var aiBtn = document.getElementById("quick-commit-ai-btn");
|
|
@@ -1861,10 +1937,102 @@
|
|
|
1861
1937
|
if (tagInput) tagInput.addEventListener("input", function() {
|
|
1862
1938
|
state.quickCommitForm.tag = tagInput.value;
|
|
1863
1939
|
});
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1940
|
+
|
|
1941
|
+
// ── Primary action split-button (caret + dropdown) ──
|
|
1942
|
+
var actionCaret = document.getElementById("quick-commit-action-caret");
|
|
1943
|
+
if (actionCaret) actionCaret.addEventListener("click", function(e) {
|
|
1944
|
+
e.stopPropagation();
|
|
1945
|
+
state.quickCommitActionMenuOpen = !state.quickCommitActionMenuOpen;
|
|
1946
|
+
state.quickCommitPushMenuOpen = false;
|
|
1947
|
+
rerenderQuickCommitModal();
|
|
1948
|
+
});
|
|
1949
|
+
var actionMenu = document.getElementById("quick-commit-action-menu");
|
|
1950
|
+
if (actionMenu) {
|
|
1951
|
+
var actionItems = actionMenu.querySelectorAll("[data-qc-action]");
|
|
1952
|
+
for (var i = 0; i < actionItems.length; i++) {
|
|
1953
|
+
(function(btn) {
|
|
1954
|
+
btn.addEventListener("click", function() {
|
|
1955
|
+
var value = btn.getAttribute("data-qc-action");
|
|
1956
|
+
if (value === "commit" || value === "commit-push") {
|
|
1957
|
+
state.quickCommitForm.primaryAction = value;
|
|
1958
|
+
savePrimaryAction(value);
|
|
1959
|
+
}
|
|
1960
|
+
state.quickCommitActionMenuOpen = false;
|
|
1961
|
+
rerenderQuickCommitModal();
|
|
1962
|
+
});
|
|
1963
|
+
})(actionItems[i]);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// ── Section 2: Tag HEAD inline panel ──
|
|
1968
|
+
var tagHeadToggle = document.getElementById("quick-commit-tag-head-toggle");
|
|
1969
|
+
if (tagHeadToggle) tagHeadToggle.addEventListener("click", function() {
|
|
1970
|
+
state.quickCommitTagHeadOpen = !state.quickCommitTagHeadOpen;
|
|
1971
|
+
if (state.quickCommitTagHeadOpen) {
|
|
1972
|
+
state.quickCommitTagHeadError = "";
|
|
1973
|
+
}
|
|
1974
|
+
rerenderQuickCommitModal();
|
|
1975
|
+
if (state.quickCommitTagHeadOpen) {
|
|
1976
|
+
var inp = document.getElementById("quick-commit-tag-head-input");
|
|
1977
|
+
if (inp) inp.focus();
|
|
1978
|
+
}
|
|
1867
1979
|
});
|
|
1980
|
+
var tagHeadCancel = document.getElementById("quick-commit-tag-head-cancel");
|
|
1981
|
+
if (tagHeadCancel) tagHeadCancel.addEventListener("click", function() {
|
|
1982
|
+
state.quickCommitTagHeadOpen = false;
|
|
1983
|
+
state.quickCommitTagHeadError = "";
|
|
1984
|
+
rerenderQuickCommitModal();
|
|
1985
|
+
});
|
|
1986
|
+
var tagHeadInput = document.getElementById("quick-commit-tag-head-input");
|
|
1987
|
+
if (tagHeadInput) {
|
|
1988
|
+
tagHeadInput.addEventListener("input", function() {
|
|
1989
|
+
state.quickCommitTagHeadForm.tag = tagHeadInput.value;
|
|
1990
|
+
});
|
|
1991
|
+
tagHeadInput.addEventListener("keydown", function(e) {
|
|
1992
|
+
if (e.key === "Enter") {
|
|
1993
|
+
e.preventDefault();
|
|
1994
|
+
submitTagHead(false);
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
var tagHeadAi = document.getElementById("quick-commit-tag-head-ai");
|
|
1999
|
+
if (tagHeadAi) tagHeadAi.addEventListener("click", generateTagHeadAI);
|
|
2000
|
+
var tagHeadPushCb = document.getElementById("quick-commit-tag-head-push");
|
|
2001
|
+
if (tagHeadPushCb) tagHeadPushCb.addEventListener("change", function() {
|
|
2002
|
+
state.quickCommitTagHeadForm.push = tagHeadPushCb.checked;
|
|
2003
|
+
});
|
|
2004
|
+
var tagHeadSubmit = document.getElementById("quick-commit-tag-head-submit");
|
|
2005
|
+
if (tagHeadSubmit) tagHeadSubmit.addEventListener("click", function() {
|
|
2006
|
+
submitTagHead(false);
|
|
2007
|
+
});
|
|
2008
|
+
|
|
2009
|
+
// ── Section 2: Push split-button ──
|
|
2010
|
+
var pushBtn = document.getElementById("quick-commit-push-btn");
|
|
2011
|
+
if (pushBtn) pushBtn.addEventListener("click", function() {
|
|
2012
|
+
submitPushOnly({ pushCommits: true, pushTags: false });
|
|
2013
|
+
});
|
|
2014
|
+
var pushCaret = document.getElementById("quick-commit-push-caret");
|
|
2015
|
+
if (pushCaret) pushCaret.addEventListener("click", function(e) {
|
|
2016
|
+
e.stopPropagation();
|
|
2017
|
+
state.quickCommitPushMenuOpen = !state.quickCommitPushMenuOpen;
|
|
2018
|
+
state.quickCommitActionMenuOpen = false;
|
|
2019
|
+
rerenderQuickCommitModal();
|
|
2020
|
+
});
|
|
2021
|
+
var pushMenu = document.getElementById("quick-commit-push-menu");
|
|
2022
|
+
if (pushMenu) {
|
|
2023
|
+
var pushItems = pushMenu.querySelectorAll("[data-qc-push]");
|
|
2024
|
+
for (var j = 0; j < pushItems.length; j++) {
|
|
2025
|
+
(function(btn) {
|
|
2026
|
+
btn.addEventListener("click", function() {
|
|
2027
|
+
var value = btn.getAttribute("data-qc-push");
|
|
2028
|
+
state.quickCommitPushMenuOpen = false;
|
|
2029
|
+
if (value === "commits") submitPushOnly({ pushCommits: true, pushTags: false });
|
|
2030
|
+
else if (value === "tags") submitPushOnly({ pushCommits: false, pushTags: true });
|
|
2031
|
+
else if (value === "both") submitPushOnly({ pushCommits: true, pushTags: true });
|
|
2032
|
+
});
|
|
2033
|
+
})(pushItems[j]);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
1868
2036
|
}
|
|
1869
2037
|
|
|
1870
2038
|
function generateCommitMessageAI() {
|
|
@@ -1925,13 +2093,14 @@
|
|
|
1925
2093
|
rerenderQuickCommitModal();
|
|
1926
2094
|
return;
|
|
1927
2095
|
}
|
|
2096
|
+
var willPush = form.primaryAction === "commit-push";
|
|
1928
2097
|
// 开了 tag 开关但没填 → 由后端在提交时调 AI 生成
|
|
1929
2098
|
var payload = {
|
|
1930
2099
|
autoMessage: false,
|
|
1931
2100
|
customMessage: message,
|
|
1932
2101
|
tag: userTag,
|
|
1933
2102
|
autoTag: !!(form.makeTag && !userTag),
|
|
1934
|
-
push:
|
|
2103
|
+
push: willPush
|
|
1935
2104
|
};
|
|
1936
2105
|
state.quickCommitSubmitting = true;
|
|
1937
2106
|
state.quickCommitError = "";
|
|
@@ -1951,8 +2120,7 @@
|
|
|
1951
2120
|
var hash = data.commit && data.commit.hash ? data.commit.hash.substring(0, 7) : "";
|
|
1952
2121
|
var tagName = data.tag && data.tag.name ? data.tag.name : "";
|
|
1953
2122
|
var base = "已提交" + (hash ? " " + hash : "") + (tagName ? ",已打 tag " + tagName : "");
|
|
1954
|
-
|
|
1955
|
-
if (pushRequested && data.pushError) {
|
|
2123
|
+
if (willPush && data.pushError) {
|
|
1956
2124
|
var msg = base + ";push 失败:" + data.pushError;
|
|
1957
2125
|
if (typeof showToast === "function") showToast(msg, "error");
|
|
1958
2126
|
} else {
|
|
@@ -1971,13 +2139,137 @@
|
|
|
1971
2139
|
});
|
|
1972
2140
|
}
|
|
1973
2141
|
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
var
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2142
|
+
// ── AI generate a tag for the existing HEAD (no commit involved) ──
|
|
2143
|
+
function generateTagHeadAI() {
|
|
2144
|
+
if (!state.selectedId || state.quickCommitTagHeadGenerating) return;
|
|
2145
|
+
var inp = document.getElementById("quick-commit-tag-head-input");
|
|
2146
|
+
if (inp) state.quickCommitTagHeadForm.tag = inp.value;
|
|
2147
|
+
state.quickCommitTagHeadGenerating = true;
|
|
2148
|
+
state.quickCommitTagHeadError = "";
|
|
2149
|
+
rerenderQuickCommitModal();
|
|
2150
|
+
// Reuse the existing generator — it stages and asks for {message, tag}.
|
|
2151
|
+
// We only consume `suggestedTag` here.
|
|
2152
|
+
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/generate-commit-message", {
|
|
2153
|
+
method: "POST",
|
|
2154
|
+
credentials: "same-origin",
|
|
2155
|
+
headers: { "Content-Type": "application/json" },
|
|
2156
|
+
body: JSON.stringify({})
|
|
2157
|
+
})
|
|
2158
|
+
.then(function(res) { return res.json().then(function(data) { return { ok: res.ok, data: data }; }); })
|
|
2159
|
+
.then(function(result) {
|
|
2160
|
+
if (!result.ok) throw new Error((result.data && result.data.error) || "AI 生成失败。");
|
|
2161
|
+
var aiTag = (result.data && typeof result.data.suggestedTag === "string") ? result.data.suggestedTag.trim() : "";
|
|
2162
|
+
var currentTag = (state.quickCommitTagHeadForm.tag || "").trim();
|
|
2163
|
+
if (!currentTag && aiTag) {
|
|
2164
|
+
state.quickCommitTagHeadForm.tag = aiTag;
|
|
2165
|
+
} else if (!aiTag) {
|
|
2166
|
+
throw new Error("AI 没有给出 tag 建议。");
|
|
2167
|
+
}
|
|
2168
|
+
})
|
|
2169
|
+
.catch(function(error) {
|
|
2170
|
+
state.quickCommitTagHeadError = (error && error.message) || "AI 生成失败。";
|
|
2171
|
+
})
|
|
2172
|
+
.finally(function() {
|
|
2173
|
+
state.quickCommitTagHeadGenerating = false;
|
|
2174
|
+
if (state.quickCommitOpen) rerenderQuickCommitModal();
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// Tag the existing HEAD without making a new commit.
|
|
2179
|
+
function submitTagHead(silent) {
|
|
2180
|
+
if (!state.selectedId || state.quickCommitTagHeadSubmitting) return;
|
|
2181
|
+
var inp = document.getElementById("quick-commit-tag-head-input");
|
|
2182
|
+
if (inp) state.quickCommitTagHeadForm.tag = inp.value;
|
|
2183
|
+
var tag = (state.quickCommitTagHeadForm.tag || "").trim();
|
|
2184
|
+
if (!tag) {
|
|
2185
|
+
state.quickCommitTagHeadError = "请填写 tag 名称,或点击 AI 建议。";
|
|
2186
|
+
rerenderQuickCommitModal();
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
state.quickCommitTagHeadSubmitting = true;
|
|
2190
|
+
state.quickCommitTagHeadError = "";
|
|
2191
|
+
rerenderQuickCommitModal();
|
|
2192
|
+
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/git/tag-head", {
|
|
2193
|
+
method: "POST",
|
|
2194
|
+
credentials: "same-origin",
|
|
2195
|
+
headers: { "Content-Type": "application/json" },
|
|
2196
|
+
body: JSON.stringify({ tag: tag, push: !!state.quickCommitTagHeadForm.push })
|
|
2197
|
+
})
|
|
2198
|
+
.then(function(res) { return res.json().then(function(data) { return { ok: res.ok, data: data }; }); })
|
|
2199
|
+
.then(function(result) {
|
|
2200
|
+
if (!result.ok) throw new Error((result.data && result.data.error) || "打 tag 失败。");
|
|
2201
|
+
var data = result.data || {};
|
|
2202
|
+
var name = data.tag && data.tag.name ? data.tag.name : tag;
|
|
2203
|
+
var pushed = !!data.pushed;
|
|
2204
|
+
var pushErr = data.pushError;
|
|
2205
|
+
var base = "已为 HEAD 打 tag " + name;
|
|
2206
|
+
if (state.quickCommitTagHeadForm.push && pushErr) {
|
|
2207
|
+
if (typeof showToast === "function") showToast(base + ";push tag 失败:" + pushErr, "error");
|
|
2208
|
+
} else {
|
|
2209
|
+
if (typeof showToast === "function") showToast(base + (pushed ? ",已 push" : ""), "success");
|
|
2210
|
+
}
|
|
2211
|
+
state.quickCommitTagHeadOpen = false;
|
|
2212
|
+
state.quickCommitTagHeadForm = { tag: "", push: false };
|
|
2213
|
+
if (state.selectedId) loadGitStatus(state.selectedId, { force: true }).then(function() {
|
|
2214
|
+
if (state.quickCommitOpen) rerenderQuickCommitModal();
|
|
2215
|
+
});
|
|
2216
|
+
})
|
|
2217
|
+
.catch(function(error) {
|
|
2218
|
+
state.quickCommitTagHeadError = (error && error.message) || "打 tag 失败。";
|
|
2219
|
+
})
|
|
2220
|
+
.finally(function() {
|
|
2221
|
+
state.quickCommitTagHeadSubmitting = false;
|
|
2222
|
+
if (state.quickCommitOpen) rerenderQuickCommitModal();
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
// Push without committing.
|
|
2227
|
+
function submitPushOnly(opts) {
|
|
2228
|
+
if (!state.selectedId || state.quickCommitPushing) return;
|
|
2229
|
+
var pushCommits = !!(opts && opts.pushCommits);
|
|
2230
|
+
var pushTags = !!(opts && opts.pushTags);
|
|
2231
|
+
if (!pushCommits && !pushTags) return;
|
|
2232
|
+
state.quickCommitPushing = true;
|
|
2233
|
+
state.quickCommitPushError = "";
|
|
2234
|
+
rerenderQuickCommitModal();
|
|
2235
|
+
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/git/push", {
|
|
2236
|
+
method: "POST",
|
|
2237
|
+
credentials: "same-origin",
|
|
2238
|
+
headers: { "Content-Type": "application/json" },
|
|
2239
|
+
body: JSON.stringify({ pushCommits: pushCommits, pushTags: pushTags })
|
|
2240
|
+
})
|
|
2241
|
+
.then(function(res) { return res.json().then(function(data) { return { ok: res.ok, data: data }; }); })
|
|
2242
|
+
.then(function(result) {
|
|
2243
|
+
var data = result.data || {};
|
|
2244
|
+
if (!result.ok) throw new Error((data && data.error) || "推送失败。");
|
|
2245
|
+
// Backend marks `ok: false` on partial failure but still returns 200 — surface error toast.
|
|
2246
|
+
if (data.error) {
|
|
2247
|
+
state.quickCommitPushError = data.error;
|
|
2248
|
+
if (typeof showToast === "function") showToast("推送失败:" + data.error, "error");
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
var parts = [];
|
|
2252
|
+
if (data.pushedCommits) parts.push("commits");
|
|
2253
|
+
if (data.pushedTags) parts.push("tags");
|
|
2254
|
+
var label = parts.length ? parts.join(" 和 ") : "(无内容)";
|
|
2255
|
+
if (typeof showToast === "function") showToast("已推送 " + label, "success");
|
|
2256
|
+
if (state.selectedId) loadGitStatus(state.selectedId, { force: true }).then(function() {
|
|
2257
|
+
if (state.quickCommitOpen) rerenderQuickCommitModal();
|
|
2258
|
+
});
|
|
2259
|
+
})
|
|
2260
|
+
.catch(function(error) {
|
|
2261
|
+
state.quickCommitPushError = (error && error.message) || "推送失败。";
|
|
2262
|
+
if (typeof showToast === "function") showToast(state.quickCommitPushError, "error");
|
|
2263
|
+
})
|
|
2264
|
+
.finally(function() {
|
|
2265
|
+
state.quickCommitPushing = false;
|
|
2266
|
+
if (state.quickCommitOpen) rerenderQuickCommitModal();
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// Build the file list portion of the modal.
|
|
2271
|
+
function renderQuickCommitFileRows(files) {
|
|
2272
|
+
var rows = files.map(function(item) {
|
|
1981
2273
|
var status = (item.status || " ").substring(0, 2);
|
|
1982
2274
|
var flag = status.trim() || "?";
|
|
1983
2275
|
var cls = "qc-flag";
|
|
@@ -1998,48 +2290,208 @@
|
|
|
1998
2290
|
}
|
|
1999
2291
|
return '<div class="qc-file-row"><span class="' + cls + '">' + escapeHtml(status) + '</span><span class="qc-file-path">' + escapeHtml(item.path || "") + '</span>' + subBadge + '</div>';
|
|
2000
2292
|
}).join("");
|
|
2001
|
-
|
|
2293
|
+
return rows || '<div class="qc-empty">没有可提交的改动。</div>';
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// Primary action button label / icon.
|
|
2297
|
+
function renderQuickCommitPrimary(hasChanges) {
|
|
2298
|
+
var f = state.quickCommitForm;
|
|
2299
|
+
var label;
|
|
2300
|
+
if (state.quickCommitSubmitting) label = "提交中…";
|
|
2301
|
+
else if (f.primaryAction === "commit-push") label = "提交并 push";
|
|
2302
|
+
else label = "仅提交";
|
|
2303
|
+
var disabled = !hasChanges || state.quickCommitSubmitting || state.quickCommitTagHeadSubmitting || state.quickCommitPushing;
|
|
2304
|
+
var caretActive = state.quickCommitActionMenuOpen ? " is-active" : "";
|
|
2305
|
+
var menuItems = [
|
|
2306
|
+
{ value: "commit", label: "仅提交", desc: "只 commit,不 push" },
|
|
2307
|
+
{ value: "commit-push", label: "提交并 push", desc: "commit 后立即 push 到远端" }
|
|
2308
|
+
];
|
|
2309
|
+
var menuHtml = menuItems.map(function(item) {
|
|
2310
|
+
var sel = f.primaryAction === item.value ? " is-selected" : "";
|
|
2311
|
+
return '<button type="button" class="qc-dropdown-item' + sel + '" data-qc-action="' + item.value + '">' +
|
|
2312
|
+
'<span class="qc-dropdown-item-title">' + escapeHtml(item.label) + '</span>' +
|
|
2313
|
+
'<span class="qc-dropdown-item-desc">' + escapeHtml(item.desc) + '</span>' +
|
|
2314
|
+
'</button>';
|
|
2315
|
+
}).join("");
|
|
2316
|
+
return '<div class="qc-split-button">' +
|
|
2317
|
+
'<button id="quick-commit-submit-btn" class="btn btn-primary qc-split-main" type="button"' + (disabled ? ' disabled' : '') + '>' +
|
|
2318
|
+
escapeHtml(label) +
|
|
2319
|
+
'</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="' + (state.quickCommitActionMenuOpen ? 'true' : 'false') + '" aria-label="更多提交方式">' +
|
|
2321
|
+
'<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
|
+
'</button>' +
|
|
2323
|
+
(state.quickCommitActionMenuOpen ?
|
|
2324
|
+
'<div id="quick-commit-action-menu" class="qc-dropdown-menu" data-qc-dropdown-menu="action" role="menu">' + menuHtml + '</div>' : '') +
|
|
2325
|
+
'</div>';
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// Status chips (ahead/behind/unpushed tags) under the HEAD card.
|
|
2329
|
+
function renderQuickCommitStatusChips(s) {
|
|
2330
|
+
var chips = [];
|
|
2331
|
+
if (typeof s.ahead === "number" && s.ahead > 0) {
|
|
2332
|
+
chips.push('<span class="qc-chip qc-chip--ahead" title="本地领先 ' + s.ahead + ' 个 commit">↑ ' + s.ahead + ' 待推送</span>');
|
|
2333
|
+
}
|
|
2334
|
+
if (typeof s.behind === "number" && s.behind > 0) {
|
|
2335
|
+
chips.push('<span class="qc-chip qc-chip--behind" title="远端领先 ' + s.behind + ' 个 commit">↓ ' + s.behind + ' 待拉取</span>');
|
|
2336
|
+
}
|
|
2337
|
+
if (typeof s.unpushedTagCount === "number" && s.unpushedTagCount > 0) {
|
|
2338
|
+
chips.push('<span class="qc-chip qc-chip--tag" title="' + s.unpushedTagCount + ' 个本地 tag 未推送">🏷 ' + s.unpushedTagCount + ' 待推送</span>');
|
|
2339
|
+
}
|
|
2340
|
+
if (s.hasUpstream === false) {
|
|
2341
|
+
chips.push('<span class="qc-chip qc-chip--warn" title="当前分支没有 upstream,将首次推送时自动设置">无 upstream</span>');
|
|
2342
|
+
}
|
|
2343
|
+
if (!chips.length && s.hasUpstream !== false) {
|
|
2344
|
+
chips.push('<span class="qc-chip qc-chip--clean">与远端同步</span>');
|
|
2345
|
+
}
|
|
2346
|
+
return '<div class="qc-status-chips">' + chips.join("") + '</div>';
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
// Inline "tag HEAD" drawer rendering.
|
|
2350
|
+
function renderQuickCommitTagHeadPanel() {
|
|
2351
|
+
var thf = state.quickCommitTagHeadForm || { tag: "", push: false };
|
|
2352
|
+
var submitting = state.quickCommitTagHeadSubmitting;
|
|
2353
|
+
var generating = state.quickCommitTagHeadGenerating;
|
|
2354
|
+
return '<div class="qc-tag-head-panel">' +
|
|
2355
|
+
'<div class="qc-tag-head-row">' +
|
|
2356
|
+
'<input type="text" id="quick-commit-tag-head-input" class="field-input" placeholder="输入 tag 名(例如 v1.2.0),或点 AI 建议" value="' + escapeHtml(thf.tag || "") + '"' + (submitting ? ' disabled' : '') + '>' +
|
|
2357
|
+
'<button type="button" id="quick-commit-tag-head-ai" class="btn btn-ghost btn-sm"' + (generating || submitting ? ' disabled' : '') + '>' + (generating ? '建议中…' : 'AI 建议') + '</button>' +
|
|
2358
|
+
'</div>' +
|
|
2359
|
+
'<label class="qc-tag-head-push">' +
|
|
2360
|
+
'<input type="checkbox" id="quick-commit-tag-head-push"' + (thf.push ? ' checked' : '') + (submitting ? ' disabled' : '') + '>' +
|
|
2361
|
+
'<span>打完 tag 立即推送这个 tag</span>' +
|
|
2362
|
+
'</label>' +
|
|
2363
|
+
(state.quickCommitTagHeadError ? '<p class="error-message">' + escapeHtml(state.quickCommitTagHeadError) + '</p>' : '') +
|
|
2364
|
+
'<div class="qc-tag-head-actions">' +
|
|
2365
|
+
'<button type="button" id="quick-commit-tag-head-cancel" class="btn btn-ghost btn-sm"' + (submitting ? ' disabled' : '') + '>收起</button>' +
|
|
2366
|
+
'<button type="button" id="quick-commit-tag-head-submit" class="btn btn-secondary btn-sm"' + (submitting ? ' disabled' : '') + '>' + (submitting ? '打 tag 中…' : '为 HEAD 打 tag') + '</button>' +
|
|
2367
|
+
'</div>' +
|
|
2368
|
+
'</div>';
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// Push split-button on the secondary row.
|
|
2372
|
+
function renderQuickCommitPushButton(s) {
|
|
2373
|
+
var ahead = typeof s.ahead === "number" ? s.ahead : 0;
|
|
2374
|
+
var unpushedTags = typeof s.unpushedTagCount === "number" ? s.unpushedTagCount : 0;
|
|
2375
|
+
var canPushCommits = ahead > 0 || s.hasUpstream === false; // first-push lets you set upstream
|
|
2376
|
+
var canPushTags = unpushedTags > 0;
|
|
2377
|
+
// Allow pushing commits even without ahead info (e.g. ls-remote unreachable) — backend will report.
|
|
2378
|
+
if (typeof s.ahead !== "number" && s.hasUpstream !== false) canPushCommits = true;
|
|
2379
|
+
if (typeof s.unpushedTagCount !== "number") canPushTags = true; // unknown — let user try
|
|
2380
|
+
var disabled = state.quickCommitPushing || state.quickCommitSubmitting || state.quickCommitTagHeadSubmitting || (!canPushCommits && !canPushTags);
|
|
2381
|
+
var mainLabel = state.quickCommitPushing ? "推送中…" : "推送";
|
|
2382
|
+
var caretActive = state.quickCommitPushMenuOpen ? " is-active" : "";
|
|
2383
|
+
var items = [];
|
|
2384
|
+
items.push({ value: "commits", label: "推送 commits", desc: ahead ? "推送 " + ahead + " 个 commit" : "推送当前分支", disabled: !canPushCommits });
|
|
2385
|
+
items.push({ value: "tags", label: "推送 tags", desc: unpushedTags ? "推送 " + unpushedTags + " 个本地 tag" : "推送所有本地 tag", disabled: !canPushTags });
|
|
2386
|
+
items.push({ value: "both", label: "推送 commits 和 tags", desc: "二合一", disabled: !(canPushCommits || canPushTags) });
|
|
2387
|
+
var menuHtml = items.map(function(it) {
|
|
2388
|
+
var dis = it.disabled ? " is-disabled" : "";
|
|
2389
|
+
return '<button type="button" class="qc-dropdown-item' + dis + '" data-qc-push="' + it.value + '"' + (it.disabled ? ' disabled' : '') + '>' +
|
|
2390
|
+
'<span class="qc-dropdown-item-title">' + escapeHtml(it.label) + '</span>' +
|
|
2391
|
+
'<span class="qc-dropdown-item-desc">' + escapeHtml(it.desc) + '</span>' +
|
|
2392
|
+
'</button>';
|
|
2393
|
+
}).join("");
|
|
2394
|
+
return '<div class="qc-split-button qc-split-button--secondary">' +
|
|
2395
|
+
'<button id="quick-commit-push-btn" class="btn btn-secondary qc-split-main" type="button"' + (disabled ? ' disabled' : '') + '>' +
|
|
2396
|
+
escapeHtml(mainLabel) +
|
|
2397
|
+
'</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="' + (state.quickCommitPushMenuOpen ? 'true' : 'false') + '" aria-label="更多推送方式">' +
|
|
2399
|
+
'<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
|
+
'</button>' +
|
|
2401
|
+
(state.quickCommitPushMenuOpen ?
|
|
2402
|
+
'<div id="quick-commit-push-menu" class="qc-dropdown-menu qc-dropdown-menu--right" data-qc-dropdown-menu="push" role="menu">' + menuHtml + '</div>' : '') +
|
|
2403
|
+
'</div>';
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
function renderQuickCommitModal() {
|
|
2407
|
+
var s = state.gitStatus || {};
|
|
2408
|
+
var f = state.quickCommitForm || { customMessage: "", makeTag: false, tag: "", primaryAction: "commit" };
|
|
2002
2409
|
var hasChanges = (s.modifiedCount || 0) > 0;
|
|
2410
|
+
var files = Array.isArray(s.files) ? s.files : [];
|
|
2411
|
+
var fileRows = renderQuickCommitFileRows(files);
|
|
2412
|
+
|
|
2413
|
+
// Subtitle: branch · N 改动 · ↑X ↓Y
|
|
2414
|
+
var subParts = [];
|
|
2415
|
+
subParts.push(s.branch || "(no branch)");
|
|
2416
|
+
subParts.push((s.modifiedCount || 0) + " 个改动");
|
|
2417
|
+
if (typeof s.ahead === "number" && s.ahead > 0) subParts.push("↑" + s.ahead);
|
|
2418
|
+
if (typeof s.behind === "number" && s.behind > 0) subParts.push("↓" + s.behind);
|
|
2419
|
+
|
|
2420
|
+
// Section 1: changes + commit form (only when there are changes)
|
|
2421
|
+
var section1 = "";
|
|
2422
|
+
if (hasChanges) {
|
|
2423
|
+
section1 = '<section class="qc-section qc-section--changes">' +
|
|
2424
|
+
'<div class="qc-section-head"><span class="qc-section-title">暂存改动</span><span class="qc-section-meta">' + escapeHtml(s.modifiedCount + " 个文件") + '</span></div>' +
|
|
2425
|
+
'<div class="qc-files-wrap">' + fileRows + '</div>' +
|
|
2426
|
+
'<div class="qc-message-row" id="quick-commit-message-row">' +
|
|
2427
|
+
'<div class="qc-message-header"><label class="field-label" for="quick-commit-message">commit message</label>' +
|
|
2428
|
+
'<button type="button" id="quick-commit-ai-btn" class="btn btn-ghost btn-sm"' + (state.quickCommitGenerating ? ' disabled' : '') + '>' + (state.quickCommitGenerating ? '生成中…' : 'AI 生成') + '</button>' +
|
|
2429
|
+
'</div>' +
|
|
2430
|
+
'<textarea id="quick-commit-message" class="field-input" rows="2" placeholder="输入 commit message 或点击 AI 生成"' + (state.quickCommitSubmitting ? ' disabled' : '') + '>' + escapeHtml(f.customMessage || "") + '</textarea>' +
|
|
2431
|
+
'</div>' +
|
|
2432
|
+
'<div class="qc-checkbox-row">' +
|
|
2433
|
+
'<label class="qc-checkbox-label" for="quick-commit-make-tag">同时为本次 commit 打 tag</label>' +
|
|
2434
|
+
'<label class="qc-switch">' +
|
|
2435
|
+
'<input type="checkbox" id="quick-commit-make-tag" class="switch-toggle"' + (f.makeTag ? ' checked' : '') + '>' +
|
|
2436
|
+
'<span class="switch-slider"></span>' +
|
|
2437
|
+
'</label>' +
|
|
2438
|
+
'</div>' +
|
|
2439
|
+
'<div class="qc-tag-row' + (f.makeTag ? '' : ' hidden') + '" id="quick-commit-tag-row">' +
|
|
2440
|
+
'<input type="text" id="quick-commit-tag" class="field-input" placeholder="输入 tag 名称;留空将由 AI 在提交时自动生成" value="' + escapeHtml(f.tag || "") + '"' + (state.quickCommitSubmitting ? ' disabled' : '') + '>' +
|
|
2441
|
+
'</div>' +
|
|
2442
|
+
(state.quickCommitError ? '<p class="error-message">' + escapeHtml(state.quickCommitError) + '</p>' : '') +
|
|
2443
|
+
'<div class="qc-section-actions">' +
|
|
2444
|
+
'<button id="quick-commit-cancel-btn" class="btn btn-ghost btn-sm" type="button">取消</button>' +
|
|
2445
|
+
renderQuickCommitPrimary(hasChanges) +
|
|
2446
|
+
'</div>' +
|
|
2447
|
+
'</section>';
|
|
2448
|
+
} else {
|
|
2449
|
+
section1 = '<section class="qc-section qc-section--empty">' +
|
|
2450
|
+
'<div class="qc-empty-state">' +
|
|
2451
|
+
'<span class="qc-empty-icon">✓</span>' +
|
|
2452
|
+
'<div><div class="qc-empty-title">工作区干净</div><div class="qc-empty-sub">没有暂存改动可提交。仍可在下方为 HEAD 打 tag 或推送。</div></div>' +
|
|
2453
|
+
'</div>' +
|
|
2454
|
+
'</section>';
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// Section 2: repo status + secondary actions (always show when there's at least one commit)
|
|
2458
|
+
var section2 = "";
|
|
2459
|
+
if (!s.initialCommit && s.isGit !== false) {
|
|
2460
|
+
var lc = s.lastCommit || {};
|
|
2461
|
+
var headLine = lc.shortHash ? lc.shortHash + " · " + (lc.subject || "") : (s.head ? s.head.substring(0, 7) : "(no commit)");
|
|
2462
|
+
var upstreamLine = s.upstream ? escapeHtml(s.branch || "") + " → " + escapeHtml(s.upstream) : escapeHtml(s.branch || "(no branch)") + " · 无 upstream";
|
|
2463
|
+
section2 = '<section class="qc-section qc-section--repo">' +
|
|
2464
|
+
'<div class="qc-section-head"><span class="qc-section-title">仓库状态</span><span class="qc-section-meta">' + upstreamLine + '</span></div>' +
|
|
2465
|
+
'<div class="qc-head-card">' +
|
|
2466
|
+
'<span class="qc-head-label">HEAD</span>' +
|
|
2467
|
+
'<code class="qc-head-text">' + escapeHtml(headLine) + '</code>' +
|
|
2468
|
+
'</div>' +
|
|
2469
|
+
renderQuickCommitStatusChips(s) +
|
|
2470
|
+
(state.quickCommitTagHeadOpen ? renderQuickCommitTagHeadPanel() : '') +
|
|
2471
|
+
'<div class="qc-section-actions qc-section-actions--secondary">' +
|
|
2472
|
+
'<button id="quick-commit-tag-head-toggle" class="btn btn-ghost btn-sm" type="button"' + (state.quickCommitPushing ? ' disabled' : '') + '>' +
|
|
2473
|
+
'<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.quickCommitTagHeadOpen ? '收起' : '为 HEAD 打 tag') +
|
|
2475
|
+
'</button>' +
|
|
2476
|
+
renderQuickCommitPushButton(s) +
|
|
2477
|
+
'</div>' +
|
|
2478
|
+
'</section>';
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
var subtitleHtml = subParts.map(escapeHtml).join(" · ");
|
|
2003
2482
|
|
|
2004
2483
|
return '<section id="quick-commit-modal" class="modal-backdrop' + (state.quickCommitOpen ? '' : ' hidden') + '">' +
|
|
2005
2484
|
'<div class="modal quick-commit-modal" role="dialog" aria-labelledby="quick-commit-title">' +
|
|
2006
2485
|
'<div class="modal-header">' +
|
|
2007
2486
|
'<div>' +
|
|
2008
2487
|
'<h2 id="quick-commit-title" class="modal-title">快捷提交</h2>' +
|
|
2009
|
-
'<p class="modal-subtitle">' +
|
|
2488
|
+
'<p class="modal-subtitle">' + subtitleHtml + '</p>' +
|
|
2010
2489
|
'</div>' +
|
|
2011
2490
|
'<button id="quick-commit-close-btn" class="btn btn-ghost btn-icon modal-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>' +
|
|
2012
2491
|
'</div>' +
|
|
2013
2492
|
'<div class="modal-body">' +
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
'<div class="qc-message-header"><label class="field-label" for="quick-commit-message">commit message</label>' +
|
|
2017
|
-
'<button type="button" id="quick-commit-ai-btn" class="btn btn-ghost btn-sm"' + (state.quickCommitGenerating ? ' disabled' : '') + '>' + (state.quickCommitGenerating ? '生成中…' : 'AI 生成') + '</button>' +
|
|
2018
|
-
'</div>' +
|
|
2019
|
-
'<textarea id="quick-commit-message" class="field-input" rows="2" placeholder="输入 commit message 或点击 AI 生成">' + escapeHtml(f.customMessage || "") + '</textarea>' +
|
|
2020
|
-
'</div>' +
|
|
2021
|
-
'<div class="qc-checkbox-row">' +
|
|
2022
|
-
'<label class="qc-checkbox-label" for="quick-commit-make-tag">提交后打 tag</label>' +
|
|
2023
|
-
'<label class="qc-switch">' +
|
|
2024
|
-
'<input type="checkbox" id="quick-commit-make-tag" class="switch-toggle"' + (f.makeTag ? ' checked' : '') + '>' +
|
|
2025
|
-
'<span class="switch-slider"></span>' +
|
|
2026
|
-
'</label>' +
|
|
2027
|
-
'</div>' +
|
|
2028
|
-
'<div class="qc-tag-row' + (f.makeTag ? '' : ' hidden') + '" id="quick-commit-tag-row">' +
|
|
2029
|
-
'<input type="text" id="quick-commit-tag" class="field-input" placeholder="输入 tag 名称;留空将由 AI 在提交时自动生成" value="' + escapeHtml(f.tag || "") + '">' +
|
|
2030
|
-
'</div>' +
|
|
2031
|
-
'<div class="qc-checkbox-row">' +
|
|
2032
|
-
'<label class="qc-checkbox-label" for="quick-commit-push">提交后 push 到远端</label>' +
|
|
2033
|
-
'<label class="qc-switch">' +
|
|
2034
|
-
'<input type="checkbox" id="quick-commit-push" class="switch-toggle"' + (f.push ? ' checked' : '') + '>' +
|
|
2035
|
-
'<span class="switch-slider"></span>' +
|
|
2036
|
-
'</label>' +
|
|
2037
|
-
'</div>' +
|
|
2038
|
-
'<p id="quick-commit-error" class="error-message' + (state.quickCommitError ? '' : ' hidden') + '">' + escapeHtml(state.quickCommitError || "") + '</p>' +
|
|
2039
|
-
'<div class="worktree-merge-actions">' +
|
|
2040
|
-
'<button id="quick-commit-cancel-btn" class="btn btn-secondary" type="button">取消</button>' +
|
|
2041
|
-
'<button id="quick-commit-submit-btn" class="btn btn-primary" type="button"' + (hasChanges && !state.quickCommitSubmitting ? '' : ' disabled') + '>' + (state.quickCommitSubmitting ? '提交中…' : '执行') + '</button>' +
|
|
2042
|
-
'</div>' +
|
|
2493
|
+
section1 +
|
|
2494
|
+
section2 +
|
|
2043
2495
|
'</div>' +
|
|
2044
2496
|
'</div>' +
|
|
2045
2497
|
'</section>';
|
|
@@ -9199,16 +9651,27 @@
|
|
|
9199
9651
|
})
|
|
9200
9652
|
.then(function(res) { return res.json(); })
|
|
9201
9653
|
.then(function(data) {
|
|
9654
|
+
if (data.error) {
|
|
9655
|
+
if (msgEl) {
|
|
9656
|
+
msgEl.textContent = data.error;
|
|
9657
|
+
msgEl.style.color = "var(--error)";
|
|
9658
|
+
msgEl.classList.remove("hidden");
|
|
9659
|
+
}
|
|
9660
|
+
updateBtn.disabled = false;
|
|
9661
|
+
return;
|
|
9662
|
+
}
|
|
9663
|
+
// \u5b89\u88c5\u6210\u529f\uff1a\u81ea\u52a8\u8c03\u7528 /api/restart\uff0c\u8ba9\u670d\u52a1\u91cd\u542f\u751f\u6548\uff0c
|
|
9664
|
+
// \u9875\u9762\u4f1a\u88ab restart overlay \u63a5\u624b\uff0c\u7b49\u540e\u7aef\u56de\u6765\u540e\u81ea\u52a8\u5237\u65b0\u3002
|
|
9202
9665
|
if (msgEl) {
|
|
9203
|
-
msgEl.textContent = data.message ||
|
|
9204
|
-
msgEl.style.color =
|
|
9666
|
+
msgEl.textContent = (data.message || "\u66f4\u65b0\u5b8c\u6210") + "\uff0c\u6b63\u5728\u91cd\u542f\u670d\u52a1\u2026";
|
|
9667
|
+
msgEl.style.color = "var(--success)";
|
|
9205
9668
|
msgEl.classList.remove("hidden");
|
|
9206
9669
|
}
|
|
9207
|
-
|
|
9208
|
-
|
|
9670
|
+
updateBtn.classList.add("hidden");
|
|
9671
|
+
if (data.restartRequired !== false) {
|
|
9672
|
+
performRestart(null, msgEl);
|
|
9209
9673
|
} else {
|
|
9210
|
-
|
|
9211
|
-
// Show restart button
|
|
9674
|
+
// \u670d\u52a1\u7aef\u660e\u786e\u8868\u793a\u4e0d\u9700\u8981\u91cd\u542f\uff0c\u4fdd\u7559\u624b\u52a8\u91cd\u542f\u6309\u94ae
|
|
9212
9675
|
var restartBtn = document.getElementById("do-restart-button");
|
|
9213
9676
|
if (restartBtn) restartBtn.classList.remove("hidden");
|
|
9214
9677
|
}
|
|
@@ -10910,6 +11373,48 @@
|
|
|
10910
11373
|
}
|
|
10911
11374
|
}
|
|
10912
11375
|
|
|
11376
|
+
// 计算一条 ConversationTurn 里所有 content block 的"信息体积"——文字 / 思考 /
|
|
11377
|
+
// tool_result 内容长度之和。用于在 lastMessage 增量更新里判断 incoming 是否
|
|
11378
|
+
// 至少和 localLast 一样完整,防止服务端偶发吐出更短的同 message.id 导致
|
|
11379
|
+
// 已显示的文字段被回退覆盖("显示了又消失"的根因之一)。
|
|
11380
|
+
function turnContentVolume(turn) {
|
|
11381
|
+
if (!turn || !Array.isArray(turn.content)) return 0;
|
|
11382
|
+
var total = 0;
|
|
11383
|
+
for (var i = 0; i < turn.content.length; i++) {
|
|
11384
|
+
var b = turn.content[i];
|
|
11385
|
+
if (!b) continue;
|
|
11386
|
+
if (typeof b.text === "string") total += b.text.length;
|
|
11387
|
+
if (typeof b.thinking === "string") total += b.thinking.length;
|
|
11388
|
+
if (typeof b.content === "string") total += b.content.length;
|
|
11389
|
+
else if (Array.isArray(b.content)) {
|
|
11390
|
+
for (var k = 0; k < b.content.length; k++) {
|
|
11391
|
+
var sub = b.content[k];
|
|
11392
|
+
if (sub && typeof sub.text === "string") total += sub.text.length;
|
|
11393
|
+
}
|
|
11394
|
+
}
|
|
11395
|
+
if (b.input) {
|
|
11396
|
+
try { total += JSON.stringify(b.input).length; } catch (_e) {}
|
|
11397
|
+
}
|
|
11398
|
+
}
|
|
11399
|
+
return total;
|
|
11400
|
+
}
|
|
11401
|
+
|
|
11402
|
+
// 合并同 role 的 last assistant turn:incoming 通常是权威更新(包含完整 usage、
|
|
11403
|
+
// 完整 block 序列),但偶发的服务端回退会让 incoming 比 local 更短。此时
|
|
11404
|
+
// 保留本地内容——下一次正常 emit 会校正。usage 字段以 incoming 优先(因为
|
|
11405
|
+
// 那是 result event 给的最终值)。
|
|
11406
|
+
function mergeAssistantTurn(localLast, incoming) {
|
|
11407
|
+
if (!localLast) return incoming;
|
|
11408
|
+
if (!incoming) return localLast;
|
|
11409
|
+
var localVol = turnContentVolume(localLast);
|
|
11410
|
+
var incVol = turnContentVolume(incoming);
|
|
11411
|
+
if (incVol >= localVol) return incoming;
|
|
11412
|
+
// incoming 更短:保留 local 的 content,但允许 incoming 更新 usage / 元字段。
|
|
11413
|
+
return Object.assign({}, localLast, {
|
|
11414
|
+
usage: incoming.usage || localLast.usage,
|
|
11415
|
+
});
|
|
11416
|
+
}
|
|
11417
|
+
|
|
10913
11418
|
// Append queued user message placeholders to currentMessages so they
|
|
10914
11419
|
// remain visible across WS updates and re-renders.
|
|
10915
11420
|
function buildMessagesForRender(session, messages) {
|
|
@@ -13554,23 +14059,30 @@
|
|
|
13554
14059
|
snapshot.sessionKind = msg.data.sessionKind;
|
|
13555
14060
|
}
|
|
13556
14061
|
|
|
13557
|
-
|
|
14062
|
+
// 优先级修正:若同一事件里同时带 messages(全量)和 lastMessage(增量),
|
|
14063
|
+
// 让全量赢。WS 端的 debounce 已经会在跨形状时 flush,但保留这层
|
|
14064
|
+
// 客户端兜底,避免任何上游再合并出双载体事件时再次丢消息。
|
|
14065
|
+
if (msg.data.messages) {
|
|
14066
|
+
// Full mode (authoritative)
|
|
14067
|
+
snapshot.messages = msg.data.messages;
|
|
14068
|
+
} else if (isIncremental && msg.data.lastMessage) {
|
|
13558
14069
|
// Incremental mode: merge lastMessage into existing session messages
|
|
13559
14070
|
var existingSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
13560
14071
|
if (existingSession) {
|
|
13561
14072
|
var msgs = Array.isArray(existingSession.messages) ? existingSession.messages.slice() : [];
|
|
13562
14073
|
var expectedCount = msg.data.messageCount || 0;
|
|
13563
|
-
//
|
|
13564
|
-
|
|
13565
|
-
|
|
14074
|
+
// 防御性合并:lastMessage 应当至少和本地最后一条一样长。如果服务端
|
|
14075
|
+
// 因为上游 bug(如 upsertBlocks 整段覆盖)回退发来一条更短的同 role
|
|
14076
|
+
// 消息,保留本地版本——文字会被刷新或下一次 emit 修正。
|
|
14077
|
+
var localLast = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
14078
|
+
var incoming = msg.data.lastMessage;
|
|
14079
|
+
if (localLast && incoming.role && localLast.role === incoming.role) {
|
|
14080
|
+
msgs[msgs.length - 1] = mergeAssistantTurn(localLast, incoming);
|
|
13566
14081
|
} else if (msgs.length < expectedCount) {
|
|
13567
|
-
msgs.push(
|
|
14082
|
+
msgs.push(incoming);
|
|
13568
14083
|
}
|
|
13569
14084
|
snapshot.messages = msgs;
|
|
13570
14085
|
}
|
|
13571
|
-
} else if (!isIncremental && msg.data.messages) {
|
|
13572
|
-
// Full mode (backward compatible)
|
|
13573
|
-
snapshot.messages = msg.data.messages;
|
|
13574
14086
|
}
|
|
13575
14087
|
|
|
13576
14088
|
// Fast path: chunk-only incremental events skip expensive chat update
|
|
@@ -14353,7 +14865,11 @@
|
|
|
14353
14865
|
var renderWasAtBottom = isChatNearBottom(chatMessages);
|
|
14354
14866
|
if (renderWasAtBottom) state.chatStickToBottom = true;
|
|
14355
14867
|
|
|
14356
|
-
|
|
14868
|
+
// 把 .system-info 卡片从计数里剔除——它由 extractPtySystemInfo 在
|
|
14869
|
+
// fullRenderChat 里穿插注入,不存在于 messages 数组中,混进 existingCount
|
|
14870
|
+
// 会让 msgCount !== existingCount 永远为真,每帧都走 fullRenderChat,从而
|
|
14871
|
+
// 不断 wipe innerHTML,触发"莫名其妙跳到最上面"的视觉错位。
|
|
14872
|
+
var existingCount = chatMessages.querySelectorAll(".chat-message:not(.system-info)").length;
|
|
14357
14873
|
// Full render when: forced, no existing messages, or message count decreased/changed
|
|
14358
14874
|
var needsFullRender = forceRender || existingCount === 0 || msgCount !== existingCount;
|
|
14359
14875
|
|
|
@@ -14401,6 +14917,29 @@
|
|
|
14401
14917
|
'</div>';
|
|
14402
14918
|
}
|
|
14403
14919
|
|
|
14920
|
+
// 在 innerHTML 整段重写前,先记下当前视口里"最靠近顶部边缘"的那条消息
|
|
14921
|
+
// 的 data-msg-index 和它到容器顶部的偏移。重写完成后找到同一 data-msg-index
|
|
14922
|
+
// 的新节点,把它放回原来的偏移——这是 column-reverse 下保住用户视线的
|
|
14923
|
+
// 标准锚点法。没有锚点时(首次渲染、空 → 非空)才走 scrollTop=0 兜底。
|
|
14924
|
+
var anchorMsgIndex = -1;
|
|
14925
|
+
var anchorOffset = 0;
|
|
14926
|
+
if (prevMsgCount > 0 && !renderWasAtBottom) {
|
|
14927
|
+
var containerTop = chatMessages.getBoundingClientRect().top;
|
|
14928
|
+
var preEls = chatMessages.querySelectorAll(".chat-message:not(.system-info)");
|
|
14929
|
+
for (var pi = 0; pi < preEls.length; pi++) {
|
|
14930
|
+
var rect = preEls[pi].getBoundingClientRect();
|
|
14931
|
+
// 第一条 top >= containerTop 的就是视口内最靠上的可见消息
|
|
14932
|
+
if (rect.bottom >= containerTop) {
|
|
14933
|
+
var idxAttr = preEls[pi].getAttribute("data-msg-index");
|
|
14934
|
+
if (idxAttr != null) {
|
|
14935
|
+
anchorMsgIndex = parseInt(idxAttr, 10);
|
|
14936
|
+
anchorOffset = rect.top - containerTop;
|
|
14937
|
+
}
|
|
14938
|
+
break;
|
|
14939
|
+
}
|
|
14940
|
+
}
|
|
14941
|
+
}
|
|
14942
|
+
|
|
14404
14943
|
chatMessages.innerHTML = html;
|
|
14405
14944
|
// 给每条消息打 data-msg-index(用 state.currentMessages 的全局索引),
|
|
14406
14945
|
// 后面 refreshChatUnreadDivider 用它找未读分割线的位置。
|
|
@@ -14425,6 +14964,21 @@
|
|
|
14425
14964
|
// 同一会话内的全量重渲染:用户原本贴底就保持贴底,浏览器在 innerHTML
|
|
14426
14965
|
// 重置后可能把 scrollTop 钳到一个奇怪的值,这里显式拉回 0。
|
|
14427
14966
|
chatMessages.scrollTop = 0;
|
|
14967
|
+
} else if (anchorMsgIndex >= 0) {
|
|
14968
|
+
// 用户当前不在底部——根据保存的锚点恢复视图位置,避免被"踢到最上面"。
|
|
14969
|
+
var newAnchor = chatMessages.querySelector(
|
|
14970
|
+
'.chat-message[data-msg-index="' + anchorMsgIndex + '"]'
|
|
14971
|
+
);
|
|
14972
|
+
if (newAnchor) {
|
|
14973
|
+
var newContainerTop = chatMessages.getBoundingClientRect().top;
|
|
14974
|
+
var newRect = newAnchor.getBoundingClientRect();
|
|
14975
|
+
var delta = (newRect.top - newContainerTop) - anchorOffset;
|
|
14976
|
+
if (Math.abs(delta) > 0.5) {
|
|
14977
|
+
state.chatIsProgrammaticScroll = true;
|
|
14978
|
+
chatMessages.scrollTop += delta;
|
|
14979
|
+
requestAnimationFrame(function() { state.chatIsProgrammaticScroll = false; });
|
|
14980
|
+
}
|
|
14981
|
+
}
|
|
14428
14982
|
}
|
|
14429
14983
|
attachAllCopyHandlers(chatMessages);
|
|
14430
14984
|
bindChatScrollListener();
|
|
@@ -14584,7 +15138,10 @@
|
|
|
14584
15138
|
// Optimization: only re-render the newest N messages (column-reverse: first children)
|
|
14585
15139
|
// that actually differ, starting from the top (newest). Most streaming updates only
|
|
14586
15140
|
// touch the latest assistant turn, so we can skip scanning all older messages.
|
|
14587
|
-
|
|
15141
|
+
// 同样剔除 system-info 卡片,否则 existingEls 长度对不上 reversedMessages,
|
|
15142
|
+
// top-N 对照会拿 system-info 卡片去比真消息的 HTML,永远 replacedAny=false,
|
|
15143
|
+
// 触发 fullRenderChat 兜底分支——这是滚动跳顶的另一条触发路径。
|
|
15144
|
+
var existingEls = Array.from(chatMessages.querySelectorAll(".chat-message:not(.system-info)"));
|
|
14588
15145
|
var reversedMessages = messages.slice().reverse();
|
|
14589
15146
|
var replacedAny = false;
|
|
14590
15147
|
// Scan from newest (index 0 in reversed) up to MAX_STREAMING_SCAN messages
|
|
@@ -17703,29 +18260,29 @@
|
|
|
17703
18260
|
})
|
|
17704
18261
|
.then(function(res) { return res.json(); })
|
|
17705
18262
|
.then(function(data) {
|
|
17706
|
-
setProgress(false);
|
|
17707
|
-
card.classList.remove("is-busy");
|
|
17708
18263
|
if (data.error) {
|
|
17709
18264
|
// Update failed
|
|
18265
|
+
setProgress(false);
|
|
18266
|
+
card.classList.remove("is-busy");
|
|
17710
18267
|
setSubtitle("\u66f4\u65b0\u672a\u5b8c\u6210");
|
|
17711
18268
|
setStatus(data.error, "error");
|
|
17712
18269
|
actionBtn.disabled = false;
|
|
17713
18270
|
if (actionLabel) actionLabel.textContent = "\u91cd\u8bd5";
|
|
17714
18271
|
return;
|
|
17715
18272
|
}
|
|
17716
|
-
// Phase 2:
|
|
17717
|
-
setSubtitle(data.message || "\u66f4\u65b0\u5b8c\u6210\uff0c\u91cd\u542f\u540e\u751f\u6548");
|
|
17718
|
-
setStatus("");
|
|
18273
|
+
// Phase 2: \u5b89\u88c5\u6210\u529f\uff0c\u81ea\u52a8\u8c03\u7528 /api/restart\uff0c\u7531 restart overlay \u63a5\u7ba1 UX\u3002
|
|
17719
18274
|
card.classList.add("is-success");
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
18275
|
+
setSubtitle((data.message || "\u66f4\u65b0\u5b8c\u6210") + "\uff0c\u6b63\u5728\u91cd\u542f\u670d\u52a1\u2026");
|
|
18276
|
+
setStatus("");
|
|
18277
|
+
if (actionLabel) actionLabel.textContent = "\u6b63\u5728\u91cd\u542f\u2026";
|
|
18278
|
+
if (data.restartRequired === false) {
|
|
18279
|
+
setProgress(false);
|
|
18280
|
+
card.classList.remove("is-busy");
|
|
18281
|
+
actionBtn.disabled = false;
|
|
18282
|
+
if (actionLabel) actionLabel.textContent = "\u5df2\u5b8c\u6210";
|
|
18283
|
+
return;
|
|
18284
|
+
}
|
|
18285
|
+
performRestartCard(actionBtn, actionLabel, subtitleEl, statusEl, progressEl);
|
|
17729
18286
|
})
|
|
17730
18287
|
.catch(function() {
|
|
17731
18288
|
setProgress(false);
|