@co0ontty/wand 1.42.0 → 1.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,41 +2241,99 @@
2249
2241
  }
2250
2242
 
2251
2243
  var quickCommitEscHandler = null;
2252
- var quickCommitDocClickHandler = null;
2244
+ var quickCommitDragCleanup = null;
2245
+ var quickCommitDragState = null;
2246
+
2247
+ function normalizeQuickCommitAction(value) {
2248
+ if (
2249
+ value === "commit-tag" ||
2250
+ value === "commit-tag-push" ||
2251
+ value === "commit-push"
2252
+ ) return value;
2253
+ return "commit";
2254
+ }
2255
+
2256
+ function getQuickCommitActionMeta(action) {
2257
+ action = normalizeQuickCommitAction(action);
2258
+ if (action === "commit-tag-push") {
2259
+ return {
2260
+ action: action,
2261
+ label: "Commit + Tag + Push",
2262
+ verb: "提交、打 Tag 并推送",
2263
+ withTag: true,
2264
+ push: true,
2265
+ tone: "all",
2266
+ };
2267
+ }
2268
+ if (action === "commit-tag") {
2269
+ return {
2270
+ action: action,
2271
+ label: "Commit + Tag",
2272
+ verb: "提交并打 Tag",
2273
+ withTag: true,
2274
+ push: false,
2275
+ tone: "tag",
2276
+ };
2277
+ }
2278
+ if (action === "commit-push") {
2279
+ return {
2280
+ action: action,
2281
+ label: "Commit + Push",
2282
+ verb: "提交并推送",
2283
+ withTag: false,
2284
+ push: true,
2285
+ tone: "push",
2286
+ };
2287
+ }
2288
+ return {
2289
+ action: "commit",
2290
+ label: "Commit",
2291
+ verb: "仅提交",
2292
+ withTag: false,
2293
+ push: false,
2294
+ tone: "commit",
2295
+ };
2296
+ }
2253
2297
 
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
2298
+ // Knob starts at LEFT (Commit). Three zones, left right:
2299
+ // commit (0.00..0.32) starting position
2300
+ // tag (0.32..0.68) — dwell here ~600ms to "arm" the Tag latch
2301
+ // push (0.68..1.00) — release here to push
2302
+ function getQuickCommitZoneFromRatio(ratio) {
2303
+ if (ratio <= 0.32) return "commit";
2304
+ if (ratio >= 0.68) return "push";
2305
+ return "tag";
2262
2306
  }
2263
- function saveCommitMode(value) {
2264
- try { localStorage.setItem("wand.quickCommit.commitMode", value); } catch (e) { /* no-op */ }
2307
+
2308
+ // tagArmed is only true if the user dwelled long enough in the Tag zone.
2309
+ // Sliding straight through Tag → Push without dwelling = commit-push (skip Tag).
2310
+ function composeQuickCommitAction(zone, tagArmed) {
2311
+ if (zone === "push") return tagArmed ? "commit-tag-push" : "commit-push";
2312
+ if (zone === "tag") return "commit-tag";
2313
+ return "commit";
2265
2314
  }
2266
2315
 
2316
+ // How long the user must hold the knob inside the Tag zone before the Tag latch arms.
2317
+ // Keep in sync with `--qc-dwell-ms` in styles.css.
2318
+ var QUICK_COMMIT_TAG_DWELL_MS = 1100;
2319
+ var QUICK_COMMIT_ZONE_RANK = { commit: 0, tag: 1, push: 2 };
2320
+
2267
2321
  function openQuickCommitModal() {
2268
2322
  if (!state.selectedId) return;
2269
2323
  state.quickCommitOpen = true;
2270
2324
  state.quickCommitSubmitting = false;
2325
+ state.quickCommitAutoGenerating = false;
2271
2326
  state.quickCommitError = "";
2272
2327
  state.quickCommitForm = {
2273
2328
  customMessage: "",
2274
2329
  tag: "",
2275
2330
  // Whether the user has manually edited the tag (so we stop auto-overwriting it).
2276
2331
  tagEdited: false,
2277
- // "commit-tag" → commit + version tag; "commit" → commit only.
2278
- commitMode: readSavedCommitMode(),
2279
2332
  };
2280
- state.quickCommitOpenMenu = null;
2281
- state.quickCommitTagHeadForm = { tag: "", push: false };
2282
- state.quickCommitTagHeadSubmitting = false;
2283
- state.quickCommitTagHeadGenerating = false;
2284
- state.quickCommitTagHeadError = "";
2285
2333
  state.quickCommitPushing = false;
2286
2334
  state.quickCommitPushError = "";
2335
+ state.quickCommitResult = null;
2336
+ state.quickCommitDragAction = "commit";
2287
2337
  closeWorktreeMergeModal();
2288
2338
  closeSessionModal();
2289
2339
  closeSettingsModal();
@@ -2296,33 +2346,11 @@
2296
2346
  }
2297
2347
  if (quickCommitEscHandler) document.removeEventListener("keydown", quickCommitEscHandler);
2298
2348
  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
- }
2349
+ if (e.key === "Escape" && state.quickCommitOpen && !state.quickCommitSubmitting && !state.quickCommitPushing) {
2306
2350
  closeQuickCommitModal();
2307
2351
  }
2308
2352
  };
2309
2353
  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
