@co0ontty/wand 1.42.0 → 1.43.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.
@@ -284,19 +284,11 @@
284
284
  quickCommitSubmitting: false,
285
285
  quickCommitGenerating: false,
286
286
  quickCommitError: "",
287
- // commitMode: "commit-tag" (commit + version tag) | "commit" (commit only).
288
- // Pushing is a separate, standalone action — never bundled into the commit button.
289
- quickCommitForm: { customMessage: "", tag: "", tagEdited: false, commitMode: "commit-tag" },
290
- // Which inline panel/dropdown is open. Only one can be open at a time, so a
291
- // single field beats juggling three sibling booleans with mutual-exclusion code.
292
- // Values: null | "action" | "push" | "tag-head".
293
- quickCommitOpenMenu: null,
294
- quickCommitTagHeadForm: { tag: "", push: false },
295
- quickCommitTagHeadSubmitting: false,
296
- quickCommitTagHeadGenerating: false,
297
- quickCommitTagHeadError: "",
287
+ quickCommitForm: { customMessage: "", tag: "", tagEdited: false },
298
288
  quickCommitPushing: false,
299
289
  quickCommitPushError: "",
290
+ quickCommitResult: null,
291
+ quickCommitDragAction: "commit",
300
292
  // Telegram 风格的"贴底"状态:true = 用户当前贴在底部,新消息会自然出现;
301
293
  // false = 用户向上滚了,未读会累积到气泡里,不会自动滚他们的视图。
302
294
  chatStickToBottom: true,
@@ -2249,19 +2241,50 @@
2249
2241
  }
2250
2242
 
2251
2243
  var quickCommitEscHandler = null;
