@co0ontty/wand 1.25.5 → 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.
@@ -195,7 +195,21 @@
195
195
  quickCommitSubmitting: false,
196
196
  quickCommitGenerating: false,
197
197
  quickCommitError: "",
198
- quickCommitForm: { autoMessage: false, customMessage: "", makeTag: false, tag: "", push: false },
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 = { autoMessage: false, customMessage: "", makeTag: false, tag: "", push: false };
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
- var pushCb = document.getElementById("quick-commit-push");
1865
- if (pushCb) pushCb.addEventListener("change", function() {
1866
- state.quickCommitForm.push = pushCb.checked;
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
+ }
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();
1867
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: !!form.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
- var pushRequested = !!payload.push;
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
- function renderQuickCommitModal() {
1975
- var s = state.gitStatus || {};
1976
- var f = state.quickCommitForm || { autoMessage: false, customMessage: "", makeTag: false, tag: "", push: false };
1977
- var langValue = (state.config && (state.config.language || "")) || "";
1978
- var langLabel = langValue ? langValue : "中文";
1979
- var files = Array.isArray(s.files) ? s.files : [];
1980
- var fileRows = files.map(function(item) {
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
- if (!fileRows) fileRows = '<div class="qc-empty">工作区干净,没有可提交的改动。</div>';
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">' + escapeHtml((s.branch || "(no branch)") + ' · ' + (s.modifiedCount || 0) + ' 个改动') + '</p>' +
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
- '<div class="qc-files-wrap">' + fileRows + '</div>' +
2015
- '<div class="qc-message-row" id="quick-commit-message-row">' +
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>';