2354
  loadGitStatus(state.selectedId, { force: true }).then(function() {
2327
2355
  if (!state.quickCommitOpen) return;
2328
2356
  // Seed the tag field with the locally-derived suggestion so a tag is
@@ -2339,7 +2367,8 @@
2339
2367
  state.quickCommitOpen = false;
2340
2368
  state.quickCommitSubmitting = false;
2341
2369
  state.quickCommitError = "";
2342
- state.quickCommitOpenMenu = null;
2370
+ state.quickCommitResult = null;
2371
+ state.quickCommitDragAction = "commit";
2343
2372
  var modal = document.getElementById("quick-commit-modal");
2344
2373
  if (modal) modal.classList.add("hidden");
2345
2374
  if (focusTrapHandler) {
@@ -2350,9 +2379,9 @@
2350
2379
  document.removeEventListener("keydown", quickCommitEscHandler);
2351
2380
  quickCommitEscHandler = null;
2352
2381
  }
2353
- if (quickCommitDocClickHandler) {
2354
- document.removeEventListener("click", quickCommitDocClickHandler, true);
2355
- quickCommitDocClickHandler = null;
2382
+ if (quickCommitDragCleanup) {
2383
+ quickCommitDragCleanup();
2384
+ quickCommitDragCleanup = null;
2356
2385
  }
2357
2386
  if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
2358
2387
  lastFocusedElement.focus();
@@ -2362,6 +2391,10 @@
2362
2391
  function rerenderQuickCommitModal() {
2363
2392
  var modal = document.getElementById("quick-commit-modal");
2364
2393
  if (!modal) return;
2394
+ if (quickCommitDragCleanup) {
2395
+ quickCommitDragCleanup();
2396
+ quickCommitDragCleanup = null;
2397
+ }
2365
2398
  var html = renderQuickCommitModal();
2366
2399
  var temp = document.createElement("div");
2367
2400
  temp.innerHTML = html;
@@ -2377,8 +2410,6 @@
2377
2410
  var cancelBtn = document.getElementById("quick-commit-cancel-btn");
2378
2411
  if (cancelBtn) cancelBtn.addEventListener("click", closeQuickCommitModal);
2379
2412
 
2380
- var submitBtn = document.getElementById("quick-commit-submit-btn");
2381
- if (submitBtn) submitBtn.addEventListener("click", submitQuickCommit);
2382
2413
  var aiBtn = document.getElementById("quick-commit-ai-btn");
2383
2414
  if (aiBtn) aiBtn.addEventListener("click", generateCommitMessageAI);
2384
2415
  var msgEl = document.getElementById("quick-commit-message");
@@ -2390,7 +2421,7 @@
2390
2421
  msgEl.addEventListener("keydown", function(e) {
2391
2422
  if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
2392
2423
  e.preventDefault();
2393
- submitQuickCommit();
2424
+ submitQuickCommit("commit");
2394
2425
  }
2395
2426
  });
2396
2427
  }
@@ -2403,114 +2434,251 @@
2403
2434
  tagInput.addEventListener("keydown", function(e) {
2404
2435
  if (e.key === "Enter") {
2405
2436
  e.preventDefault();
2406
- submitQuickCommit();
2437
+ submitQuickCommit("commit-tag");
2407
2438
  }
2408
2439
  });
2409
2440
  }
2410
2441
 
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();
2442
+ var pushAfterBtn = document.getElementById("quick-commit-push-after-btn");
2443
+ if (pushAfterBtn) pushAfterBtn.addEventListener("click", function() {
2444
+ var result = state.quickCommitResult || {};
2445
+ submitPushOnly({ pushCommits: true, pushTags: !!result.tagName, closeOnSuccess: true });
2416
2446
  });
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
2447
 
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
- }
2448
+ attachQuickCommitDrag();
2449
+ }
2450
+
2451
+ // ratio: knob CENTER position as fraction of track [0..1]. 0 = left (Commit start), 1 = right (Push).
2452
+ function updateQuickCommitDragVisual(action, ratio, opts) {
2453
+ action = normalizeQuickCommitAction(action);
2454
+ state.quickCommitDragAction = action;
2455
+ var track = document.getElementById("quick-commit-drag-track");
2456
+ if (!track) return;
2457
+ var meta = getQuickCommitActionMeta(action);
2458
+ // Resting position per action when user isn't dragging.
2459
+ var defaultRatio = 0;
2460
+ if (action === "commit-tag") defaultRatio = 0.5;
2461
+ else if (action === "commit-push" || action === "commit-tag-push") defaultRatio = 1;
2462
+ var progress = typeof ratio === "number"
2463
+ ? Math.max(0, Math.min(1, ratio))
2464
+ : defaultRatio;
2465
+ var zone = getQuickCommitZoneFromRatio(progress);
2466
+ track.setAttribute("data-action", action);
2467
+ track.setAttribute("data-zone", zone);
2468
+ track.setAttribute("data-tag-armed", (action === "commit-tag" || action === "commit-tag-push") ? "1" : "0");
2469
+ track.style.setProperty("--qc-progress", (progress * 100).toFixed(1) + "%");
2470
+ var knob = document.getElementById("quick-commit-drag-action");
2471
+ if (knob) {
2472
+ var trackWidth = track.clientWidth;
2473
+ var knobWidth = knob.offsetWidth;
2474
+ var pad = 6;
2475
+ // Knob CENTER tracks ratio * trackWidth, clamped so the knob stays fully inside.
2476
+ var center = progress * trackWidth;
2477
+ var minLeft = pad;
2478
+ var maxLeft = Math.max(pad, trackWidth - knobWidth - pad);
2479
+ var left = Math.max(minLeft, Math.min(maxLeft, center - knobWidth / 2));
2480
+ track.style.setProperty("--qc-knob-x", left.toFixed(1) + "px");
2481
+ }
2482
+ var label = document.getElementById("quick-commit-drag-label");
2483
+ if (label) {
2484
+ var dwell = track.getAttribute("data-tag-dwell") || "idle";
2485
+ var cancelMode = track.getAttribute("data-cancel-mode") === "1";
2486
+ var txt;
2487
+ if (state.quickCommitSubmitting) {
2488
+ txt = state.quickCommitAutoGenerating ? "AI 生成 + 提交中…" : "执行中…";
2489
+ } else if (cancelMode) {
2490
+ txt = "松手取消 ✕";
2491
+ } else if (dwell === "active") {
2492
+ txt = "锁定 Tag 中…";
2493
+ } else if (dwell === "armed" && zone === "tag") {
2494
+ txt = "Tag 已就绪 · 继续推送 →";
2495
+ } else {
2496
+ txt = meta.label;
2453
2497
  }
2454
- rerenderQuickCommitModal();
2455
- if (willOpen) {
2456
- var inp = document.getElementById("quick-commit-tag-head-input");
2457
- if (inp) inp.focus();
2498
+ label.textContent = txt;
2499
+ }
2500
+ // Stage class refresh.
2501
+ var stages = track.querySelectorAll("[data-qc-stage]");
2502
+ for (var i = 0; i < stages.length; i++) {
2503
+ var st = stages[i].getAttribute("data-qc-stage");
2504
+ var active = false, passed = false;
2505
+ if (st === "commit") {
2506
+ active = action === "commit";
2507
+ passed = true;
2508
+ } else if (st === "tag") {
2509
+ active = action === "commit-tag";
2510
+ passed = action === "commit-tag" || action === "commit-tag-push";
2511
+ } else if (st === "push") {
2512
+ active = action === "commit-push" || action === "commit-tag-push";
2513
+ passed = active;
2458
2514
  }
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
- });
2515
+ stages[i].classList.toggle("is-active", active);
2516
+ stages[i].classList.toggle("is-passed", passed);
2477
2517
  }
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
- });
2518
+ }
2488
2519
 
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]);
2520
+ function attachQuickCommitDrag() {
2521
+ var track = document.getElementById("quick-commit-drag-track");
2522
+ var knob = document.getElementById("quick-commit-drag-action");
2523
+ if (!track || !knob) return;
2524
+ updateQuickCommitDragVisual(state.quickCommitDragAction || "commit");
2525
+
2526
+ function setDwell(state) { track.setAttribute("data-tag-dwell", state); }
2527
+ function clearArmTimer() {
2528
+ if (quickCommitDragState && quickCommitDragState.tagArmTimer) {
2529
+ clearTimeout(quickCommitDragState.tagArmTimer);
2530
+ quickCommitDragState.tagArmTimer = null;
2512
2531
  }
2513
2532
  }
2533
+
2534
+ function setCancelMode(on) { track.setAttribute("data-cancel-mode", on ? "1" : "0"); }
2535
+
2536
+ var onPointerDown = function(e) {
2537
+ if (knob.disabled || isQuickCommitOpInFlight()) return;
2538
+ var rect = track.getBoundingClientRect();
2539
+ quickCommitDragState = {
2540
+ pointerId: e.pointerId,
2541
+ rect: rect,
2542
+ startX: e.clientX,
2543
+ moved: false,
2544
+ zone: "commit",
2545
+ maxZone: "commit", // furthest zone the knob ever reached this drag
2546
+ tagArmed: false,
2547
+ tagArmTimer: null,
2548
+ action: "commit",
2549
+ };
2550
+ try { knob.setPointerCapture(e.pointerId); } catch (err) { /* ignored */ }
2551
+ track.classList.add("is-dragging");
2552
+ setDwell("idle");
2553
+ setCancelMode(false);
2554
+ updateQuickCommitDragVisual("commit", 0);
2555
+ e.preventDefault();
2556
+ };
2557
+ var onPointerMove = function(e) {
2558
+ if (!quickCommitDragState || quickCommitDragState.pointerId !== e.pointerId) return;
2559
+ var rect = quickCommitDragState.rect;
2560
+ var ratio = rect.width > 0 ? (e.clientX - rect.left) / rect.width : 0;
2561
+ ratio = Math.max(0, Math.min(1, ratio));
2562
+ if (Math.abs(e.clientX - quickCommitDragState.startX) > 5) quickCommitDragState.moved = true;
2563
+ var zone = getQuickCommitZoneFromRatio(ratio);
2564
+ // Track furthest zone reached so we can detect a backtrack-to-commit.
2565
+ if (QUICK_COMMIT_ZONE_RANK[zone] > QUICK_COMMIT_ZONE_RANK[quickCommitDragState.maxZone]) {
2566
+ quickCommitDragState.maxZone = zone;
2567
+ }
2568
+ // Zone transitions drive the dwell state machine.
2569
+ if (zone !== quickCommitDragState.zone) {
2570
+ if (zone === "tag" && !quickCommitDragState.tagArmed) {
2571
+ // Entered Tag zone unarmed → kick off the dwell timer + CSS fill animation.
2572
+ clearArmTimer();
2573
+ setDwell("active");
2574
+ quickCommitDragState.tagArmTimer = setTimeout(function() {
2575
+ if (!quickCommitDragState) return;
2576
+ quickCommitDragState.tagArmed = true;
2577
+ quickCommitDragState.tagArmTimer = null;
2578
+ setDwell("armed");
2579
+ var newAction = composeQuickCommitAction(quickCommitDragState.zone, true);
2580
+ quickCommitDragState.action = newAction;
2581
+ updateQuickCommitDragVisual(newAction);
2582
+ }, QUICK_COMMIT_TAG_DWELL_MS);
2583
+ } else if (zone !== "tag" && !quickCommitDragState.tagArmed) {
2584
+ // Left Tag zone before arming → cancel the dwell.
2585
+ clearArmTimer();
2586
+ setDwell("idle");
2587
+ } else if (zone === "tag" && quickCommitDragState.tagArmed) {
2588
+ setDwell("armed");
2589
+ }
2590
+ quickCommitDragState.zone = zone;
2591
+ }
2592
+ // Cancel-mode: backed back to commit after venturing further.
2593
+ // Visual cue is set here; the actual "no-submit" decision is made in finish().
2594
+ var inCancelMode = quickCommitDragState.maxZone !== "commit" && zone === "commit";
2595
+ setCancelMode(inCancelMode);
2596
+ var action = composeQuickCommitAction(zone, quickCommitDragState.tagArmed);
2597
+ quickCommitDragState.action = action;
2598
+ updateQuickCommitDragVisual(action, ratio);
2599
+ };
2600
+ var finish = function(e, cancelled) {
2601
+ if (!quickCommitDragState) return;
2602
+ var current = quickCommitDragState;
2603
+ clearArmTimer();
2604
+ quickCommitDragState = null;
2605
+ track.classList.remove("is-dragging");
2606
+ setDwell("idle");
2607
+ setCancelMode(false);
2608
+ try {
2609
+ if (typeof knob.releasePointerCapture === "function") knob.releasePointerCapture(current.pointerId);
2610
+ } catch (err) { /* ignored */ }
2611
+ // Backtrack: user ventured out past Commit, then dragged back and released at Commit → cancel.
2612
+ var isBacktrack = current.moved
2613
+ && current.zone === "commit"
2614
+ && current.maxZone !== "commit";
2615
+ if (cancelled || isBacktrack) {
2616
+ updateQuickCommitDragVisual("commit");
2617
+ return;
2618
+ }
2619
+ // Tap (no drag) → just commit. Auto-message will fire if the message is empty.
2620
+ var action = current.moved
2621
+ ? composeQuickCommitAction(current.zone, current.tagArmed)
2622
+ : "commit";
2623
+ updateQuickCommitDragVisual(action);
2624
+ submitQuickCommit(action);
2625
+ if (e && typeof e.preventDefault === "function") e.preventDefault();
2626
+ };
2627
+ var onPointerUp = function(e) {
2628
+ if (!quickCommitDragState || quickCommitDragState.pointerId !== e.pointerId) return;
2629
+ finish(e, false);
2630
+ };
2631
+ var onPointerCancel = function(e) {
2632
+ if (!quickCommitDragState || quickCommitDragState.pointerId !== e.pointerId) return;
2633
+ finish(e, true);
2634
+ };
2635
+ // Keyboard parity:
2636
+ // → step zone right (commit → commit-tag → commit-tag-push). The first → into Tag arms it instantly.
2637
+ // ← step zone left
2638
+ // Home → reset to commit
2639
+ // Enter / Space → submit current action
2640
+ var onKeyDown = function(e) {
2641
+ if (knob.disabled || isQuickCommitOpInFlight()) return;
2642
+ var cur = state.quickCommitDragAction || "commit";
2643
+ if (e.key === "ArrowRight") {
2644
+ e.preventDefault();
2645
+ var next = cur;
2646
+ if (cur === "commit") next = "commit-tag"; // arm Tag
2647
+ else if (cur === "commit-tag") next = "commit-tag-push";
2648
+ else if (cur === "commit-push") next = "commit-push";
2649
+ updateQuickCommitDragVisual(next);
2650
+ } else if (e.key === "ArrowLeft") {
2651
+ e.preventDefault();
2652
+ var prev = cur;
2653
+ if (cur === "commit-tag-push") prev = "commit-tag";
2654
+ else if (cur === "commit-tag") prev = "commit";
2655
+ else if (cur === "commit-push") prev = "commit";
2656
+ updateQuickCommitDragVisual(prev);
2657
+ } else if (e.key === "Home") {
2658
+ e.preventDefault();
2659
+ updateQuickCommitDragVisual("commit");
2660
+ } else if (e.key === "Enter" || e.key === " ") {
2661
+ e.preventDefault();
2662
+ submitQuickCommit(state.quickCommitDragAction || "commit");
2663
+ }
2664
+ };
2665
+
2666
+ knob.addEventListener("pointerdown", onPointerDown);
2667
+ knob.addEventListener("pointermove", onPointerMove);
2668
+ knob.addEventListener("pointerup", onPointerUp);
2669
+ knob.addEventListener("pointercancel", onPointerCancel);
2670
+ knob.addEventListener("keydown", onKeyDown);
2671
+ quickCommitDragCleanup = function() {
2672
+ knob.removeEventListener("pointerdown", onPointerDown);
2673
+ knob.removeEventListener("pointermove", onPointerMove);
2674
+ knob.removeEventListener("pointerup", onPointerUp);
2675
+ knob.removeEventListener("pointercancel", onPointerCancel);
2676
+ knob.removeEventListener("keydown", onKeyDown);
2677
+ if (quickCommitDragState && quickCommitDragState.tagArmTimer) {
2678
+ clearTimeout(quickCommitDragState.tagArmTimer);
2679
+ }
2680
+ quickCommitDragState = null;
2681
+ };
2514
2682
  }