2252
- var quickCommitDocClickHandler = null;
2253
-
2254
- // Restore the user's last commit-mode choice so the split button feels sticky.
2255
- // Modes: "commit-tag" (commit + version tag) | "commit" (commit only).
2256
- function readSavedCommitMode() {
2257
- try {
2258
- var v = localStorage.getItem("wand.quickCommit.commitMode");
2259
- if (v === "commit" || v === "commit-tag") return v;
2260
- } catch (e) { /* localStorage may be blocked */ }
2261
- return "commit-tag"; // default: commit + tag
2244
+ var quickCommitDragCleanup = null;
2245
+ var quickCommitDragState = null;
2246
+
2247
+ function normalizeQuickCommitAction(value) {
2248
+ if (value === "commit-tag" || value === "commit-tag-push") return value;
2249
+ return "commit";
2250
+ }
2251
+
2252
+ function getQuickCommitActionMeta(action) {
2253
+ action = normalizeQuickCommitAction(action);
2254
+ if (action === "commit-tag-push") {
2255
+ return {
2256
+ action: action,
2257
+ label: "Commit + Tag + Push",
2258
+ verb: "提交、打 Tag 并推送",
2259
+ withTag: true,
2260
+ push: true,
2261
+ tone: "push",
2262
+ };
2263
+ }
2264
+ if (action === "commit-tag") {
2265
+ return {
2266
+ action: action,
2267
+ label: "Commit + Tag",
2268
+ verb: "提交并打 Tag",
2269
+ withTag: true,
2270
+ push: false,
2271
+ tone: "tag",
2272
+ };
2273
+ }
2274
+ return {
2275
+ action: "commit",
2276
+ label: "Commit",
2277
+ verb: "仅提交",
2278
+ withTag: false,
2279
+ push: false,
2280
+ tone: "commit",
2281
+ };
2262
2282
  }
2263
- function saveCommitMode(value) {
2264
- try { localStorage.setItem("wand.quickCommit.commitMode", value); } catch (e) { /* no-op */ }
2283
+
2284
+ function getQuickCommitActionFromRatio(ratio) {
2285
+ if (ratio >= 0.74) return "commit-tag-push";
2286
+ if (ratio >= 0.38) return "commit-tag";
2287
+ return "commit";
2265
2288
  }
2266
2289
 
2267
2290
  function openQuickCommitModal() {
@@ -2274,16 +2297,11 @@
2274
2297
  tag: "",
2275
2298
  // Whether the user has manually edited the tag (so we stop auto-overwriting it).
2276
2299
  tagEdited: false,
2277
- // "commit-tag" → commit + version tag; "commit" → commit only.
2278
- commitMode: readSavedCommitMode(),
2279
2300
  };
2280
- state.quickCommitOpenMenu = null;
2281
- state.quickCommitTagHeadForm = { tag: "", push: false };
2282
- state.quickCommitTagHeadSubmitting = false;
2283
- state.quickCommitTagHeadGenerating = false;
2284
- state.quickCommitTagHeadError = "";
2285
2301
  state.quickCommitPushing = false;
2286
2302
  state.quickCommitPushError = "";
2303
+ state.quickCommitResult = null;
2304
+ state.quickCommitDragAction = "commit";
2287
2305
  closeWorktreeMergeModal();
2288
2306
  closeSessionModal();
2289
2307
  closeSettingsModal();
@@ -2296,33 +2314,11 @@
2296
2314
  }
2297
2315
  if (quickCommitEscHandler) document.removeEventListener("keydown", quickCommitEscHandler);
2298
2316
  quickCommitEscHandler = function(e) {
2299
- if (e.key === "Escape" && state.quickCommitOpen && !state.quickCommitSubmitting && !state.quickCommitTagHeadSubmitting && !state.quickCommitPushing) {
2300
- // First Esc closes any open dropdown; second closes the modal.
2301
- if (state.quickCommitOpenMenu) {
2302
- state.quickCommitOpenMenu = null;
2303
- rerenderQuickCommitModal();
2304
- return;
2305
- }
2317
+ if (e.key === "Escape" && state.quickCommitOpen && !state.quickCommitSubmitting && !state.quickCommitPushing) {
2306
2318
  closeQuickCommitModal();
2307
2319
  }
2308
2320
  };
2309
2321
  document.addEventListener("keydown", quickCommitEscHandler);
2310
- if (quickCommitDocClickHandler) document.removeEventListener("click", quickCommitDocClickHandler, true);
2311
- quickCommitDocClickHandler = function(e) {
2312
- if (!state.quickCommitOpen) return;
2313
- // tag-head is an inline drawer, not a dropdown — clicking outside shouldn't close it.
2314
- if (state.quickCommitOpenMenu !== "action" && state.quickCommitOpenMenu !== "push") return;
2315
- var modalEl = document.getElementById("quick-commit-modal");
2316
- if (!modalEl) return;
2317
- var t = e.target;
2318
- while (t && t !== modalEl) {
2319
- if (t.dataset && (t.dataset.qcDropdownToggle || t.dataset.qcDropdownMenu)) return;
2320
- t = t.parentNode;
2321
- }
2322
- state.quickCommitOpenMenu = null;
2323
- rerenderQuickCommitModal();
2324
- };
2325
- document.addEventListener("click", quickCommitDocClickHandler, true);
2326
2322
  loadGitStatus(state.selectedId, { force: true }).then(function() {
2327
2323
  if (!state.quickCommitOpen) return;
2328
2324
  // Seed the tag field with the locally-derived suggestion so a tag is
@@ -2339,7 +2335,8 @@
2339
2335
  state.quickCommitOpen = false;
2340
2336
  state.quickCommitSubmitting = false;
2341
2337
  state.quickCommitError = "";
2342
- state.quickCommitOpenMenu = null;
2338
+ state.quickCommitResult = null;
2339
+ state.quickCommitDragAction = "commit";
2343
2340
  var modal = document.getElementById("quick-commit-modal");
2344
2341
  if (modal) modal.classList.add("hidden");
2345
2342
  if (focusTrapHandler) {
@@ -2350,9 +2347,9 @@
2350
2347
  document.removeEventListener("keydown", quickCommitEscHandler);
2351
2348
  quickCommitEscHandler = null;
2352
2349
  }
2353
- if (quickCommitDocClickHandler) {
2354
- document.removeEventListener("click", quickCommitDocClickHandler, true);
2355
- quickCommitDocClickHandler = null;
2350
+ if (quickCommitDragCleanup) {
2351
+ quickCommitDragCleanup();
2352
+ quickCommitDragCleanup = null;
2356
2353
  }
2357
2354
  if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
2358
2355
  lastFocusedElement.focus();
@@ -2362,6 +2359,10 @@
2362
2359
  function rerenderQuickCommitModal() {
2363
2360
  var modal = document.getElementById("quick-commit-modal");
2364
2361
  if (!modal) return;
2362
+ if (quickCommitDragCleanup) {
2363
+ quickCommitDragCleanup();
2364
+ quickCommitDragCleanup = null;
2365
+ }
2365
2366
  var html = renderQuickCommitModal();
2366
2367
  var temp = document.createElement("div");
2367
2368
  temp.innerHTML = html;
@@ -2377,8 +2378,6 @@
2377
2378
  var cancelBtn = document.getElementById("quick-commit-cancel-btn");
2378
2379
  if (cancelBtn) cancelBtn.addEventListener("click", closeQuickCommitModal);
2379
2380
 
2380
- var submitBtn = document.getElementById("quick-commit-submit-btn");
2381
- if (submitBtn) submitBtn.addEventListener("click", submitQuickCommit);
2382
2381
  var aiBtn = document.getElementById("quick-commit-ai-btn");
2383
2382
  if (aiBtn) aiBtn.addEventListener("click", generateCommitMessageAI);
2384
2383
  var msgEl = document.getElementById("quick-commit-message");
@@ -2390,7 +2389,7 @@
2390
2389
  msgEl.addEventListener("keydown", function(e) {
2391
2390
  if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
2392
2391
  e.preventDefault();
2393
- submitQuickCommit();
2392
+ submitQuickCommit("commit");
2394
2393
  }
2395
2394
  });
2396
2395
  }
@@ -2403,114 +2402,136 @@
2403
2402
  tagInput.addEventListener("keydown", function(e) {
2404
2403
  if (e.key === "Enter") {
2405
2404
  e.preventDefault();
2406
- submitQuickCommit();
2405
+ submitQuickCommit("commit-tag");
2407
2406
  }
2408
2407
  });
2409
2408
  }
2410
2409
 
2411
- var actionCaret = document.getElementById("quick-commit-action-caret");
2412
- if (actionCaret) actionCaret.addEventListener("click", function(e) {
2413
- e.stopPropagation();
2414
- state.quickCommitOpenMenu = state.quickCommitOpenMenu === "action" ? null : "action";
2415
- rerenderQuickCommitModal();
2410
+ var pushAfterBtn = document.getElementById("quick-commit-push-after-btn");
2411
+ if (pushAfterBtn) pushAfterBtn.addEventListener("click", function() {
2412
+ var result = state.quickCommitResult || {};
2413
+ submitPushOnly({ pushCommits: true, pushTags: !!result.tagName, closeOnSuccess: true });
2416
2414
  });
2417
- var actionMenu = document.getElementById("quick-commit-action-menu");
2418
- if (actionMenu) {
2419
- var actionItems = actionMenu.querySelectorAll("[data-qc-commit-mode]");
2420
- for (var i = 0; i < actionItems.length; i++) {
2421
- (function(btn) {
2422
- btn.addEventListener("click", function() {
2423
- // Keep what the user typed before the re-render.
2424
- var liveTag = document.getElementById("quick-commit-tag");
2425
- if (liveTag && !liveTag.disabled) state.quickCommitForm.tag = liveTag.value;
2426
- var value = btn.getAttribute("data-qc-commit-mode");
2427
- if (value === "commit" || value === "commit-tag") {
2428
- state.quickCommitForm.commitMode = value;
2429
- saveCommitMode(value);
2430
- }
2431
- state.quickCommitOpenMenu = null;
2432
- rerenderQuickCommitModal();
2433
- if (value === "commit-tag") {
2434
- var inp = document.getElementById("quick-commit-tag");
2435
- if (inp) setTimeout(function() { inp.focus(); var v = inp.value; inp.value = ""; inp.value = v; }, 0);
2436
- }
2437
- });
2438
- })(actionItems[i]);
2439
- }
2440
- }
2441
2415
 
2442
- var tagHeadToggle = document.getElementById("quick-commit-tag-head-toggle");
2443
- if (tagHeadToggle) tagHeadToggle.addEventListener("click", function() {
2444
- var willOpen = state.quickCommitOpenMenu !== "tag-head";
2445
- state.quickCommitOpenMenu = willOpen ? "tag-head" : null;
2446
- if (willOpen) {
2447
- state.quickCommitTagHeadError = "";
2448
- // Pre-fill with the locally-derived suggestion (unless already set).
2449
- var sug = (state.gitStatus || {}).suggestedTag;
2450
- if (sug && !(state.quickCommitTagHeadForm.tag || "").trim()) {
2451
- state.quickCommitTagHeadForm.tag = sug;
2452
- }
2416
+ attachQuickCommitDrag();
2417
+ }
2418
+
2419
+ function updateQuickCommitDragVisual(action, ratio) {
2420
+ action = normalizeQuickCommitAction(action);
2421
+ state.quickCommitDragAction = action;
2422
+ var track = document.getElementById("quick-commit-drag-track");
2423
+ if (!track) return;
2424
+ var meta = getQuickCommitActionMeta(action);
2425
+ var progress = typeof ratio === "number"
2426
+ ? Math.max(0, Math.min(1, ratio))
2427
+ : (action === "commit-tag-push" ? 1 : (action === "commit-tag" ? 0.5 : 0));
2428
+ track.setAttribute("data-action", action);
2429
+ track.style.setProperty("--qc-progress", (progress * 100).toFixed(1) + "%");
2430
+ var knob = document.getElementById("quick-commit-drag-action");
2431
+ if (knob) {
2432
+ var maxX = Math.max(0, track.clientWidth - knob.offsetWidth - 14);
2433
+ track.style.setProperty("--qc-knob-x", (maxX * progress).toFixed(1) + "px");
2434
+ }
2435
+ var label = document.getElementById("quick-commit-drag-label");
2436
+ if (label) label.textContent = state.quickCommitSubmitting ? "执行中..." : meta.label;
2437
+ var stages = track.querySelectorAll("[data-qc-stage]");
2438
+ var order = { "commit": 0, "commit-tag": 1, "commit-tag-push": 2 };
2439
+ for (var i = 0; i < stages.length; i++) {
2440
+ var stageAction = stages[i].getAttribute("data-qc-stage");
2441
+ var passed = order[stageAction] <= order[action];
2442
+ stages[i].classList.toggle("is-active", stageAction === action);
2443
+ stages[i].classList.toggle("is-passed", passed);
2444
+ }
2445
+ }
2446
+
2447
+ function attachQuickCommitDrag() {
2448
+ var track = document.getElementById("quick-commit-drag-track");
2449
+ var knob = document.getElementById("quick-commit-drag-action");
2450
+ if (!track || !knob) return;
2451
+ updateQuickCommitDragVisual(state.quickCommitDragAction || "commit");
2452
+
2453
+ var onPointerDown = function(e) {
2454
+ if (knob.disabled || isQuickCommitOpInFlight()) return;
2455
+ var rect = track.getBoundingClientRect();
2456
+ quickCommitDragState = {
2457
+ pointerId: e.pointerId,
2458
+ rect: rect,
2459
+ moved: false,
2460
+ action: "commit",
2461
+ };
2462
+ knob.setPointerCapture(e.pointerId);
2463
+ track.classList.add("is-dragging");
2464
+ updateQuickCommitDragVisual("commit", 0);
2465
+ e.preventDefault();
2466
+ };
2467
+ var onPointerMove = function(e) {
2468
+ if (!quickCommitDragState || quickCommitDragState.pointerId !== e.pointerId) return;
2469
+ var rect = quickCommitDragState.rect;
2470
+ var ratio = rect.width > 0 ? (e.clientX - rect.left) / rect.width : 0;
2471
+ ratio = Math.max(0, Math.min(1, ratio));
2472
+ if (Math.abs(e.clientX - rect.left) > 6) quickCommitDragState.moved = true;
2473
+ var action = getQuickCommitActionFromRatio(ratio);
2474
+ quickCommitDragState.action = action;
2475
+ updateQuickCommitDragVisual(action, ratio);
2476
+ };
2477
+ var finish = function(e, cancelled) {
2478
+ if (!quickCommitDragState) return;
2479
+ var current = quickCommitDragState;
2480
+ quickCommitDragState = null;
2481
+ track.classList.remove("is-dragging");
2482
+ try {
2483
+ if (typeof knob.releasePointerCapture === "function") knob.releasePointerCapture(current.pointerId);
2484
+ } catch (err) { /* ignored */ }
2485
+ if (cancelled) {
2486
+ updateQuickCommitDragVisual("commit");
2487
+ return;
2453
2488
  }
2454
- rerenderQuickCommitModal();
2455
- if (willOpen) {
2456
- var inp = document.getElementById("quick-commit-tag-head-input");
2457
- if (inp) inp.focus();
2489
+ var action = current.moved ? current.action : "commit";
2490
+ updateQuickCommitDragVisual(action);
2491
+ submitQuickCommit(action);
2492
+ if (e && typeof e.preventDefault === "function") e.preventDefault();
2493
+ };
2494
+ var onPointerUp = function(e) {
2495
+ if (!quickCommitDragState || quickCommitDragState.pointerId !== e.pointerId) return;
2496
+ finish(e, false);
2497
+ };
2498
+ var onPointerCancel = function(e) {
2499
+ if (!quickCommitDragState || quickCommitDragState.pointerId !== e.pointerId) return;
2500
+ finish(e, true);
2501
+ };
2502
+ var onKeyDown = function(e) {
2503
+ if (knob.disabled || isQuickCommitOpInFlight()) return;
2504
+ if (e.key === "ArrowRight") {
2505
+ e.preventDefault();
2506
+ var next = state.quickCommitDragAction === "commit"
2507
+ ? "commit-tag"
2508
+ : (state.quickCommitDragAction === "commit-tag" ? "commit-tag-push" : "commit-tag-push");
2509
+ updateQuickCommitDragVisual(next);
2510
+ } else if (e.key === "ArrowLeft") {
2511
+ e.preventDefault();
2512
+ var prev = state.quickCommitDragAction === "commit-tag-push"
2513
+ ? "commit-tag"
2514
+ : (state.quickCommitDragAction === "commit-tag" ? "commit" : "commit");
2515
+ updateQuickCommitDragVisual(prev);
2516
+ } else if (e.key === "Enter" || e.key === " ") {
2517
+ e.preventDefault();
2518
+ submitQuickCommit(state.quickCommitDragAction || "commit");
2458
2519
  }
2459
- });
2460
- var tagHeadCancel = document.getElementById("quick-commit-tag-head-cancel");
2461
- if (tagHeadCancel) tagHeadCancel.addEventListener("click", function() {
2462
- if (state.quickCommitOpenMenu === "tag-head") state.quickCommitOpenMenu = null;
2463
- state.quickCommitTagHeadError = "";
2464
- rerenderQuickCommitModal();
2465
- });
2466
- var tagHeadInput = document.getElementById("quick-commit-tag-head-input");
2467
- if (tagHeadInput) {
2468
- tagHeadInput.addEventListener("input", function() {
2469
- state.quickCommitTagHeadForm.tag = tagHeadInput.value;
2470
- });
2471
- tagHeadInput.addEventListener("keydown", function(e) {
2472
- if (e.key === "Enter") {
2473
- e.preventDefault();
2474
- submitTagHead(false);
2475
- }
2476
- });
2477
- }
2478
- var tagHeadAi = document.getElementById("quick-commit-tag-head-ai");
2479
- if (tagHeadAi) tagHeadAi.addEventListener("click", generateTagHeadAI);
2480
- var tagHeadPushCb = document.getElementById("quick-commit-tag-head-push");
2481
- if (tagHeadPushCb) tagHeadPushCb.addEventListener("change", function() {
2482
- state.quickCommitTagHeadForm.push = tagHeadPushCb.checked;
2483
- });
2484
- var tagHeadSubmit = document.getElementById("quick-commit-tag-head-submit");
2485
- if (tagHeadSubmit) tagHeadSubmit.addEventListener("click", function() {
2486
- submitTagHead(false);
2487
- });
2520
+ };
2488
2521
 
2489
- var pushBtn = document.getElementById("quick-commit-push-btn");
2490
- if (pushBtn) pushBtn.addEventListener("click", function() {
2491
- submitPushOnly({ pushCommits: true, pushTags: false });
2492
- });
2493
- var pushCaret = document.getElementById("quick-commit-push-caret");
2494
- if (pushCaret) pushCaret.addEventListener("click", function(e) {
2495
- e.stopPropagation();
2496
- state.quickCommitOpenMenu = state.quickCommitOpenMenu === "push" ? null : "push";
2497
- rerenderQuickCommitModal();
2498
- });
2499
- var pushMenu = document.getElementById("quick-commit-push-menu");
2500
- if (pushMenu) {
2501
- var pushItems = pushMenu.querySelectorAll("[data-qc-push]");
2502
- for (var j = 0; j < pushItems.length; j++) {
2503
- (function(btn) {
2504
- btn.addEventListener("click", function() {
2505
- var value = btn.getAttribute("data-qc-push");
2506
- state.quickCommitOpenMenu = null;
2507
- if (value === "commits") submitPushOnly({ pushCommits: true, pushTags: false });
2508
- else if (value === "tags") submitPushOnly({ pushCommits: false, pushTags: true });
2509
- else if (value === "both") submitPushOnly({ pushCommits: true, pushTags: true });
2510
- });
2511
- })(pushItems[j]);
2512
- }
2513
- }
2522
+ knob.addEventListener("pointerdown", onPointerDown);
2523
+ knob.addEventListener("pointermove", onPointerMove);
2524
+ knob.addEventListener("pointerup", onPointerUp);
2525
+ knob.addEventListener("pointercancel", onPointerCancel);
2526
+ knob.addEventListener("keydown", onKeyDown);
2527
+ quickCommitDragCleanup = function() {
2528
+ knob.removeEventListener("pointerdown", onPointerDown);
2529
+ knob.removeEventListener("pointermove", onPointerMove);
2530
+ knob.removeEventListener("pointerup", onPointerUp);
2531
+ knob.removeEventListener("pointercancel", onPointerCancel);
2532
+ knob.removeEventListener("keydown", onKeyDown);
2533
+ quickCommitDragState = null;
2534
+ };
2514
2535
  }
2515
2536
 
2516
2537
  function generateCommitMessageAI() {
@@ -2548,8 +2569,7 @@
2548
2569
  // recommendation is actually applied on commit.
2549
2570
  if (aiTag) {
2550
2571
  if (!state.quickCommitForm.tagEdited) state.quickCommitForm.tag = aiTag;
2551
- state.quickCommitForm.commitMode = "commit-tag";
2552
- saveCommitMode("commit-tag");
2572
+ state.quickCommitDragAction = "commit-tag";
2553
2573
  }
2554
2574
  })
2555
2575
  .catch(function(error) {
@@ -2561,14 +2581,15 @@
2561
2581
  });
2562
2582
  }
2563
2583
 
2564
- function submitQuickCommit() {
2584
+ function submitQuickCommit(action) {
2565
2585
  if (!state.selectedId || state.quickCommitSubmitting) return;
2566
2586
  var msgEl = document.getElementById("quick-commit-message");
2567
2587
  if (msgEl) state.quickCommitForm.customMessage = msgEl.value;
2568
2588
  var tagEl = document.getElementById("quick-commit-tag");
2569
2589
  if (tagEl) state.quickCommitForm.tag = tagEl.value;
2570
2590
  var form = state.quickCommitForm || {};
2571
- var withTag = form.commitMode === "commit-tag";
2591
+ var meta = getQuickCommitActionMeta(action || state.quickCommitDragAction || "commit");
2592
+ var withTag = meta.withTag;
2572
2593
  var userTag = withTag ? (form.tag || "").trim() : "";
2573
2594
  var message = (form.customMessage || "").trim();
2574
2595
  if (!message) {
@@ -2576,17 +2597,28 @@
2576
2597
  rerenderQuickCommitModal();
2577
2598
  return;
2578
2599
  }
2579
- // Commit no longer pushes — pushing is a separate, standalone action.
2580
- // 选了「提交并打 Tag」但 tag 留空 → 由后端在提交时调 AI 生成。
2600
+ var before = {
2601
+ branch: (state.gitStatus || {}).branch || "",
2602
+ commitHash: (state.gitStatus || {}).lastCommit && (state.gitStatus || {}).lastCommit.shortHash
2603
+ ? (state.gitStatus || {}).lastCommit.shortHash
2604
+ : ((state.gitStatus || {}).head ? (state.gitStatus || {}).head.substring(0, 7) : ""),
2605
+ commitSubject: (state.gitStatus || {}).lastCommit && (state.gitStatus || {}).lastCommit.subject
2606
+ ? (state.gitStatus || {}).lastCommit.subject
2607
+ : "",
2608
+ tag: (state.gitStatus || {}).latestTag || "",
2609
+ };
2581
2610
  var payload = {
2582
2611
  autoMessage: false,
2583
2612
  customMessage: message,
2584
2613
  tag: userTag,
2585
2614
  autoTag: !!(withTag && !userTag),
2586
- push: false
2615
+ push: !!meta.push
2587
2616
  };
2588
2617
  state.quickCommitSubmitting = true;
2589
2618
  state.quickCommitError = "";
2619
+ state.quickCommitPushError = "";
2620
+ state.quickCommitResult = null;
2621
+ state.quickCommitDragAction = meta.action;
2590
2622
  rerenderQuickCommitModal();
2591
2623
  fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/quick-commit", {
2592
2624
  method: "POST",
@@ -2607,99 +2639,35 @@
2607
2639
  ? "已先提交 " + subCommits.length + " 个 submodule(" + subCommits.map(function(c) { return c.path; }).join("、") + "),"
2608
2640
  : "";
2609
2641
  var base = subPrefix + "已提交" + (hash ? " " + hash : "") + (tagName ? ",已打 Tag " + tagName : "");
2610
- if (typeof showToast === "function") showToast(base + "。可在「同步」区推送到远端。", "success");
2611
- closeQuickCommitModal();
2612
- if (state.selectedId) loadGitStatus(state.selectedId, { force: true });
2613
- })
2614
- .catch(function(error) {
2615
- state.quickCommitError = (error && error.message) || "快捷提交失败。";
2616
- })
2617
- .finally(function() {
2618
- state.quickCommitSubmitting = false;
2619
- if (state.quickCommitOpen) rerenderQuickCommitModal();
2620
- });
2621
- }
2622
-
2623
- // ── AI generate a tag for the existing HEAD (no commit involved) ──
2624
- function generateTagHeadAI() {
2625
- if (!state.selectedId || state.quickCommitTagHeadGenerating) return;
2626
- var inp = document.getElementById("quick-commit-tag-head-input");
2627
- if (inp) state.quickCommitTagHeadForm.tag = inp.value;
2628
- state.quickCommitTagHeadGenerating = true;
2629
- state.quickCommitTagHeadError = "";
2630
- rerenderQuickCommitModal();
2631
- // Reuse the existing generator — it stages and asks for {message, tag}.
2632
- // We only consume `suggestedTag` here.
2633
- fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/generate-commit-message", {
2634
- method: "POST",
2635
- credentials: "same-origin",
2636
- headers: { "Content-Type": "application/json" },
2637
- body: JSON.stringify({})
2638
- })
2639
- .then(function(res) { return res.json().then(function(data) { return { ok: res.ok, data: data }; }); })
2640
- .then(function(result) {
2641
- if (!result.ok) throw new Error((result.data && result.data.error) || "AI 生成失败。");
2642
- var aiTag = (result.data && typeof result.data.suggestedTag === "string") ? result.data.suggestedTag.trim() : "";
2643
- var currentTag = (state.quickCommitTagHeadForm.tag || "").trim();
2644
- if (!currentTag && aiTag) {
2645
- state.quickCommitTagHeadForm.tag = aiTag;
2646
- } else if (!aiTag) {
2647
- throw new Error("AI 没有给出 tag 建议。");
2648
- }
2649
- })
2650
- .catch(function(error) {
2651
- state.quickCommitTagHeadError = (error && error.message) || "AI 生成失败。";
2652
- })
2653
- .finally(function() {
2654
- state.quickCommitTagHeadGenerating = false;
2655
- if (state.quickCommitOpen) rerenderQuickCommitModal();
2656
- });
2657
- }
2658
-
2659
- // Tag the existing HEAD without making a new commit.
2660
- function submitTagHead(silent) {
2661
- if (!state.selectedId || state.quickCommitTagHeadSubmitting) return;
2662
- var inp = document.getElementById("quick-commit-tag-head-input");
2663
- if (inp) state.quickCommitTagHeadForm.tag = inp.value;
2664
- var tag = (state.quickCommitTagHeadForm.tag || "").trim();
2665
- if (!tag) {
2666
- state.quickCommitTagHeadError = "请填写 tag 名称,或点击 AI 建议。";
2667
- rerenderQuickCommitModal();
2668
- return;
2669
- }
2670
- state.quickCommitTagHeadSubmitting = true;
2671
- state.quickCommitTagHeadError = "";
2672
- rerenderQuickCommitModal();
2673
- fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/git/tag-head", {
2674
- method: "POST",
2675
- credentials: "same-origin",
2676
- headers: { "Content-Type": "application/json" },
2677
- body: JSON.stringify({ tag: tag, push: !!state.quickCommitTagHeadForm.push })
2678
- })
2679
- .then(function(res) { return res.json().then(function(data) { return { ok: res.ok, data: data }; }); })
2680
- .then(function(result) {
2681
- if (!result.ok) throw new Error((result.data && result.data.error) || "打 tag 失败。");
2682
- var data = result.data || {};
2683
- var name = data.tag && data.tag.name ? data.tag.name : tag;
2684
- var pushed = !!data.pushed;
2685
- var pushErr = data.pushError;
2686
- var base = "已为 HEAD 打 tag " + name;
2687
- if (state.quickCommitTagHeadForm.push && pushErr) {
2688
- if (typeof showToast === "function") showToast(base + ";push tag 失败:" + pushErr, "error");
2642
+ state.quickCommitResult = {
2643
+ action: meta.action,
2644
+ pushed: !!data.pushed,
2645
+ pushError: data.pushError || "",
2646
+ commitHash: hash,
2647
+ commitMessage: data.commit && data.commit.message ? data.commit.message : message,
2648
+ tagName: tagName,
2649
+ oldTag: before.tag,
2650
+ oldCommitHash: before.commitHash,
2651
+ oldCommitSubject: before.commitSubject,
2652
+ submoduleCount: subCommits.length,
2653
+ };
2654
+ if (meta.push && !data.pushError) {
2655
+ if (typeof showToast === "function") showToast(base + ",已推送。", "success");
2656
+ closeQuickCommitModal();
2689
2657
  } else {
2690
- if (typeof showToast === "function") showToast(base + (pushed ? ",已 push" : ""), "success");
2658
+ if (typeof showToast === "function") {
2659
+ showToast(base + (data.pushError ? ";push 失败:" + data.pushError : "。"), data.pushError ? "error" : "success");
2660
+ }
2661
+ if (state.selectedId) loadGitStatus(state.selectedId, { force: true }).then(function() {
2662
+ if (state.quickCommitOpen) rerenderQuickCommitModal();
2663
+ });
2691
2664
  }
2692
- if (state.quickCommitOpenMenu === "tag-head") state.quickCommitOpenMenu = null;
2693
- state.quickCommitTagHeadForm = { tag: "", push: false };
2694
- if (state.selectedId) loadGitStatus(state.selectedId, { force: true }).then(function() {
2695
- if (state.quickCommitOpen) rerenderQuickCommitModal();
2696
- });
2697
2665
  })
2698
2666
  .catch(function(error) {
2699
- state.quickCommitTagHeadError = (error && error.message) || "打 tag 失败。";
2667
+ state.quickCommitError = (error && error.message) || "快捷提交失败。";
2700
2668
  })
2701
2669
  .finally(function() {
2702
- state.quickCommitTagHeadSubmitting = false;
2670
+ state.quickCommitSubmitting = false;
2703
2671
  if (state.quickCommitOpen) rerenderQuickCommitModal();
2704
2672
  });
2705
2673
  }
@@ -2708,6 +2676,7 @@
2708
2676
  if (!state.selectedId || state.quickCommitPushing) return;
2709
2677
  var pushCommits = !!(opts && opts.pushCommits);
2710
2678
  var pushTags = !!(opts && opts.pushTags);
2679
+ var closeOnSuccess = !!(opts && opts.closeOnSuccess);
2711
2680
  if (!pushCommits && !pushTags) return;
2712
2681
  state.quickCommitPushing = true;
2713
2682
  state.quickCommitPushError = "";
@@ -2733,9 +2702,15 @@
2733
2702
  if (data.pushedTags) parts.push("tags");
2734
2703
  var label = parts.length ? parts.join(" 和 ") : "(无内容)";
2735
2704
  if (typeof showToast === "function") showToast("已推送 " + label, "success");
2736
- if (state.selectedId) loadGitStatus(state.selectedId, { force: true }).then(function() {
2737
- if (state.quickCommitOpen) rerenderQuickCommitModal();
2738
- });
2705
+ if (state.quickCommitResult) state.quickCommitResult.pushed = true;
2706
+ if (closeOnSuccess) {
2707
+ closeQuickCommitModal();
2708
+ if (state.selectedId) loadGitStatus(state.selectedId, { force: true });
2709
+ } else if (state.selectedId) {
2710
+ loadGitStatus(state.selectedId, { force: true }).then(function() {
2711
+ if (state.quickCommitOpen) rerenderQuickCommitModal();
2712
+ });
2713
+ }
2739
2714
  })