2515
2683
 
2516
2684
  function generateCommitMessageAI() {
@@ -2548,8 +2716,7 @@
2548
2716
  // recommendation is actually applied on commit.
2549
2717
  if (aiTag) {
2550
2718
  if (!state.quickCommitForm.tagEdited) state.quickCommitForm.tag = aiTag;
2551
- state.quickCommitForm.commitMode = "commit-tag";
2552
- saveCommitMode("commit-tag");
2719
+ state.quickCommitDragAction = "commit-tag";
2553
2720
  }
2554
2721
  })
2555
2722
  .catch(function(error) {
@@ -2561,32 +2728,43 @@
2561
2728
  });
2562
2729
  }
2563
2730
 
2564
- function submitQuickCommit() {
2731
+ function submitQuickCommit(action) {
2565
2732
  if (!state.selectedId || state.quickCommitSubmitting) return;
2566
2733
  var msgEl = document.getElementById("quick-commit-message");
2567
2734
  if (msgEl) state.quickCommitForm.customMessage = msgEl.value;
2568
2735
  var tagEl = document.getElementById("quick-commit-tag");
2569
2736
  if (tagEl) state.quickCommitForm.tag = tagEl.value;
2570
2737
  var form = state.quickCommitForm || {};
2571
- var withTag = form.commitMode === "commit-tag";
2738
+ var meta = getQuickCommitActionMeta(action || state.quickCommitDragAction || "commit");
2739
+ var withTag = meta.withTag;
2572
2740
  var userTag = withTag ? (form.tag || "").trim() : "";
2573
2741
  var message = (form.customMessage || "").trim();
2574
- if (!message) {
2575
- state.quickCommitError = "请填写提交信息,或点击「AI 生成」。";
2576
- rerenderQuickCommitModal();
2577
- return;
2578
- }
2579
- // Commit no longer pushes pushing is a separate, standalone action.
2580
- // 选了「提交并打 Tag」但 tag 留空 → 由后端在提交时调 AI 生成。
2742
+ // Auto-generate flow: empty commit message → ask backend to write one (autoMessage:true).
2743
+ // Empty tag (when withTag) → ask backend to derive one (autoTag:true). Both go in one round-trip.
2744
+ var autoMessage = !message;
2745
+ var before = {
2746
+ branch: (state.gitStatus || {}).branch || "",
2747
+ commitHash: (state.gitStatus || {}).lastCommit && (state.gitStatus || {}).lastCommit.shortHash
2748
+ ? (state.gitStatus || {}).lastCommit.shortHash
2749
+ : ((state.gitStatus || {}).head ? (state.gitStatus || {}).head.substring(0, 7) : ""),
2750
+ commitSubject: (state.gitStatus || {}).lastCommit && (state.gitStatus || {}).lastCommit.subject
2751
+ ? (state.gitStatus || {}).lastCommit.subject
2752
+ : "",
2753
+ tag: (state.gitStatus || {}).latestTag || "",
2754
+ };
2581
2755
  var payload = {
2582
- autoMessage: false,
2583
- customMessage: message,
2756
+ autoMessage: autoMessage,
2757
+ customMessage: autoMessage ? "" : message,
2584
2758
  tag: userTag,
2585
2759
  autoTag: !!(withTag && !userTag),
2586
- push: false
2760
+ push: !!meta.push
2587
2761
  };
2588
2762
  state.quickCommitSubmitting = true;
2763
+ state.quickCommitAutoGenerating = autoMessage || payload.autoTag;
2589
2764
  state.quickCommitError = "";
2765
+ state.quickCommitPushError = "";
2766
+ state.quickCommitResult = null;
2767
+ state.quickCommitDragAction = meta.action;
2590
2768
  rerenderQuickCommitModal();
2591
2769
  fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/quick-commit", {
2592
2770
  method: "POST",
@@ -2607,99 +2785,36 @@
2607
2785
  ? "已先提交 " + subCommits.length + " 个 submodule(" + subCommits.map(function(c) { return c.path; }).join("、") + "),"
2608
2786
  : "";
2609
2787
  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");
2788
+ state.quickCommitResult = {
2789
+ action: meta.action,
2790
+ pushed: !!data.pushed,
2791
+ pushError: data.pushError || "",
2792
+ commitHash: hash,
2793
+ commitMessage: data.commit && data.commit.message ? data.commit.message : message,
2794
+ tagName: tagName,
2795
+ oldTag: before.tag,
2796
+ oldCommitHash: before.commitHash,
2797
+ oldCommitSubject: before.commitSubject,
2798
+ submoduleCount: subCommits.length,
2799
+ };
2800
+ if (meta.push && !data.pushError) {
2801
+ if (typeof showToast === "function") showToast(base + ",已推送。", "success");
2802
+ closeQuickCommitModal();
2689
2803
  } else {
2690
- if (typeof showToast === "function") showToast(base + (pushed ? ",已 push" : ""), "success");
2804
+ if (typeof showToast === "function") {
2805
+ showToast(base + (data.pushError ? ";push 失败:" + data.pushError : "。"), data.pushError ? "error" : "success");
2806
+ }
2807
+ if (state.selectedId) loadGitStatus(state.selectedId, { force: true }).then(function() {
2808
+ if (state.quickCommitOpen) rerenderQuickCommitModal();
2809
+ });
2691
2810
  }
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
2811
  })
2698
2812
  .catch(function(error) {
2699
- state.quickCommitTagHeadError = (error && error.message) || "打 tag 失败。";
2813
+ state.quickCommitError = (error && error.message) || "快捷提交失败。";
2700
2814
  })
2701
2815
  .finally(function() {
2702
- state.quickCommitTagHeadSubmitting = false;
2816
+ state.quickCommitSubmitting = false;
2817
+ state.quickCommitAutoGenerating = false;
2703
2818
  if (state.quickCommitOpen) rerenderQuickCommitModal();
2704
2819
  });
2705
2820
  }
@@ -2708,6 +2823,7 @@
2708
2823
  if (!state.selectedId || state.quickCommitPushing) return;
2709
2824
  var pushCommits = !!(opts && opts.pushCommits);
2710
2825
  var pushTags = !!(opts && opts.pushTags);
2826
+ var closeOnSuccess = !!(opts && opts.closeOnSuccess);
2711
2827
  if (!pushCommits && !pushTags) return;
2712
2828
  state.quickCommitPushing = true;
2713
2829
  state.quickCommitPushError = "";
@@ -2733,9 +2849,15 @@
2733
2849
  if (data.pushedTags) parts.push("tags");
2734
2850
  var label = parts.length ? parts.join(" 和 ") : "(无内容)";