2740
2715
  .catch(function(error) {
2741
2716
  state.quickCommitPushError = (error && error.message) || "推送失败。";
@@ -2800,213 +2775,110 @@
2800
2775
  }
2801
2776
 
2802
2777
  function isQuickCommitOpInFlight() {
2803
- return state.quickCommitSubmitting || state.quickCommitTagHeadSubmitting || state.quickCommitPushing;
2778
+ return state.quickCommitSubmitting || state.quickCommitPushing;
2804
2779
  }
2805
2780
 
2806
- function renderQuickCommitCommitButton(hasChanges) {
2807
- var f = state.quickCommitForm;
2808
- var withTag = f.commitMode === "commit-tag";
2809
- var label;
2810
- if (state.quickCommitSubmitting) label = "提交中…";
2811
- else label = withTag ? "提交并打 Tag" : "提交";
2812
- var disabled = !hasChanges || isQuickCommitOpInFlight();
2813
- var menuOpen = state.quickCommitOpenMenu === "action";
2814
- var caretActive = menuOpen ? " is-active" : "";
2815
- var menuItems = [
2816
- { value: "commit-tag", label: "提交并打 Tag", desc: "创建 commit,并为它打一个版本 Tag" },
2817
- { value: "commit", label: "仅提交", desc: "只创建 commit,不打 Tag" }
2818
- ];
2819
- var menuHtml = menuItems.map(function(item) {
2820
- var sel = f.commitMode === item.value ? " is-selected" : "";
2821
- return '<button type="button" class="qc-dropdown-item' + sel + '" data-qc-commit-mode="' + item.value + '" role="menuitemradio" aria-checked="' + (f.commitMode === item.value ? 'true' : 'false') + '">' +
2822
- '<span class="qc-dropdown-item-main"><span class="qc-dropdown-check" aria-hidden="true">' +
2823
- (f.commitMode === item.value ? '<svg viewBox="0 0 16 16" width="13" height="13"><path d="M13 4.5l-6 6L3 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>' : '') +
2824
- '</span><span class="qc-dropdown-item-title">' + escapeHtml(item.label) + '</span></span>' +
2825
- '<span class="qc-dropdown-item-desc">' + escapeHtml(item.desc) + '</span>' +
2826
- '</button>';
2827
- }).join("");
2828
- return '<div class="qc-split-button">' +
2829
- '<button id="quick-commit-submit-btn" class="btn btn-primary qc-split-main" type="button"' + (disabled ? ' disabled' : '') + '>' +
2830
- escapeHtml(label) +
2831
- '</button>' +
2832
- '<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="切换提交方式">' +
2833
- '<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>' +
2834
- '</button>' +
2835
- (menuOpen ?
2836
- '<div id="quick-commit-action-menu" class="qc-dropdown-menu" data-qc-dropdown-menu="action" role="menu">' + menuHtml + '</div>' : '') +
2837
- '</div>';
2838
- }
2839
-
2840
- function renderQuickCommitStatusChips(s) {
2841
- var chips = [];
2842
- var hasUpstream = !!s.upstream;
2843
- if (typeof s.ahead === "number" && s.ahead > 0) {
2844
- chips.push('<span class="qc-chip qc-chip--ahead" title="本地领先 ' + s.ahead + ' 个 commit">↑ ' + s.ahead + ' 待推送</span>');
2845
- }
2846
- if (typeof s.behind === "number" && s.behind > 0) {
2847
- chips.push('<span class="qc-chip qc-chip--behind" title="远端领先 ' + s.behind + ' 个 commit">↓ ' + s.behind + ' 待拉取</span>');
2848
- }
2849
- if (!hasUpstream) {
2850
- chips.push('<span class="qc-chip qc-chip--warn" title="当前分支没有 upstream,将首次推送时自动设置">无 upstream</span>');
2851
- } else if (!chips.length) {
2852
- chips.push('<span class="qc-chip qc-chip--clean">与远端同步</span>');
2853
- }
2854
- return '<div class="qc-status-chips">' + chips.join("") + '</div>';
2781
+ function renderQuickCommitPair(label, fromHtml, toHtml, extraClass) {
2782
+ return '<div class="qc-pair' + (extraClass ? ' ' + extraClass : '') + '">' +
2783
+ '<div class="qc-pair-label">' + escapeHtml(label) + '</div>' +
2784
+ '<div class="qc-pair-flow">' +
2785
+ '<div class="qc-pair-value qc-pair-value--from">' + fromHtml + '</div>' +
2786
+ '<div class="qc-pair-arrow" aria-hidden="true">→</div>' +
2787
+ '<div class="qc-pair-value qc-pair-value--to">' + toHtml + '</div>' +
2788
+ '</div>' +
2789
+ '</div>';
2855
2790
  }
2856
2791
 
2857
- // Inline drawer used by the "为 HEAD 打 tag" toggle.
2858
- function renderQuickCommitTagHeadPanel() {
2859
- var thf = state.quickCommitTagHeadForm || { tag: "", push: false };
2860
- var submitting = state.quickCommitTagHeadSubmitting;
2861
- var generating = state.quickCommitTagHeadGenerating;
2862
- return '<div class="qc-tag-head-panel">' +
2863
- '<div class="qc-tag-head-hint">为当前最新提交(HEAD)打 Tag,不创建新提交。</div>' +
2864
- '<div class="qc-tag-head-row">' +
2865
- '<input type="text" id="quick-commit-tag-head-input" class="field-input" placeholder="版本号,如 v1.2.0" value="' + escapeHtml(thf.tag || "") + '"' + (submitting ? ' disabled' : '') + '>' +
2866
- '<button type="button" id="quick-commit-tag-head-ai" class="btn btn-ghost btn-sm"' + (generating || submitting ? ' disabled' : '') + '>' + (generating ? '建议中…' : 'AI 建议') + '</button>' +
2867
- '</div>' +
2868
- '<label class="qc-tag-head-push">' +
2869
- '<input type="checkbox" id="quick-commit-tag-head-push"' + (thf.push ? ' checked' : '') + (submitting ? ' disabled' : '') + '>' +
2870
- '<span>打完后立即推送这个 Tag 到远端</span>' +
2871
- '</label>' +
2872
- (state.quickCommitTagHeadError ? '<p class="error-message">' + escapeHtml(state.quickCommitTagHeadError) + '</p>' : '') +
2873
- '<div class="qc-tag-head-actions">' +
2874
- '<button type="button" id="quick-commit-tag-head-cancel" class="btn btn-ghost btn-sm"' + (submitting ? ' disabled' : '') + '>收起</button>' +
2875
- '<button type="button" id="quick-commit-tag-head-submit" class="btn btn-secondary btn-sm"' + (submitting ? ' disabled' : '') + '>' + (submitting ? '打 Tag 中…' : '打 Tag') + '</button>' +
2792
+ function renderQuickCommitDragControl(hasChanges) {
2793
+ var action = normalizeQuickCommitAction(state.quickCommitDragAction || "commit");
2794
+ var meta = getQuickCommitActionMeta(action);
2795
+ var disabled = !hasChanges || isQuickCommitOpInFlight();
2796
+ var label = state.quickCommitSubmitting ? "执行中..." : meta.label;
2797
+ return '<div class="qc-drag-wrap">' +
2798
+ '<div id="quick-commit-drag-track" class="qc-drag-track" data-action="' + escapeHtml(action) + '" style="--qc-progress: ' + (action === "commit-tag-push" ? "100%" : (action === "commit-tag" ? "50%" : "0%")) + ';">' +
2799
+ '<div class="qc-drag-progress" aria-hidden="true"></div>' +
2800
+ '<div class="qc-drag-stages" aria-hidden="true">' +
2801
+ '<span class="qc-drag-stage is-active is-passed" data-qc-stage="commit">Commit</span>' +
2802
+ '<span class="qc-drag-stage' + (action !== "commit" ? ' is-passed' : '') + (action === "commit-tag" ? ' is-active' : '') + '" data-qc-stage="commit-tag">Tag</span>' +
2803
+ '<span class="qc-drag-stage' + (action === "commit-tag-push" ? ' is-active is-passed' : '') + '" data-qc-stage="commit-tag-push">Push</span>' +
2804
+ '</div>' +
2805
+ '<button id="quick-commit-drag-action" class="qc-drag-action" type="button"' + (disabled ? ' disabled' : '') + ' aria-label="' + escapeHtml(meta.verb) + '">' +
2806
+ '<span id="quick-commit-drag-label">' + escapeHtml(label) + '</span>' +
2807
+ '<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M5 12h14"/><path d="M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
2808
+ '</button>' +
2876
2809
  '</div>' +
2877
2810
  '</div>';
2878
2811
  }
2879
2812
 
2880
- function renderQuickCommitPushButton(s) {
2881
- var ahead = typeof s.ahead === "number" ? s.ahead : 0;
2882
- var hasUpstream = !!s.upstream;
2883
- // Allow pushing commits if we're ahead, if upstream is missing (first push will set it up),
2884
- // or if ahead is simply unknown backend will surface real errors.
2885
- var canPushCommits = ahead > 0 || !hasUpstream || typeof s.ahead !== "number";
2886
- // Tag count isn't computed locally (would require a network probe). Let the user try.
2887
- var canPushTags = true;
2888
- var disabled = isQuickCommitOpInFlight() || (!canPushCommits && !canPushTags);
2889
- var mainLabel = state.quickCommitPushing ? "推送中…" : "推送";
2890
- var menuOpen = state.quickCommitOpenMenu === "push";
2891
- var caretActive = menuOpen ? " is-active" : "";
2892
- var items = [
2893
- { value: "commits", label: "推送 commits", desc: ahead ? "推送 " + ahead + " commit" : "推送当前分支", disabled: !canPushCommits },
2894
- { value: "tags", label: "推送 tags", desc: "推送所有本地 tag", disabled: !canPushTags },
2895
- { value: "both", label: "推送 commits 和 tags", desc: "二合一", disabled: !(canPushCommits || canPushTags) }
2896
- ];
2897
- var menuHtml = items.map(function(it) {
2898
- var dis = it.disabled ? " is-disabled" : "";
2899
- return '<button type="button" class="qc-dropdown-item' + dis + '" data-qc-push="' + it.value + '"' + (it.disabled ? ' disabled' : '') + '>' +
2900
- '<span class="qc-dropdown-item-title">' + escapeHtml(it.label) + '</span>' +
2901
- '<span class="qc-dropdown-item-desc">' + escapeHtml(it.desc) + '</span>' +
2902
- '</button>';
2903
- }).join("");
2904
- return '<div class="qc-split-button qc-split-button--secondary">' +
2905
- '<button id="quick-commit-push-btn" class="btn btn-secondary qc-split-main" type="button"' + (disabled ? ' disabled' : '') + '>' +
2906
- escapeHtml(mainLabel) +
2907
- '</button>' +
2908
- '<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="更多推送方式">' +
2909
- '<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>' +
2910
- '</button>' +
2911
- (menuOpen ?
2912
- '<div id="quick-commit-push-menu" class="qc-dropdown-menu qc-dropdown-menu--right" data-qc-dropdown-menu="push" role="menu">' + menuHtml + '</div>' : '') +
2913
- '</div>';
2813
+ function renderQuickCommitResultPanel() {
2814
+ var r = state.quickCommitResult;
2815
+ if (!r) return "";
2816
+ var oldCommit = r.oldCommitHash
2817
+ ? '<code>' + escapeHtml(r.oldCommitHash) + '</code>' + (r.oldCommitSubject ? '<span>' + escapeHtml(r.oldCommitSubject) + '</span>' : '')
2818
+ : '<span class="qc-muted">无</span>';
2819
+ var newCommit = r.commitHash
2820
+ ? '<code>' + escapeHtml(r.commitHash) + '</code><span>' + escapeHtml(r.commitMessage || "") + '</span>'
2821
+ : '<span class="qc-muted">无</span>';
2822
+ var oldTag = r.oldTag ? '<code>' + escapeHtml(r.oldTag) + '</code>' : '<span class="qc-muted">无 tag</span>';
2823
+ var newTag = r.tagName ? '<code>' + escapeHtml(r.tagName) + '</code>' : '<span class="qc-muted">未打 tag</span>';
2824
+ var pushButton = r.pushed
2825
+ ? '<span class="qc-result-pushed">已推送</span>'
2826
+ : '<button id="quick-commit-push-after-btn" class="btn btn-primary btn-sm" type="button"' + (state.quickCommitPushing ? ' disabled' : '') + '>' + (state.quickCommitPushing ? '推送中...' : 'Push & Close') + '</button>';
2827
+ return '<section class="qc-result-panel">' +
2828
+ renderQuickCommitPair("Commit", oldCommit, newCommit, "") +
2829
+ renderQuickCommitPair("Tag", oldTag, newTag, "qc-pair--tag") +
2830
+ (r.pushError || state.quickCommitPushError ? '<p class="error-message">' + escapeHtml(r.pushError || state.quickCommitPushError) + '</p>' : '') +
2831
+ '<div class="qc-result-actions">' +
2832
+ '<button id="quick-commit-cancel-btn" class="btn btn-ghost btn-sm" type="button">关闭</button>' +
2833
+ pushButton +
2834
+ '</div>' +
2835
+ '</section>';
2914
2836
  }
2915
2837
 
2916
2838
  function renderQuickCommitModal() {
2917
2839
  var s = state.gitStatus || {};
2918
- var f = state.quickCommitForm || { customMessage: "", tag: "", tagEdited: false, commitMode: "commit-tag" };
2840
+ var f = state.quickCommitForm || { customMessage: "", tag: "", tagEdited: false };
2919
2841
  var hasChanges = (s.modifiedCount || 0) > 0;
2920
- var files = Array.isArray(s.files) ? s.files : [];
2921
- var fileRows = renderQuickCommitFileRows(files);
2922
-
2923
- // Subtitle: branch · N 改动 · ↑X ↓Y (clean repos show a small ✓ badge instead of "0 个改动")
2924
- var subParts = [];
2925
- subParts.push(s.branch || "(no branch)");
2926
- if (hasChanges) subParts.push((s.modifiedCount || 0) + " 个改动");
2927
- if (typeof s.ahead === "number" && s.ahead > 0) subParts.push("" + s.ahead);
2928
- if (typeof s.behind === "number" && s.behind > 0) subParts.push("↓" + s.behind);
2929
-
2930
- // Section 1: changes + commit form (only when there are changes)
2931
- var section1 = "";
2932
- if (hasChanges) {
2933
- var genBusy = state.quickCommitGenerating;
2934
- var withTag = f.commitMode === "commit-tag";
2935
- section1 = '<section class="qc-section qc-section--changes">' +
2936
- '<div class="qc-section-head"><span class="qc-section-title">更改</span><span class="qc-section-meta">' + escapeHtml(s.modifiedCount + " 个文件") + '</span></div>' +
2937
- '<div class="qc-files-wrap">' + fileRows + '</div>' +
2938
- '<div class="qc-message-row" id="quick-commit-message-row">' +
2939
- '<div class="qc-message-header">' +
2940
- '<label class="field-label qc-message-label" for="quick-commit-message">提交信息</label>' +
2941
- '<button type="button" id="quick-commit-ai-btn" class="btn btn-ghost btn-sm qc-ai-btn"' + (genBusy ? ' disabled' : '') + ' title="读取改动,用 AI 生成提交信息与版本 Tag">' +
2942
- '<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M8 1.5l1.4 3.6L13 6.5 9.4 7.9 8 11.5 6.6 7.9 3 6.5l3.6-1.4L8 1.5zM12.5 10.5l.7 1.8 1.8.7-1.8.7-.7 1.8-.7-1.8-1.8-.7 1.8-.7.7-1.8z" fill="currentColor"/></svg>' +
2943
- '<span>' + (genBusy ? '生成中…' : 'AI 生成') + '</span>' +
2944
- '</button>' +
2945
- '</div>' +
2946
- '<textarea id="quick-commit-message" class="field-input qc-message-input" rows="3" placeholder="描述这次改动;或点「AI 生成」自动填写" ' + (state.quickCommitSubmitting ? 'disabled' : '') + '>' + escapeHtml(f.customMessage || "") + '</textarea>' +
2947
- '</div>' +
2948
- '<div class="qc-tag-field' + (withTag ? '' : ' is-off') + '">' +
2949
- '<span class="qc-tag-field-label" title="' + (withTag ? '这次提交会打上这个版本 Tag' : '当前为「仅提交」,不会打 Tag') + '">Tag</span>' +
2950
- '<input type="text" id="quick-commit-tag" class="field-input qc-tag-field-input" placeholder="版本号,如 v1.2.0" value="' + escapeHtml(f.tag || "") + '"' + ((!withTag || state.quickCommitSubmitting) ? ' disabled' : '') + '>' +
2951
- (withTag ? '' : '<span class="qc-tag-field-note">仅提交</span>') +
2952
- '</div>' +
2953
- (state.quickCommitError ? '<p class="error-message">' + escapeHtml(state.quickCommitError) + '</p>' : '') +
2954
- '<div class="qc-section-actions">' +
2955
- '<button id="quick-commit-cancel-btn" class="btn btn-ghost btn-sm" type="button">取消</button>' +
2956
- '<div class="qc-action-group">' +
2957
- renderQuickCommitCommitButton(hasChanges) +
2958
- renderQuickCommitPushButton(s) +
2959
- '</div>' +
2960
- '</div>' +
2961
- '</section>';
2962
- }
2963
- // When clean, we skip the big "changes" card entirely — a small green
2964
- // indicator in the header subtitle is enough of a signal (see below).
2965
-
2966
- // Section 2: repo status + secondary actions (always show when there's at least one commit)
2967
- var section2 = "";
2968
- if (!s.initialCommit && s.isGit !== false) {
2969
- var lc = s.lastCommit || {};
2970
- var headLine = lc.shortHash ? lc.shortHash + " · " + (lc.subject || "") : (s.head ? s.head.substring(0, 7) : "(no commit)");
2971
- var upstreamLine = s.upstream ? escapeHtml(s.branch || "") + " → " + escapeHtml(s.upstream) : escapeHtml(s.branch || "(no branch)") + " · 无 upstream";
2972
- var tagHeadOpen = state.quickCommitOpenMenu === "tag-head";
2973
- section2 = '<section class="qc-section qc-section--repo">' +
2974
- '<div class="qc-section-head"><span class="qc-section-title">仓库 · 同步</span><span class="qc-section-meta">' + upstreamLine + '</span></div>' +
2975
- '<div class="qc-head-card">' +
2976
- '<span class="qc-head-label">HEAD</span>' +
2977
- '<code class="qc-head-text">' + escapeHtml(headLine) + '</code>' +
2978
- '</div>' +
2979
- renderQuickCommitStatusChips(s) +
2980
- (tagHeadOpen ? renderQuickCommitTagHeadPanel() : '') +
2981
- '<div class="qc-section-actions qc-section-actions--secondary">' +
2982
- '<button id="quick-commit-tag-head-toggle" class="btn btn-secondary btn-sm qc-tag-head-btn' + (tagHeadOpen ? ' is-open' : '') + '" type="button"' + (state.quickCommitPushing ? ' disabled' : '') + ' title="给当前最新提交(HEAD)打 Tag,不会创建新提交">' +
2983
- '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><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>' +
2984
- '<span>' + (tagHeadOpen ? '收起' : '为当前提交打 Tag') + '</span>' +
2985
- '</button>' +
2986
- // Push lives in the commit footer when there are changes; show it here otherwise.
2987
- (hasChanges ? '' : renderQuickCommitPushButton(s)) +
2988
- '</div>' +
2989
- '</section>';
2990
- }
2991
-
2992
- var subtitleHtml = subParts.map(escapeHtml).join(" · ");
2993
- // Small "clean" badge shown inline in the header subtitle (replaces the old empty-state card).
2994
- var cleanBadge = (!hasChanges && s.isGit !== false)
2995
- ? '<span class="qc-clean-badge" title="工作区干净,没有待提交的改动"><svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 4.5l-6 6L3 7"/></svg>干净</span>'
2996
- : '';
2997
-
2842
+ var genBusy = state.quickCommitGenerating;
2843
+ var lc = s.lastCommit || {};
2844
+ var oldCommitHtml = lc.shortHash
2845
+ ? '<code>' + escapeHtml(lc.shortHash) + '</code><span>' + escapeHtml(lc.subject || "") + '</span>'
2846
+ : (s.head ? '<code>' + escapeHtml(s.head.substring(0, 7)) + '</code>' : '<span class="qc-muted">无 commit</span>');
2847
+ var oldTagHtml = s.latestTag ? '<code>' + escapeHtml(s.latestTag) + '</code>' : '<span class="qc-muted">无 tag</span>';
2848
+ var newTagHtml = '<input type="text" id="quick-commit-tag" class="field-input qc-tag-field-input" placeholder="v1.2.0" value="' + escapeHtml(f.tag || "") + '"' + (state.quickCommitSubmitting ? ' disabled' : '') + '>';
2849
+ var nextCommitHtml = '<textarea id="quick-commit-message" class="field-input qc-message-input" rows="3" placeholder="New commit message" ' + (state.quickCommitSubmitting ? 'disabled' : '') + '>' + escapeHtml(f.customMessage || "") + '</textarea>';
2850
+ var subtitleParts = [];
2851
+ subtitleParts.push(s.branch || "(no branch)");
2852
+ subtitleParts.push(hasChanges ? ((s.modifiedCount || 0) + " 个改动") : "工作区干净");
2853
+ if (typeof s.ahead === "number" && s.ahead > 0) subtitleParts.push("↑" + s.ahead);
2854
+ if (typeof s.behind === "number" && s.behind > 0) subtitleParts.push("↓" + s.behind);
2855
+ var formPanel = state.quickCommitResult ? "" : '<section class="qc-release-panel">' +
2856
+ '<div class="qc-message-header">' +
2857
+ '<span class="qc-section-title">New</span>' +
2858
+ '<button type="button" id="quick-commit-ai-btn" class="btn btn-ghost btn-sm qc-ai-btn"' + (genBusy ? ' disabled' : '') + ' title="AI 生成 commit message 与 tag">' +
2859
+ '<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M8 1.5l1.4 3.6L13 6.5 9.4 7.9 8 11.5 6.6 7.9 3 6.5l3.6-1.4L8 1.5zM12.5 10.5l.7 1.8 1.8.7-1.8.7-.7 1.8-.7-1.8-1.8-.7 1.8-.7.7-1.8z" fill="currentColor"/></svg>' +
2860
+ '<span>' + (genBusy ? '生成中...' : 'AI') + '</span>' +
2861
+ '</button>' +
2862
+ '</div>' +
2863
+ renderQuickCommitPair("Commit", oldCommitHtml, nextCommitHtml, "qc-pair--commit") +
2864
+ renderQuickCommitPair("Tag", oldTagHtml, newTagHtml, "qc-pair--tag") +
2865
+ (state.quickCommitError ? '<p class="error-message">' + escapeHtml(state.quickCommitError) + '</p>' : '') +
2866
+ renderQuickCommitDragControl(hasChanges) +
2867
+ '<div class="qc-modal-actions"><button id="quick-commit-cancel-btn" class="btn btn-ghost btn-sm" type="button">取消</button></div>' +
2868
+ '</section>';
2869
+ var resultPanel = renderQuickCommitResultPanel();
2998
2870
  return '<section id="quick-commit-modal" class="modal-backdrop' + (state.quickCommitOpen ? '' : ' hidden') + '">' +
2999
2871
  '<div class="modal quick-commit-modal" role="dialog" aria-labelledby="quick-commit-title">' +
3000
2872
  '<div class="modal-header">' +
3001
2873
  '<div>' +
3002
2874
  '<h2 id="quick-commit-title" class="modal-title">快捷提交</h2>' +
3003
- '<p class="modal-subtitle">' + subtitleHtml + cleanBadge + '</p>' +
2875
+ '<p class="modal-subtitle">' + escapeHtml(subtitleParts.join(" · ")) + '</p>' +
3004
2876
  '</div>' +
3005
2877
  '<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>' +
3006
2878
  '</div>' +
3007
2879
  '<div class="modal-body">' +
3008
- section1 +
3009
- section2 +
2880
+ formPanel +
2881
+ resultPanel +
3010
2882
  '</div>' +
3011
2883
  '</div>' +
3012
2884
  '</section>';
@@ -8444,9 +8316,16 @@
8444
8316
  rows: 36,
8445
8317
  autoResize: true,
8446
8318
  cursorBlink: false,
8447
- onData: function(data) {
8448
- if (state.terminalInteractive) return;
8449
- queueDirectInput(data);
8319
+ onData: function() {
8320
+ // 物理键盘进 PTY 只允许在「终端交互(键盘透传)」开启时发生,而开启态那条
8321
+ // 路径由 captureTerminalInput(document keydown capture)独占处理——所以
8322
+ // wterm 自身的 onData 一律不再直接发:
8323
+ // · 关闭态(默认):用户点一下终端会触发 wterm 内部 _onClickFocus,让它的
8324
+ // 隐藏输入元素拿到焦点;之后敲的每个键都从 onData 冒出来。旧代码在这里
8325
+ // 直接 queueDirectInput,于是"没开透传也漏键进 PTY"(反复误触的根因)。
8326
+ // · 开启态:captureTerminalInput 已接管全部按键,onData 再发就是双份重复。
8327
+ // 两种状态都让路。要发命令请先开透传开关,或直接用输入框。
8328
+ return;
8450
8329
  },
8451
8330
  onResize: function(cols, rows) {
8452
8331
  sendTerminalResize(cols, rows);