2735
2851
  if (typeof showToast === "function") showToast("已推送 " + label, "success");
2736
- if (state.selectedId) loadGitStatus(state.selectedId, { force: true }).then(function() {
2737
- if (state.quickCommitOpen) rerenderQuickCommitModal();
2738
- });
2852
+ if (state.quickCommitResult) state.quickCommitResult.pushed = true;
2853
+ if (closeOnSuccess) {
2854
+ closeQuickCommitModal();
2855
+ if (state.selectedId) loadGitStatus(state.selectedId, { force: true });
2856
+ } else if (state.selectedId) {
2857
+ loadGitStatus(state.selectedId, { force: true }).then(function() {
2858
+ if (state.quickCommitOpen) rerenderQuickCommitModal();
2859
+ });
2860
+ }
2739
2861
  })
2740
2862
  .catch(function(error) {
2741
2863
  state.quickCommitPushError = (error && error.message) || "推送失败。";
@@ -2800,213 +2922,132 @@
2800
2922
  }
2801
2923
 
2802
2924
  function isQuickCommitOpInFlight() {
2803
- return state.quickCommitSubmitting || state.quickCommitTagHeadSubmitting || state.quickCommitPushing;
2925
+ return state.quickCommitSubmitting || state.quickCommitPushing;
2804
2926
  }
2805
2927
 
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>';
2928
+ function renderQuickCommitPair(label, fromHtml, toHtml, extraClass) {
2929
+ return '<div class="qc-pair' + (extraClass ? ' ' + extraClass : '') + '">' +
2930
+ '<div class="qc-pair-label">' + escapeHtml(label) + '</div>' +
2931
+ '<div class="qc-pair-flow">' +
2932
+ '<div class="qc-pair-value qc-pair-value--from">' + fromHtml + '</div>' +
2933
+ '<div class="qc-pair-arrow" aria-hidden="true">→</div>' +
2934
+ '<div class="qc-pair-value qc-pair-value--to">' + toHtml + '</div>' +
2935
+ '</div>' +
2936
+ '</div>';
2855
2937
  }
2856
2938
 
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>' +
2939
+ function renderQuickCommitDragControl(hasChanges) {
2940
+ var action = normalizeQuickCommitAction(state.quickCommitDragAction || "commit");
2941
+ var meta = getQuickCommitActionMeta(action);
2942
+ var disabled = !hasChanges || isQuickCommitOpInFlight();
2943
+ var label = state.quickCommitSubmitting ? "执行中…" : meta.label;
2944
+ // Stage classes derived from the current action.
2945
+ var commitActive = action === "commit";
2946
+ var tagActive = action === "commit-tag";
2947
+ var tagPassed = action === "commit-tag" || action === "commit-tag-push";
2948
+ var pushActive = action === "commit-push" || action === "commit-tag-push";
2949
+ var hint = "向右拖 · 经过 Tag 时停一下打 Tag · 直接划到 Push 跳过 Tag · 留空会自动生成";
2950
+ return '<div class="qc-drag-wrap">' +
2951
+ '<div id="quick-commit-drag-track" class="qc-drag-track" data-action="' + escapeHtml(action) + '" data-zone="commit" data-tag-dwell="idle">' +
2952
+ // Baseline track: subtle grey rail under everything, segments brighten as the knob passes.
2953
+ '<svg class="qc-drag-baseline" viewBox="0 0 100 24" preserveAspectRatio="none" aria-hidden="true">' +
2954
+ '<line class="qc-baseline-rail" x1="4" y1="12" x2="96" y2="12" />' +
2955
+ '<line class="qc-baseline-seg qc-baseline-seg--left" x1="4" y1="12" x2="50" y2="12" />' +
2956
+ '<line class="qc-baseline-seg qc-baseline-seg--right" x1="50" y1="12" x2="96" y2="12" />' +
2957
+ '</svg>' +
2958
+ // Three marching chevrons running right — "drag this way".
2959
+ '<div class="qc-chevrons" aria-hidden="true">' +
2960
+ '<svg class="qc-chevron" viewBox="0 0 12 16" width="9" height="12"><path d="M3 2 L9 8 L3 14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
2961
+ '<svg class="qc-chevron" viewBox="0 0 12 16" width="9" height="12"><path d="M3 2 L9 8 L3 14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
2962
+ '<svg class="qc-chevron" viewBox="0 0 12 16" width="9" height="12"><path d="M3 2 L9 8 L3 14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
2963
+ '</div>' +
2964
+ // Stage dots / labels along the rail.
2965
+ '<div class="qc-drag-stages" aria-hidden="true">' +
2966
+ '<span class="qc-drag-stage qc-stage-commit' + (commitActive ? ' is-active' : '') + ' is-passed" data-qc-stage="commit">Commit</span>' +
2967
+ '<span class="qc-drag-stage qc-stage-tag' + (tagActive ? ' is-active' : '') + (tagPassed ? ' is-passed' : '') + '" data-qc-stage="tag">Tag</span>' +
2968
+ '<span class="qc-drag-stage qc-stage-push' + (pushActive ? ' is-active is-passed' : '') + '" data-qc-stage="push">Push</span>' +
2969
+ '</div>' +
2970
+ // Knob — starts at left, slides right.
2971
+ '<button id="quick-commit-drag-action" class="qc-drag-action" type="button"' + (disabled ? ' disabled' : '') + ' aria-label="' + escapeHtml(meta.verb) + '" title="' + escapeHtml(hint) + '">' +
2972
+ '<span class="qc-drag-grip" aria-hidden="true"><i></i><i></i><i></i></span>' +
2973
+ '<span id="quick-commit-drag-label" class="qc-drag-label">' + escapeHtml(label) + '</span>' +
2974
+ // Bottom progress bar — fills 0→100% over DWELL_MS while sitting in the Tag zone.
2975
+ '<span class="qc-drag-dwell-bar" aria-hidden="true"></span>' +
2976
+ '</button>' +
2876
2977
  '</div>' +
2978
+ '<div class="qc-drag-help" aria-hidden="true">' + escapeHtml(hint) + '</div>' +
2877
2979
  '</div>';
2878
2980
  }
2879
2981
 
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>';
2982
+ function renderQuickCommitResultPanel() {
2983
+ var r = state.quickCommitResult;
2984
+ if (!r) return "";
2985
+ var oldCommit = r.oldCommitHash
2986
+ ? '<code>' + escapeHtml(r.oldCommitHash) + '</code>' + (r.oldCommitSubject ? '<span>' + escapeHtml(r.oldCommitSubject) + '</span>' : '')
2987
+ : '<span class="qc-muted">无</span>';
2988
+ var newCommit = r.commitHash
2989
+ ? '<code>' + escapeHtml(r.commitHash) + '</code><span>' + escapeHtml(r.commitMessage || "") + '</span>'
2990
+ : '<span class="qc-muted">无</span>';
2991
+ var oldTag = r.oldTag ? '<code>' + escapeHtml(r.oldTag) + '</code>' : '<span class="qc-muted">无 tag</span>';
2992
+ var newTag = r.tagName ? '<code>' + escapeHtml(r.tagName) + '</code>' : '<span class="qc-muted">未打 tag</span>';
2993
+ var pushButton = r.pushed
2994
+ ? '<span class="qc-result-pushed">已推送</span>'
2995
+ : '<button id="quick-commit-push-after-btn" class="btn btn-primary btn-sm" type="button"' + (state.quickCommitPushing ? ' disabled' : '') + '>' + (state.quickCommitPushing ? '推送中...' : 'Push & Close') + '</button>';
2996
+ return '<section class="qc-result-panel">' +
2997
+ renderQuickCommitPair("Commit", oldCommit, newCommit, "") +
2998
+ renderQuickCommitPair("Tag", oldTag, newTag, "qc-pair--tag") +
2999
+ (r.pushError || state.quickCommitPushError ? '<p class="error-message">' + escapeHtml(r.pushError || state.quickCommitPushError) + '</p>' : '') +
3000
+ '<div class="qc-result-actions">' +
3001
+ '<button id="quick-commit-cancel-btn" class="btn btn-ghost btn-sm" type="button">关闭</button>' +
3002
+ pushButton +
3003
+ '</div>' +
3004
+ '</section>';
2914
3005
  }
2915
3006
 
2916
3007
  function renderQuickCommitModal() {
2917
3008
  var s = state.gitStatus || {};
2918
- var f = state.quickCommitForm || { customMessage: "", tag: "", tagEdited: false, commitMode: "commit-tag" };
3009
+ var f = state.quickCommitForm || { customMessage: "", tag: "", tagEdited: false };
2919
3010
  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
-
3011
+ var genBusy = state.quickCommitGenerating;
3012
+ var lc = s.lastCommit || {};
3013
+ var oldCommitHtml = lc.shortHash
3014
+ ? '<code>' + escapeHtml(lc.shortHash) + '</code><span>' + escapeHtml(lc.subject || "") + '</span>'
3015
+ : (s.head ? '<code>' + escapeHtml(s.head.substring(0, 7)) + '</code>' : '<span class="qc-muted">无 commit</span>');
3016
+ var oldTagHtml = s.latestTag ? '<code>' + escapeHtml(s.latestTag) + '</code>' : '<span class="qc-muted">无 tag</span>';
3017
+ 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' : '') + '>';
3018
+ 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>';
3019
+ var subtitleParts = [];
3020
+ subtitleParts.push(s.branch || "(no branch)");
3021
+ subtitleParts.push(hasChanges ? ((s.modifiedCount || 0) + " 个改动") : "工作区干净");
3022
+ if (typeof s.ahead === "number" && s.ahead > 0) subtitleParts.push("↑" + s.ahead);
3023
+ if (typeof s.behind === "number" && s.behind > 0) subtitleParts.push("↓" + s.behind);
3024
+ var formPanel = state.quickCommitResult ? "" : '<section class="qc-release-panel">' +
3025
+ '<div class="qc-message-header">' +
3026
+ '<span class="qc-section-title">New</span>' +
3027
+ '<button type="button" id="quick-commit-ai-btn" class="btn btn-ghost btn-sm qc-ai-btn"' + (genBusy ? ' disabled' : '') + ' title="AI 生成 commit message 与 tag">' +
3028
+ '<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>' +
3029
+ '<span>' + (genBusy ? '生成中...' : 'AI') + '</span>' +
3030
+ '</button>' +
3031
+ '</div>' +
3032
+ renderQuickCommitPair("Commit", oldCommitHtml, nextCommitHtml, "qc-pair--commit") +
3033
+ renderQuickCommitPair("Tag", oldTagHtml, newTagHtml, "qc-pair--tag") +
3034
+ (state.quickCommitError ? '<p class="error-message">' + escapeHtml(state.quickCommitError) + '</p>' : '') +
3035
+ renderQuickCommitDragControl(hasChanges) +
3036
+ '<div class="qc-modal-actions"><button id="quick-commit-cancel-btn" class="btn btn-ghost btn-sm" type="button">取消</button></div>' +
3037
+ '</section>';
3038
+ var resultPanel = renderQuickCommitResultPanel();
2998
3039
  return '<section id="quick-commit-modal" class="modal-backdrop' + (state.quickCommitOpen ? '' : ' hidden') + '">' +
2999
3040
  '<div class="modal quick-commit-modal" role="dialog" aria-labelledby="quick-commit-title">' +
3000
3041
  '<div class="modal-header">' +
3001
3042
  '<div>' +
3002
3043
  '<h2 id="quick-commit-title" class="modal-title">快捷提交</h2>' +
3003
- '<p class="modal-subtitle">' + subtitleHtml + cleanBadge + '</p>' +
3044
+ '<p class="modal-subtitle">' + escapeHtml(subtitleParts.join(" · ")) + '</p>' +
3004
3045
  '</div>' +
3005
3046
  '<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
3047
  '</div>' +
3007
3048
  '<div class="modal-body">' +
3008
- section1 +
3009
- section2 +
3049
+ formPanel +
3050
+ resultPanel +
3010
3051
  '</div>' +
3011
3052
  '</div>' +
3012
3053
  '</section>';
@@ -8444,9 +8485,16 @@
8444
8485
  rows: 36,
8445
8486
  autoResize: true,
8446
8487
  cursorBlink: false,
8447
- onData: function(data) {
8448
- if (state.terminalInteractive) return;
8449
- queueDirectInput(data);
8488
+ onData: function() {
8489
+ // 物理键盘进 PTY 只允许在「终端交互(键盘透传)」开启时发生,而开启态那条
8490
+ // 路径由 captureTerminalInput(document keydown capture)独占处理——所以
8491
+ // wterm 自身的 onData 一律不再直接发:
8492
+ // · 关闭态(默认):用户点一下终端会触发 wterm 内部 _onClickFocus,让它的
8493
+ // 隐藏输入元素拿到焦点;之后敲的每个键都从 onData 冒出来。旧代码在这里
8494
+ // 直接 queueDirectInput,于是"没开透传也漏键进 PTY"(反复误触的根因)。
8495
+ // · 开启态:captureTerminalInput 已接管全部按键,onData 再发就是双份重复。
8496
+ // 两种状态都让路。要发命令请先开透传开关,或直接用输入框。
8497
+ return;
8450
8498
  },
8451
8499
  onResize: function(cols, rows) {
8452
8500
  sendTerminalResize(cols, rows);