@co0ontty/wand 1.39.0 → 1.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +0 -0
- package/dist/git-quick-commit.js +27 -0
- package/dist/process-manager.d.ts +1 -0
- package/dist/process-manager.js +134 -11
- package/dist/resume-policy.d.ts +1 -0
- package/dist/resume-policy.js +7 -1
- package/dist/server-session-routes.js +30 -10
- package/dist/types.d.ts +4 -0
- package/dist/web-ui/content/scripts.js +563 -263
- package/dist/web-ui/content/styles.css +372 -356
- package/package.json +1 -1
- package/dist/acp-protocol.d.ts +0 -67
- package/dist/acp-protocol.js +0 -291
- package/dist/claude-stream-adapter.d.ts +0 -35
- package/dist/claude-stream-adapter.js +0 -153
- package/dist/claude-structured-runner.d.ts +0 -27
- package/dist/claude-structured-runner.js +0 -106
- package/dist/message-parser.d.ts +0 -2
- package/dist/message-parser.js +0 -329
- package/dist/message-queue.d.ts +0 -57
- package/dist/message-queue.js +0 -127
- package/dist/session-lifecycle.d.ts +0 -81
- package/dist/session-lifecycle.js +0 -176
|
@@ -277,10 +277,9 @@
|
|
|
277
277
|
quickCommitSubmitting: false,
|
|
278
278
|
quickCommitGenerating: false,
|
|
279
279
|
quickCommitError: "",
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
quickCommitForm: { customMessage: "", makeTag: false, tag: "", primaryAction: "commit" },
|
|
280
|
+
// commitMode: "commit-tag" (commit + version tag) | "commit" (commit only).
|
|
281
|
+
// Pushing is a separate, standalone action — never bundled into the commit button.
|
|
282
|
+
quickCommitForm: { customMessage: "", tag: "", tagEdited: false, commitMode: "commit-tag" },
|
|
284
283
|
// Which inline panel/dropdown is open. Only one can be open at a time, so a
|
|
285
284
|
// single field beats juggling three sibling booleans with mutual-exclusion code.
|
|
286
285
|
// Values: null | "action" | "push" | "tag-head".
|
|
@@ -1531,8 +1530,14 @@
|
|
|
1531
1530
|
if (_macAppVersion) {
|
|
1532
1531
|
checkDmgAutoUpdate();
|
|
1533
1532
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1533
|
+
// Warm up history in the background a beat after first paint so
|
|
1534
|
+
// the inline 历史会话 count is real (not "···") and recent CLI
|
|
1535
|
+
// sessions merge into the list without a manual expand. Deferred
|
|
1536
|
+
// to avoid competing with the initial session/output load.
|
|
1537
|
+
if (!state.claudeHistoryLoaded) {
|
|
1538
|
+
setTimeout(function() {
|
|
1539
|
+
if (!state.claudeHistoryLoaded) ensureClaudeHistoryLoaded();
|
|
1540
|
+
}, 600);
|
|
1536
1541
|
}
|
|
1537
1542
|
});
|
|
1538
1543
|
})
|
|
@@ -1785,7 +1790,9 @@
|
|
|
1785
1790
|
// isAnchored = 边栏占据布局空间(推开主内容)。桌面 pin 或 任意端窄条都算 anchored。
|
|
1786
1791
|
var isMobile = isMobileLayout();
|
|
1787
1792
|
var isCollapsed = !!state.sidebarPinned && !!state.sidebarCollapsed;
|
|
1788
|
-
|
|
1793
|
+
// 桌面端任何「可见」的侧栏都停靠(推开内容),绝不悬浮遮挡——避免主区被压到
|
|
1794
|
+
// 侧栏下面。pinned 只表示「锁定常驻」,open 则是临时可见,两者都算停靠。
|
|
1795
|
+
var isAnchored = isCollapsed || (!isMobile && (!!state.sidebarPinned || !!state.sessionsDrawerOpen));
|
|
1789
1796
|
var collapsedCls = isCollapsed ? ' sidebar-collapsed' : '';
|
|
1790
1797
|
var sidebarCollapsedCls = isCollapsed ? ' collapsed' : '';
|
|
1791
1798
|
return '<div class="app-container">' +
|
|
@@ -1814,7 +1821,10 @@
|
|
|
1814
1821
|
'</button>' +
|
|
1815
1822
|
'</div>' +
|
|
1816
1823
|
'</div>' +
|
|
1817
|
-
'<button id="sidebar-
|
|
1824
|
+
'<button id="sidebar-pin-btn" class="btn btn-ghost btn-sm sidebar-pin-toggle' + (state.sidebarPinned ? ' pinned' : '') + '" type="button" title="' + (state.sidebarPinned ? '已固定常驻(点击解除锁定)' : '固定侧栏常驻') + '" aria-label="' + (state.sidebarPinned ? '解除固定常驻' : '固定侧栏常驻') + '" aria-pressed="' + (state.sidebarPinned ? 'true' : 'false') + '">' +
|
|
1825
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24z"/></svg>' +
|
|
1826
|
+
'</button>' +
|
|
1827
|
+
'<button id="sidebar-collapse-btn" class="btn btn-ghost btn-sm sidebar-collapse-toggle' + (isCollapsed ? ' collapsed' : '') + '" type="button" title="' + (isCollapsed ? '展开为全尺寸' : '收起为窄条') + '" aria-label="' + (isCollapsed ? '展开为全尺寸' : '收起为窄条') + '">' +
|
|
1818
1828
|
(isCollapsed
|
|
1819
1829
|
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="10 6 16 12 10 18"/><line x1="20" y1="5" x2="20" y2="19"/></svg>'
|
|
1820
1830
|
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="14 6 8 12 14 18"/><line x1="4" y1="5" x2="4" y2="19"/></svg>') +
|
|
@@ -1827,7 +1837,6 @@
|
|
|
1827
1837
|
'<div class="sessions-list" id="sessions-list">' + renderSessionsListContent() + '</div>' +
|
|
1828
1838
|
'</div>' +
|
|
1829
1839
|
'</div>' +
|
|
1830
|
-
'<div id="sidebar-history-region" class="sidebar-history-region">' + renderClaudeHistoryRegion() + '</div>' +
|
|
1831
1840
|
'<div class="sidebar-footer">' +
|
|
1832
1841
|
'<button id="drawer-new-session-button" class="btn btn-primary btn-block"><span>+</span> 新会话</button>' +
|
|
1833
1842
|
'<div class="sidebar-footer-actions">' +
|
|
@@ -1912,7 +1921,7 @@
|
|
|
1912
1921
|
'</div>' +
|
|
1913
1922
|
'<div class="file-search-box">' +
|
|
1914
1923
|
'<span class="file-search-icon">' + wandFileIcon("search", { size: 14 }) + '</span>' +
|
|
1915
|
-
'<input type="text" id="file-search-input" class="file-search-input" placeholder="搜索当前目录…" autocomplete="off" />' +
|
|
1924
|
+
'<input type="text" id="file-search-input" class="file-search-input" placeholder="搜索当前目录…" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />' +
|
|
1916
1925
|
'<button class="file-search-clear" id="file-search-clear" type="button" aria-label="清除搜索" title="清除">' +
|
|
1917
1926
|
wandFileIcon("x", { size: 13 }) +
|
|
1918
1927
|
'</button>' +
|
|
@@ -2057,13 +2066,14 @@
|
|
|
2057
2066
|
'</div>' +
|
|
2058
2067
|
'</div>' +
|
|
2059
2068
|
renderExpandedShortcutsRow() +
|
|
2060
|
-
// Session info bar at bottom —
|
|
2069
|
+
// Session info bar at bottom — 仅保留信息类徽章(历史会话 id / exit code)。
|
|
2061
2070
|
// 自动批准已从这里移到主 pill 行(renderAutoApproveChip)。
|
|
2062
2071
|
(selectedSession
|
|
2063
2072
|
? (function() {
|
|
2064
2073
|
var bits = "";
|
|
2065
|
-
if (selectedSession.provider === "claude" && selectedSession.claudeSessionId) {
|
|
2066
|
-
|
|
2074
|
+
if ((selectedSession.provider === "claude" || selectedSession.provider === "codex") && selectedSession.claudeSessionId) {
|
|
2075
|
+
var historyIdTitle = selectedSession.provider === "codex" ? "点击复制 Codex thread ID" : "点击复制 Claude 会话 ID";
|
|
2076
|
+
bits += '<span id="claude-session-id-badge" class="claude-session-id-badge" data-claude-id="' + escapeHtml(selectedSession.claudeSessionId) + '" title="' + historyIdTitle + '">' + iconSvg("cloud", { size: 11, strokeWidth: 1.7, cls: "claude-session-id-icon" }) + '<span class="claude-session-id-text">' + escapeHtml(selectedSession.claudeSessionId.slice(0, 8)) + '</span></span>';
|
|
2067
2077
|
}
|
|
2068
2078
|
// 非结构化会话:进程退出后展示退出码(哪怕 0,告诉用户已正常结束)。
|
|
2069
2079
|
// 结构化会话:只在退出码非 0(即真有失败)时展示,避免成功的多轮对话也挂个 "退出码=0" 误导。
|
|
@@ -2095,7 +2105,7 @@
|
|
|
2095
2105
|
'<div id="folder-breadcrumb" class="folder-breadcrumb"></div>' +
|
|
2096
2106
|
'<div class="folder-picker">' +
|
|
2097
2107
|
'<span class="folder-picker-icon">' + iconSvg("folder", { size: 15, strokeWidth: 1.7 }) + '</span>' +
|
|
2098
|
-
'<input type="text" id="folder-picker-input" class="folder-picker-input" value="" placeholder="输入或选择工作目录..." autocomplete="off" />' +
|
|
2108
|
+
'<input type="text" id="folder-picker-input" class="folder-picker-input" value="" placeholder="输入或选择工作目录..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />' +
|
|
2099
2109
|
'</div>' +
|
|
2100
2110
|
'<div id="folder-picker-dropdown" class="folder-picker-dropdown hidden"></div>' +
|
|
2101
2111
|
'<div id="folder-picker-validation" class="folder-picker-validation"></div>' +
|
|
@@ -2162,7 +2172,8 @@
|
|
|
2162
2172
|
|
|
2163
2173
|
var infoItems = "";
|
|
2164
2174
|
if (hasClaudeId) {
|
|
2165
|
-
|
|
2175
|
+
var historyIdLabel = session.provider === "codex" ? "复制 Codex thread ID" : "复制 Claude 会话 ID";
|
|
2176
|
+
infoItems += '<button class="topbar-more-item" data-action="copy-claude-session-id" type="button" role="menuitem">' + cloudIconSvg + '<span>' + historyIdLabel + '</span></button>';
|
|
2166
2177
|
}
|
|
2167
2178
|
if (hasCwd) {
|
|
2168
2179
|
infoItems += '<button class="topbar-more-item" data-action="copy-cwd" type="button" role="menuitem">' + folderIconSvg + '<span>复制工作目录</span></button>';
|
|
@@ -2232,16 +2243,17 @@
|
|
|
2232
2243
|
var quickCommitEscHandler = null;
|
|
2233
2244
|
var quickCommitDocClickHandler = null;
|
|
2234
2245
|
|
|
2235
|
-
// Restore the user's last
|
|
2236
|
-
|
|
2246
|
+
// Restore the user's last commit-mode choice so the split button feels sticky.
|
|
2247
|
+
// Modes: "commit-tag" (commit + version tag) | "commit" (commit only).
|
|
2248
|
+
function readSavedCommitMode() {
|
|
2237
2249
|
try {
|
|
2238
|
-
var v = localStorage.getItem("wand.quickCommit.
|
|
2239
|
-
if (v === "commit" || v === "commit-
|
|
2250
|
+
var v = localStorage.getItem("wand.quickCommit.commitMode");
|
|
2251
|
+
if (v === "commit" || v === "commit-tag") return v;
|
|
2240
2252
|
} catch (e) { /* localStorage may be blocked */ }
|
|
2241
|
-
return "commit";
|
|
2253
|
+
return "commit-tag"; // default: commit + tag
|
|
2242
2254
|
}
|
|
2243
|
-
function
|
|
2244
|
-
try { localStorage.setItem("wand.quickCommit.
|
|
2255
|
+
function saveCommitMode(value) {
|
|
2256
|
+
try { localStorage.setItem("wand.quickCommit.commitMode", value); } catch (e) { /* no-op */ }
|
|
2245
2257
|
}
|
|
2246
2258
|
|
|
2247
2259
|
function openQuickCommitModal() {
|
|
@@ -2251,9 +2263,11 @@
|
|
|
2251
2263
|
state.quickCommitError = "";
|
|
2252
2264
|
state.quickCommitForm = {
|
|
2253
2265
|
customMessage: "",
|
|
2254
|
-
makeTag: false,
|
|
2255
2266
|
tag: "",
|
|
2256
|
-
|
|
2267
|
+
// Whether the user has manually edited the tag (so we stop auto-overwriting it).
|
|
2268
|
+
tagEdited: false,
|
|
2269
|
+
// "commit-tag" → commit + version tag; "commit" → commit only.
|
|
2270
|
+
commitMode: readSavedCommitMode(),
|
|
2257
2271
|
};
|
|
2258
2272
|
state.quickCommitOpenMenu = null;
|
|
2259
2273
|
state.quickCommitTagHeadForm = { tag: "", push: false };
|
|
@@ -2303,6 +2317,12 @@
|
|
|
2303
2317
|
document.addEventListener("click", quickCommitDocClickHandler, true);
|
|
2304
2318
|
loadGitStatus(state.selectedId, { force: true }).then(function() {
|
|
2305
2319
|
if (!state.quickCommitOpen) return;
|
|
2320
|
+
// Seed the tag field with the locally-derived suggestion so a tag is
|
|
2321
|
+
// always shown by default (greyed until the toggle is turned on).
|
|
2322
|
+
var st = state.gitStatus || {};
|
|
2323
|
+
if (!state.quickCommitForm.tagEdited && st.suggestedTag) {
|
|
2324
|
+
state.quickCommitForm.tag = st.suggestedTag;
|
|
2325
|
+
}
|
|
2306
2326
|
rerenderQuickCommitModal();
|
|
2307
2327
|
});
|
|
2308
2328
|
}
|
|
@@ -2354,23 +2374,31 @@
|
|
|
2354
2374
|
var aiBtn = document.getElementById("quick-commit-ai-btn");
|
|
2355
2375
|
if (aiBtn) aiBtn.addEventListener("click", generateCommitMessageAI);
|
|
2356
2376
|
var msgEl = document.getElementById("quick-commit-message");
|
|
2357
|
-
if (msgEl)
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
});
|
|
2377
|
+
if (msgEl) {
|
|
2378
|
+
msgEl.addEventListener("input", function() {
|
|
2379
|
+
state.quickCommitForm.customMessage = msgEl.value;
|
|
2380
|
+
});
|
|
2381
|
+
// Cmd/Ctrl+Enter submits, matching the common editor shortcut.
|
|
2382
|
+
msgEl.addEventListener("keydown", function(e) {
|
|
2383
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
2384
|
+
e.preventDefault();
|
|
2385
|
+
submitQuickCommit();
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2370
2389
|
var tagInput = document.getElementById("quick-commit-tag");
|
|
2371
|
-
if (tagInput)
|
|
2372
|
-
|
|
2373
|
-
|
|
2390
|
+
if (tagInput) {
|
|
2391
|
+
tagInput.addEventListener("input", function() {
|
|
2392
|
+
state.quickCommitForm.tag = tagInput.value;
|
|
2393
|
+
state.quickCommitForm.tagEdited = true;
|
|
2394
|
+
});
|
|
2395
|
+
tagInput.addEventListener("keydown", function(e) {
|
|
2396
|
+
if (e.key === "Enter") {
|
|
2397
|
+
e.preventDefault();
|
|
2398
|
+
submitQuickCommit();
|
|
2399
|
+
}
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2374
2402
|
|
|
2375
2403
|
var actionCaret = document.getElementById("quick-commit-action-caret");
|
|
2376
2404
|
if (actionCaret) actionCaret.addEventListener("click", function(e) {
|
|
@@ -2380,17 +2408,24 @@
|
|
|
2380
2408
|
});
|
|
2381
2409
|
var actionMenu = document.getElementById("quick-commit-action-menu");
|
|
2382
2410
|
if (actionMenu) {
|
|
2383
|
-
var actionItems = actionMenu.querySelectorAll("[data-qc-
|
|
2411
|
+
var actionItems = actionMenu.querySelectorAll("[data-qc-commit-mode]");
|
|
2384
2412
|
for (var i = 0; i < actionItems.length; i++) {
|
|
2385
2413
|
(function(btn) {
|
|
2386
2414
|
btn.addEventListener("click", function() {
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2415
|
+
// Keep what the user typed before the re-render.
|
|
2416
|
+
var liveTag = document.getElementById("quick-commit-tag");
|
|
2417
|
+
if (liveTag && !liveTag.disabled) state.quickCommitForm.tag = liveTag.value;
|
|
2418
|
+
var value = btn.getAttribute("data-qc-commit-mode");
|
|
2419
|
+
if (value === "commit" || value === "commit-tag") {
|
|
2420
|
+
state.quickCommitForm.commitMode = value;
|
|
2421
|
+
saveCommitMode(value);
|
|
2391
2422
|
}
|
|
2392
2423
|
state.quickCommitOpenMenu = null;
|
|
2393
2424
|
rerenderQuickCommitModal();
|
|
2425
|
+
if (value === "commit-tag") {
|
|
2426
|
+
var inp = document.getElementById("quick-commit-tag");
|
|
2427
|
+
if (inp) setTimeout(function() { inp.focus(); var v = inp.value; inp.value = ""; inp.value = v; }, 0);
|
|
2428
|
+
}
|
|
2394
2429
|
});
|
|
2395
2430
|
})(actionItems[i]);
|
|
2396
2431
|
}
|
|
@@ -2400,7 +2435,14 @@
|
|
|
2400
2435
|
if (tagHeadToggle) tagHeadToggle.addEventListener("click", function() {
|
|
2401
2436
|
var willOpen = state.quickCommitOpenMenu !== "tag-head";
|
|
2402
2437
|
state.quickCommitOpenMenu = willOpen ? "tag-head" : null;
|
|
2403
|
-
if (willOpen)
|
|
2438
|
+
if (willOpen) {
|
|
2439
|
+
state.quickCommitTagHeadError = "";
|
|
2440
|
+
// Pre-fill with the locally-derived suggestion (unless already set).
|
|
2441
|
+
var sug = (state.gitStatus || {}).suggestedTag;
|
|
2442
|
+
if (sug && !(state.quickCommitTagHeadForm.tag || "").trim()) {
|
|
2443
|
+
state.quickCommitTagHeadForm.tag = sug;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2404
2446
|
rerenderQuickCommitModal();
|
|
2405
2447
|
if (willOpen) {
|
|
2406
2448
|
var inp = document.getElementById("quick-commit-tag-head-input");
|
|
@@ -2487,14 +2529,19 @@
|
|
|
2487
2529
|
var data = result.data || {};
|
|
2488
2530
|
var aiMessage = typeof data.message === "string" ? data.message : "";
|
|
2489
2531
|
var aiTag = typeof data.suggestedTag === "string" ? data.suggestedTag.trim() : "";
|
|
2490
|
-
//
|
|
2532
|
+
// "AI 生成" recommends BOTH a commit message and a version tag.
|
|
2533
|
+
// Fill the message only when empty (never clobber what the user typed).
|
|
2491
2534
|
var currentMessage = (state.quickCommitForm.customMessage || "").trim();
|
|
2492
2535
|
if (!currentMessage && aiMessage) {
|
|
2493
2536
|
state.quickCommitForm.customMessage = aiMessage;
|
|
2494
2537
|
}
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2538
|
+
// Adopt the AI tag (smarter than the local patch-bump default) unless the
|
|
2539
|
+
// user has manually edited it, and switch to "commit + tag" so the
|
|
2540
|
+
// recommendation is actually applied on commit.
|
|
2541
|
+
if (aiTag) {
|
|
2542
|
+
if (!state.quickCommitForm.tagEdited) state.quickCommitForm.tag = aiTag;
|
|
2543
|
+
state.quickCommitForm.commitMode = "commit-tag";
|
|
2544
|
+
saveCommitMode("commit-tag");
|
|
2498
2545
|
}
|
|
2499
2546
|
})
|
|
2500
2547
|
.catch(function(error) {
|
|
@@ -2513,21 +2560,22 @@
|
|
|
2513
2560
|
var tagEl = document.getElementById("quick-commit-tag");
|
|
2514
2561
|
if (tagEl) state.quickCommitForm.tag = tagEl.value;
|
|
2515
2562
|
var form = state.quickCommitForm || {};
|
|
2516
|
-
var
|
|
2563
|
+
var withTag = form.commitMode === "commit-tag";
|
|
2564
|
+
var userTag = withTag ? (form.tag || "").trim() : "";
|
|
2517
2565
|
var message = (form.customMessage || "").trim();
|
|
2518
2566
|
if (!message) {
|
|
2519
|
-
state.quickCommitError = "
|
|
2567
|
+
state.quickCommitError = "请填写提交信息,或点击「AI 生成」。";
|
|
2520
2568
|
rerenderQuickCommitModal();
|
|
2521
2569
|
return;
|
|
2522
2570
|
}
|
|
2523
|
-
|
|
2524
|
-
//
|
|
2571
|
+
// Commit no longer pushes — pushing is a separate, standalone action.
|
|
2572
|
+
// 选了「提交并打 Tag」但 tag 留空 → 由后端在提交时调 AI 生成。
|
|
2525
2573
|
var payload = {
|
|
2526
2574
|
autoMessage: false,
|
|
2527
2575
|
customMessage: message,
|
|
2528
2576
|
tag: userTag,
|
|
2529
|
-
autoTag: !!(
|
|
2530
|
-
push:
|
|
2577
|
+
autoTag: !!(withTag && !userTag),
|
|
2578
|
+
push: false
|
|
2531
2579
|
};
|
|
2532
2580
|
state.quickCommitSubmitting = true;
|
|
2533
2581
|
state.quickCommitError = "";
|
|
@@ -2550,14 +2598,8 @@
|
|
|
2550
2598
|
var subPrefix = subCommits.length > 0
|
|
2551
2599
|
? "已先提交 " + subCommits.length + " 个 submodule(" + subCommits.map(function(c) { return c.path; }).join("、") + "),"
|
|
2552
2600
|
: "";
|
|
2553
|
-
var base = subPrefix + "已提交" + (hash ? " " + hash : "") + (tagName ? ",已打
|
|
2554
|
-
if (
|
|
2555
|
-
var msg = base + ";push 失败:" + data.pushError;
|
|
2556
|
-
if (typeof showToast === "function") showToast(msg, "error");
|
|
2557
|
-
} else {
|
|
2558
|
-
var okMsg = base + (data.pushed ? ",已 push" : "");
|
|
2559
|
-
if (typeof showToast === "function") showToast(okMsg, "success");
|
|
2560
|
-
}
|
|
2601
|
+
var base = subPrefix + "已提交" + (hash ? " " + hash : "") + (tagName ? ",已打 Tag " + tagName : "");
|
|
2602
|
+
if (typeof showToast === "function") showToast(base + "。可在「同步」区推送到远端。", "success");
|
|
2561
2603
|
closeQuickCommitModal();
|
|
2562
2604
|
if (state.selectedId) loadGitStatus(state.selectedId, { force: true });
|
|
2563
2605
|
})
|
|
@@ -2697,16 +2739,37 @@
|
|
|
2697
2739
|
});
|
|
2698
2740
|
}
|
|
2699
2741
|
|
|
2742
|
+
// Map a porcelain status (XY two-char, e.g. " M", "A.", "??") to a single
|
|
2743
|
+
// VS-Code-style letter badge: pick the first meaningful char, color by kind.
|
|
2744
|
+
function qcStatusBadge(status) {
|
|
2745
|
+
var raw = (status || "").trim();
|
|
2746
|
+
if (raw === "??") return { letter: "U", cls: "untracked", title: "未跟踪" };
|
|
2747
|
+
if (raw === "!!") return { letter: "I", cls: "ignored", title: "已忽略" };
|
|
2748
|
+
var c = "";
|
|
2749
|
+
for (var i = 0; i < status.length; i++) {
|
|
2750
|
+
if (status[i] && status[i] !== "." && status[i] !== " ") { c = status[i]; break; }
|
|
2751
|
+
}
|
|
2752
|
+
c = (c || raw[0] || "?").toUpperCase();
|
|
2753
|
+
var map = {
|
|
2754
|
+
A: { cls: "add", title: "新增" },
|
|
2755
|
+
M: { cls: "mod", title: "修改" },
|
|
2756
|
+
D: { cls: "del", title: "删除" },
|
|
2757
|
+
R: { cls: "ren", title: "重命名" },
|
|
2758
|
+
C: { cls: "ren", title: "复制" },
|
|
2759
|
+
T: { cls: "mod", title: "类型变更" },
|
|
2760
|
+
U: { cls: "del", title: "冲突" }
|
|
2761
|
+
};
|
|
2762
|
+
var hit = map[c] || { cls: "other", title: "已更改" };
|
|
2763
|
+
return { letter: c, cls: hit.cls, title: hit.title };
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2700
2766
|
function renderQuickCommitFileRows(files) {
|
|
2701
2767
|
var rows = files.map(function(item) {
|
|
2702
|
-
var
|
|
2703
|
-
var
|
|
2704
|
-
var
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
else if (flag === "M" || status[0] === "M") cls += " qc-flag-mod";
|
|
2708
|
-
else if (flag === "??" || status === "??") cls += " qc-flag-untracked";
|
|
2709
|
-
else if (flag === "R") cls += " qc-flag-ren";
|
|
2768
|
+
var badge = qcStatusBadge(item.status || "");
|
|
2769
|
+
var fullPath = item.path || "";
|
|
2770
|
+
var slash = fullPath.lastIndexOf("/");
|
|
2771
|
+
var dir = slash >= 0 ? fullPath.slice(0, slash + 1) : "";
|
|
2772
|
+
var base = slash >= 0 ? fullPath.slice(slash + 1) : fullPath;
|
|
2710
2773
|
var subBadge = "";
|
|
2711
2774
|
if (item.isSubmodule) {
|
|
2712
2775
|
var st = item.submoduleState || {};
|
|
@@ -2717,7 +2780,13 @@
|
|
|
2717
2780
|
var label = parts.length ? "submodule · " + parts.join(" / ") : "submodule";
|
|
2718
2781
|
subBadge = '<span class="qc-submodule-badge">' + escapeHtml(label) + '</span>';
|
|
2719
2782
|
}
|
|
2720
|
-
return '<div class="qc-file-row"
|
|
2783
|
+
return '<div class="qc-file-row" title="' + escapeHtml(fullPath) + '">' +
|
|
2784
|
+
'<span class="qc-file-badge qc-badge-' + badge.cls + '" title="' + escapeHtml(badge.title) + '">' + escapeHtml(badge.letter) + '</span>' +
|
|
2785
|
+
'<span class="qc-file-path">' +
|
|
2786
|
+
(dir ? '<span class="qc-file-dir">' + escapeHtml(dir) + '</span>' : '') +
|
|
2787
|
+
'<span class="qc-file-name">' + escapeHtml(base) + '</span>' +
|
|
2788
|
+
'</span>' + subBadge +
|
|
2789
|
+
'</div>';
|
|
2721
2790
|
}).join("");
|
|
2722
2791
|
return rows || '<div class="qc-empty">没有可提交的改动。</div>';
|
|
2723
2792
|
}
|
|
@@ -2726,23 +2795,25 @@
|
|
|
2726
2795
|
return state.quickCommitSubmitting || state.quickCommitTagHeadSubmitting || state.quickCommitPushing;
|
|
2727
2796
|
}
|
|
2728
2797
|
|
|
2729
|
-
function
|
|
2798
|
+
function renderQuickCommitCommitButton(hasChanges) {
|
|
2730
2799
|
var f = state.quickCommitForm;
|
|
2800
|
+
var withTag = f.commitMode === "commit-tag";
|
|
2731
2801
|
var label;
|
|
2732
2802
|
if (state.quickCommitSubmitting) label = "提交中…";
|
|
2733
|
-
else
|
|
2734
|
-
else label = "仅提交";
|
|
2803
|
+
else label = withTag ? "提交并打 Tag" : "提交";
|
|
2735
2804
|
var disabled = !hasChanges || isQuickCommitOpInFlight();
|
|
2736
2805
|
var menuOpen = state.quickCommitOpenMenu === "action";
|
|
2737
2806
|
var caretActive = menuOpen ? " is-active" : "";
|
|
2738
2807
|
var menuItems = [
|
|
2739
|
-
{ value: "commit", label: "
|
|
2740
|
-
{ value: "commit
|
|
2808
|
+
{ value: "commit-tag", label: "提交并打 Tag", desc: "创建 commit,并为它打一个版本 Tag" },
|
|
2809
|
+
{ value: "commit", label: "仅提交", desc: "只创建 commit,不打 Tag" }
|
|
2741
2810
|
];
|
|
2742
2811
|
var menuHtml = menuItems.map(function(item) {
|
|
2743
|
-
var sel = f.
|
|
2744
|
-
return '<button type="button" class="qc-dropdown-item' + sel + '" data-qc-
|
|
2745
|
-
'<span class="qc-dropdown-item-
|
|
2812
|
+
var sel = f.commitMode === item.value ? " is-selected" : "";
|
|
2813
|
+
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') + '">' +
|
|
2814
|
+
'<span class="qc-dropdown-item-main"><span class="qc-dropdown-check" aria-hidden="true">' +
|
|
2815
|
+
(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>' : '') +
|
|
2816
|
+
'</span><span class="qc-dropdown-item-title">' + escapeHtml(item.label) + '</span></span>' +
|
|
2746
2817
|
'<span class="qc-dropdown-item-desc">' + escapeHtml(item.desc) + '</span>' +
|
|
2747
2818
|
'</button>';
|
|
2748
2819
|
}).join("");
|
|
@@ -2750,7 +2821,7 @@
|
|
|
2750
2821
|
'<button id="quick-commit-submit-btn" class="btn btn-primary qc-split-main" type="button"' + (disabled ? ' disabled' : '') + '>' +
|
|
2751
2822
|
escapeHtml(label) +
|
|
2752
2823
|
'</button>' +
|
|
2753
|
-
'<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="
|
|
2824
|
+
'<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="切换提交方式">' +
|
|
2754
2825
|
'<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>' +
|
|
2755
2826
|
'</button>' +
|
|
2756
2827
|
(menuOpen ?
|
|
@@ -2781,18 +2852,19 @@
|
|
|
2781
2852
|
var submitting = state.quickCommitTagHeadSubmitting;
|
|
2782
2853
|
var generating = state.quickCommitTagHeadGenerating;
|
|
2783
2854
|
return '<div class="qc-tag-head-panel">' +
|
|
2855
|
+
'<div class="qc-tag-head-hint">为当前最新提交(HEAD)打 Tag,不创建新提交。</div>' +
|
|
2784
2856
|
'<div class="qc-tag-head-row">' +
|
|
2785
|
-
'<input type="text" id="quick-commit-tag-head-input" class="field-input" placeholder="
|
|
2857
|
+
'<input type="text" id="quick-commit-tag-head-input" class="field-input" placeholder="版本号,如 v1.2.0" value="' + escapeHtml(thf.tag || "") + '"' + (submitting ? ' disabled' : '') + '>' +
|
|
2786
2858
|
'<button type="button" id="quick-commit-tag-head-ai" class="btn btn-ghost btn-sm"' + (generating || submitting ? ' disabled' : '') + '>' + (generating ? '建议中…' : 'AI 建议') + '</button>' +
|
|
2787
2859
|
'</div>' +
|
|
2788
2860
|
'<label class="qc-tag-head-push">' +
|
|
2789
2861
|
'<input type="checkbox" id="quick-commit-tag-head-push"' + (thf.push ? ' checked' : '') + (submitting ? ' disabled' : '') + '>' +
|
|
2790
|
-
'<span
|
|
2862
|
+
'<span>打完后立即推送这个 Tag 到远端</span>' +
|
|
2791
2863
|
'</label>' +
|
|
2792
2864
|
(state.quickCommitTagHeadError ? '<p class="error-message">' + escapeHtml(state.quickCommitTagHeadError) + '</p>' : '') +
|
|
2793
2865
|
'<div class="qc-tag-head-actions">' +
|
|
2794
2866
|
'<button type="button" id="quick-commit-tag-head-cancel" class="btn btn-ghost btn-sm"' + (submitting ? ' disabled' : '') + '>收起</button>' +
|
|
2795
|
-
'<button type="button" id="quick-commit-tag-head-submit" class="btn btn-secondary btn-sm"' + (submitting ? ' disabled' : '') + '>' + (submitting ? '打
|
|
2867
|
+
'<button type="button" id="quick-commit-tag-head-submit" class="btn btn-secondary btn-sm"' + (submitting ? ' disabled' : '') + '>' + (submitting ? '打 Tag 中…' : '打 Tag') + '</button>' +
|
|
2796
2868
|
'</div>' +
|
|
2797
2869
|
'</div>';
|
|
2798
2870
|
}
|
|
@@ -2835,56 +2907,53 @@
|
|
|
2835
2907
|
|
|
2836
2908
|
function renderQuickCommitModal() {
|
|
2837
2909
|
var s = state.gitStatus || {};
|
|
2838
|
-
var f = state.quickCommitForm || { customMessage: "",
|
|
2910
|
+
var f = state.quickCommitForm || { customMessage: "", tag: "", tagEdited: false, commitMode: "commit-tag" };
|
|
2839
2911
|
var hasChanges = (s.modifiedCount || 0) > 0;
|
|
2840
2912
|
var files = Array.isArray(s.files) ? s.files : [];
|
|
2841
2913
|
var fileRows = renderQuickCommitFileRows(files);
|
|
2842
2914
|
|
|
2843
|
-
// Subtitle: branch · N 改动 · ↑X ↓Y
|
|
2915
|
+
// Subtitle: branch · N 改动 · ↑X ↓Y (clean repos show a small ✓ badge instead of "0 个改动")
|
|
2844
2916
|
var subParts = [];
|
|
2845
2917
|
subParts.push(s.branch || "(no branch)");
|
|
2846
|
-
subParts.push((s.modifiedCount || 0) + " 个改动");
|
|
2918
|
+
if (hasChanges) subParts.push((s.modifiedCount || 0) + " 个改动");
|
|
2847
2919
|
if (typeof s.ahead === "number" && s.ahead > 0) subParts.push("↑" + s.ahead);
|
|
2848
2920
|
if (typeof s.behind === "number" && s.behind > 0) subParts.push("↓" + s.behind);
|
|
2849
2921
|
|
|
2850
2922
|
// Section 1: changes + commit form (only when there are changes)
|
|
2851
2923
|
var section1 = "";
|
|
2852
2924
|
if (hasChanges) {
|
|
2925
|
+
var genBusy = state.quickCommitGenerating;
|
|
2926
|
+
var withTag = f.commitMode === "commit-tag";
|
|
2853
2927
|
section1 = '<section class="qc-section qc-section--changes">' +
|
|
2854
|
-
'<div class="qc-section-head"><span class="qc-section-title"
|
|
2928
|
+
'<div class="qc-section-head"><span class="qc-section-title">更改</span><span class="qc-section-meta">' + escapeHtml(s.modifiedCount + " 个文件") + '</span></div>' +
|
|
2855
2929
|
'<div class="qc-files-wrap">' + fileRows + '</div>' +
|
|
2856
2930
|
'<div class="qc-message-row" id="quick-commit-message-row">' +
|
|
2857
|
-
'<div class="qc-message-header"
|
|
2858
|
-
'<
|
|
2859
|
-
|
|
2860
|
-
'<
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
'<input type="checkbox" id="quick-commit-make-tag" class="switch-toggle" aria-label="同时为本次 commit 打 tag"' + (f.makeTag ? ' checked' : '') + ((state.quickCommitSubmitting || state.quickCommitGenerating) ? ' disabled' : '') + '>' +
|
|
2864
|
-
'<span class="switch-slider"></span>' +
|
|
2865
|
-
'</span>' +
|
|
2866
|
-
'</label>' +
|
|
2867
|
-
'</div>' +
|
|
2931
|
+
'<div class="qc-message-header">' +
|
|
2932
|
+
'<label class="field-label qc-message-label" for="quick-commit-message">提交信息</label>' +
|
|
2933
|
+
'<button type="button" id="quick-commit-ai-btn" class="btn btn-ghost btn-sm qc-ai-btn"' + (genBusy ? ' disabled' : '') + ' title="读取改动,用 AI 生成提交信息与版本 Tag">' +
|
|
2934
|
+
'<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>' +
|
|
2935
|
+
'<span>' + (genBusy ? '生成中…' : 'AI 生成') + '</span>' +
|
|
2936
|
+
'</button>' +
|
|
2868
2937
|
'</div>' +
|
|
2869
|
-
'<textarea id="quick-commit-message" class="field-input" rows="
|
|
2938
|
+
'<textarea id="quick-commit-message" class="field-input qc-message-input" rows="3" placeholder="描述这次改动;或点「AI 生成」自动填写" ' + (state.quickCommitSubmitting ? 'disabled' : '') + '>' + escapeHtml(f.customMessage || "") + '</textarea>' +
|
|
2870
2939
|
'</div>' +
|
|
2871
|
-
'<div class="qc-tag-
|
|
2872
|
-
'<
|
|
2940
|
+
'<div class="qc-tag-field' + (withTag ? '' : ' is-off') + '">' +
|
|
2941
|
+
'<span class="qc-tag-field-label" title="' + (withTag ? '这次提交会打上这个版本 Tag' : '当前为「仅提交」,不会打 Tag') + '">Tag</span>' +
|
|
2942
|
+
'<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' : '') + '>' +
|
|
2943
|
+
(withTag ? '' : '<span class="qc-tag-field-note">仅提交</span>') +
|
|
2873
2944
|
'</div>' +
|
|
2874
2945
|
(state.quickCommitError ? '<p class="error-message">' + escapeHtml(state.quickCommitError) + '</p>' : '') +
|
|
2875
2946
|
'<div class="qc-section-actions">' +
|
|
2876
2947
|
'<button id="quick-commit-cancel-btn" class="btn btn-ghost btn-sm" type="button">取消</button>' +
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
section1 = '<section class="qc-section qc-section--empty">' +
|
|
2882
|
-
'<div class="qc-empty-state">' +
|
|
2883
|
-
'<span class="qc-empty-icon">✓</span>' +
|
|
2884
|
-
'<div><div class="qc-empty-title">工作区干净</div><div class="qc-empty-sub">没有暂存改动可提交。仍可在下方为 HEAD 打 tag 或推送。</div></div>' +
|
|
2948
|
+
'<div class="qc-action-group">' +
|
|
2949
|
+
renderQuickCommitCommitButton(hasChanges) +
|
|
2950
|
+
renderQuickCommitPushButton(s) +
|
|
2951
|
+
'</div>' +
|
|
2885
2952
|
'</div>' +
|
|
2886
2953
|
'</section>';
|
|
2887
2954
|
}
|
|
2955
|
+
// When clean, we skip the big "changes" card entirely — a small green
|
|
2956
|
+
// indicator in the header subtitle is enough of a signal (see below).
|
|
2888
2957
|
|
|
2889
2958
|
// Section 2: repo status + secondary actions (always show when there's at least one commit)
|
|
2890
2959
|
var section2 = "";
|
|
@@ -2892,32 +2961,38 @@
|
|
|
2892
2961
|
var lc = s.lastCommit || {};
|
|
2893
2962
|
var headLine = lc.shortHash ? lc.shortHash + " · " + (lc.subject || "") : (s.head ? s.head.substring(0, 7) : "(no commit)");
|
|
2894
2963
|
var upstreamLine = s.upstream ? escapeHtml(s.branch || "") + " → " + escapeHtml(s.upstream) : escapeHtml(s.branch || "(no branch)") + " · 无 upstream";
|
|
2964
|
+
var tagHeadOpen = state.quickCommitOpenMenu === "tag-head";
|
|
2895
2965
|
section2 = '<section class="qc-section qc-section--repo">' +
|
|
2896
|
-
'<div class="qc-section-head"><span class="qc-section-title"
|
|
2966
|
+
'<div class="qc-section-head"><span class="qc-section-title">仓库 · 同步</span><span class="qc-section-meta">' + upstreamLine + '</span></div>' +
|
|
2897
2967
|
'<div class="qc-head-card">' +
|
|
2898
2968
|
'<span class="qc-head-label">HEAD</span>' +
|
|
2899
2969
|
'<code class="qc-head-text">' + escapeHtml(headLine) + '</code>' +
|
|
2900
2970
|
'</div>' +
|
|
2901
2971
|
renderQuickCommitStatusChips(s) +
|
|
2902
|
-
(
|
|
2972
|
+
(tagHeadOpen ? renderQuickCommitTagHeadPanel() : '') +
|
|
2903
2973
|
'<div class="qc-section-actions qc-section-actions--secondary">' +
|
|
2904
|
-
'<button id="quick-commit-tag-head-toggle" class="btn btn-
|
|
2905
|
-
'<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"
|
|
2906
|
-
|
|
2974
|
+
'<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,不会创建新提交">' +
|
|
2975
|
+
'<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>' +
|
|
2976
|
+
'<span>' + (tagHeadOpen ? '收起' : '为当前提交打 Tag') + '</span>' +
|
|
2907
2977
|
'</button>' +
|
|
2908
|
-
|
|
2978
|
+
// Push lives in the commit footer when there are changes; show it here otherwise.
|
|
2979
|
+
(hasChanges ? '' : renderQuickCommitPushButton(s)) +
|
|
2909
2980
|
'</div>' +
|
|
2910
2981
|
'</section>';
|
|
2911
2982
|
}
|
|
2912
2983
|
|
|
2913
2984
|
var subtitleHtml = subParts.map(escapeHtml).join(" · ");
|
|
2985
|
+
// Small "clean" badge shown inline in the header subtitle (replaces the old empty-state card).
|
|
2986
|
+
var cleanBadge = (!hasChanges && s.isGit !== false)
|
|
2987
|
+
? '<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>'
|
|
2988
|
+
: '';
|
|
2914
2989
|
|
|
2915
2990
|
return '<section id="quick-commit-modal" class="modal-backdrop' + (state.quickCommitOpen ? '' : ' hidden') + '">' +
|
|
2916
2991
|
'<div class="modal quick-commit-modal" role="dialog" aria-labelledby="quick-commit-title">' +
|
|
2917
2992
|
'<div class="modal-header">' +
|
|
2918
2993
|
'<div>' +
|
|
2919
2994
|
'<h2 id="quick-commit-title" class="modal-title">快捷提交</h2>' +
|
|
2920
|
-
'<p class="modal-subtitle">' + subtitleHtml + '</p>' +
|
|
2995
|
+
'<p class="modal-subtitle">' + subtitleHtml + cleanBadge + '</p>' +
|
|
2921
2996
|
'</div>' +
|
|
2922
2997
|
'<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>' +
|
|
2923
2998
|
'</div>' +
|
|
@@ -3509,14 +3584,16 @@
|
|
|
3509
3584
|
}
|
|
3510
3585
|
|
|
3511
3586
|
function renderSessions() {
|
|
3512
|
-
// Claude history is
|
|
3513
|
-
//
|
|
3514
|
-
//
|
|
3587
|
+
// Claude/Codex history is rendered INLINE as the final collapsible
|
|
3588
|
+
// group of this same scrolling list (see renderClaudeHistoryGroup),
|
|
3589
|
+
// styled like 已归档. There is no separate docked region anymore —
|
|
3590
|
+
// that previously stranded an empty void above the footer.
|
|
3515
3591
|
var archivedSessions = state.sessions.filter(function(session) { return session.archived; });
|
|
3516
3592
|
var groups = [];
|
|
3517
3593
|
groups.push(renderSessionManageBar());
|
|
3518
3594
|
|
|
3519
3595
|
var recentEntries = getRecentEntries();
|
|
3596
|
+
var historyGroup = renderClaudeHistoryGroup();
|
|
3520
3597
|
|
|
3521
3598
|
if (recentEntries.length > 0) {
|
|
3522
3599
|
groups.push(renderRecentGroup(recentEntries));
|
|
@@ -3524,9 +3601,12 @@
|
|
|
3524
3601
|
if (archivedSessions.length > 0) {
|
|
3525
3602
|
groups.push(renderArchivedGroup(archivedSessions));
|
|
3526
3603
|
}
|
|
3527
|
-
if (recentEntries.length === 0 && archivedSessions.length === 0) {
|
|
3604
|
+
if (recentEntries.length === 0 && archivedSessions.length === 0 && !historyGroup) {
|
|
3528
3605
|
return renderSessionManageBar() + '<div class="empty-state"><strong>还没有会话记录</strong><br>点击上方「新对话」开始你的第一次对话。</div>';
|
|
3529
3606
|
}
|
|
3607
|
+
if (historyGroup) {
|
|
3608
|
+
groups.push(historyGroup);
|
|
3609
|
+
}
|
|
3530
3610
|
return groups.join("");
|
|
3531
3611
|
}
|
|
3532
3612
|
|
|
@@ -3538,9 +3618,6 @@
|
|
|
3538
3618
|
|
|
3539
3619
|
function renderCollapsedSessionTiles() {
|
|
3540
3620
|
var entries = getRecentEntries();
|
|
3541
|
-
if (entries.length === 0) {
|
|
3542
|
-
return '<div class="sidebar-collapsed-empty" title="无会话">—</div>';
|
|
3543
|
-
}
|
|
3544
3621
|
var tiles = entries.map(function(e, i) {
|
|
3545
3622
|
var idx = i + 1;
|
|
3546
3623
|
if (e.kind === "session") {
|
|
@@ -3554,7 +3631,11 @@
|
|
|
3554
3631
|
var hTitle = preview + " · " + formatHistoryTime(h.timestamp);
|
|
3555
3632
|
return '<button class="sidebar-collapsed-tile history" type="button" data-collapsed-history-id="' + escapeHtml(h.claudeSessionId) + '" data-cwd="' + escapeHtml(h.cwd || "") + '" title="' + escapeHtml(hTitle) + '">' + idx + '</button>';
|
|
3556
3633
|
}).join("");
|
|
3557
|
-
|
|
3634
|
+
// 窄条底部固定一个「+」快速新建会话方块,替代被隐藏的 footer 新会话入口。
|
|
3635
|
+
var addTile = '<button class="sidebar-collapsed-tile add" type="button" data-collapsed-new-session="1" title="新建会话" aria-label="新建会话">' +
|
|
3636
|
+
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>' +
|
|
3637
|
+
'</button>';
|
|
3638
|
+
return '<div class="sidebar-collapsed-tiles">' + tiles + addTile + '</div>';
|
|
3558
3639
|
}
|
|
3559
3640
|
|
|
3560
3641
|
function renderSessionsListContent() {
|
|
@@ -3582,19 +3663,21 @@
|
|
|
3582
3663
|
var selectAllAction = allSelected ? "clear-selection" : "select-all-visible";
|
|
3583
3664
|
var selectAllDisabled = selectable === 0 ? ' disabled' : '';
|
|
3584
3665
|
|
|
3585
|
-
//
|
|
3586
|
-
//
|
|
3666
|
+
// Flat in-place toolbar (NOT a popped card): the same sub-header row
|
|
3667
|
+
// morphs to [✕] N 已选 ........ 全选/取消全选 删除. Sticky to the top
|
|
3668
|
+
// of the scroll so the count + delete stay reachable while selecting.
|
|
3669
|
+
var exitBtn = '<button class="session-manage-exit" data-action="toggle-manage-mode" type="button" aria-label="退出管理模式" title="退出管理模式">' +
|
|
3670
|
+
'<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>' +
|
|
3671
|
+
'</button>';
|
|
3672
|
+
var summary = hasAny
|
|
3673
|
+
? '<span class="session-manage-count">' + totalCount + '</span><span class="session-manage-summary-label">已选择</span>'
|
|
3674
|
+
: '<span class="session-manage-summary-label muted">选择要管理的项目</span>';
|
|
3587
3675
|
return '<div class="session-manage-bar active">' +
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
'<span class="session-manage-summary-label">已选择</span>' +
|
|
3591
|
-
'</div>' +
|
|
3676
|
+
exitBtn +
|
|
3677
|
+
'<div class="session-manage-summary">' + summary + '</div>' +
|
|
3592
3678
|
'<div class="session-manage-actions">' +
|
|
3593
3679
|
'<button class="btn btn-ghost btn-xs" data-action="' + selectAllAction + '" type="button"' + selectAllDisabled + '>' + selectAllLabel + '</button>' +
|
|
3594
|
-
'<button class="btn btn-
|
|
3595
|
-
'<span class="session-manage-divider"></span>' +
|
|
3596
|
-
'<button class="btn btn-danger btn-xs" data-action="delete-selected" type="button"' + (hasAny ? '' : ' disabled') + '>删除</button>' +
|
|
3597
|
-
'<button class="btn btn-primary btn-xs" data-action="toggle-manage-mode" type="button">完成</button>' +
|
|
3680
|
+
'<button class="btn btn-danger btn-xs" data-action="delete-selected" type="button"' + (hasAny ? '' : ' disabled') + '>删除' + (hasAny ? ' ' + totalCount : '') + '</button>' +
|
|
3598
3681
|
'</div>' +
|
|
3599
3682
|
'</div>';
|
|
3600
3683
|
}
|
|
@@ -3621,8 +3704,9 @@
|
|
|
3621
3704
|
}
|
|
3622
3705
|
|
|
3623
3706
|
function renderRecentGroup(entries) {
|
|
3624
|
-
|
|
3625
|
-
|
|
3707
|
+
// No "最近" group title here — the section intro ("最近的会话记录") above
|
|
3708
|
+
// already labels this group, so a second label would be redundant.
|
|
3709
|
+
var html = '<section class="session-group session-group--recent">';
|
|
3626
3710
|
html += entries.map(function(e) {
|
|
3627
3711
|
return e.kind === "session"
|
|
3628
3712
|
? renderSessionItem(e.ref, "sessions")
|
|
@@ -3641,46 +3725,30 @@
|
|
|
3641
3725
|
});
|
|
3642
3726
|
}
|
|
3643
3727
|
|
|
3644
|
-
// Render the
|
|
3645
|
-
//
|
|
3646
|
-
//
|
|
3647
|
-
//
|
|
3648
|
-
function
|
|
3728
|
+
// Render history as the final INLINE collapsible group of #sessions-list,
|
|
3729
|
+
// styled like the 已归档 group. Returns '' when history is fully loaded
|
|
3730
|
+
// and empty, so a workspace with no older CLI history shows no stray
|
|
3731
|
+
// "历史会话 0" row (and no stranded bar above the footer).
|
|
3732
|
+
function renderClaudeHistoryGroup() {
|
|
3649
3733
|
var visibleHistory = getClaudeHistoryRegionItems();
|
|
3650
|
-
var expanded = !!state.claudeHistoryExpanded;
|
|
3651
3734
|
var loaded = !!state.claudeHistoryLoaded;
|
|
3652
3735
|
var codexVisible = getVisibleCodexHistorySessions();
|
|
3653
3736
|
var codexLoaded = !!state.codexHistoryLoaded;
|
|
3737
|
+
var fullyLoaded = loaded && codexLoaded;
|
|
3654
3738
|
var count = (loaded ? visibleHistory.length : 0) + (codexLoaded ? codexVisible.length : 0);
|
|
3739
|
+
if (fullyLoaded && count === 0) return '';
|
|
3655
3740
|
|
|
3656
|
-
var
|
|
3657
|
-
var
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
badgeContent = "0";
|
|
3664
|
-
} else {
|
|
3665
|
-
badgeContent = count > 999 ? "999+" : String(count);
|
|
3666
|
-
}
|
|
3667
|
-
var badge = '<span class="' + badgeCls + '">' + badgeContent + '</span>';
|
|
3668
|
-
|
|
3669
|
-
// Chevron rotates: collapsed → up (▲, suggests "expand upward"),
|
|
3670
|
-
// expanded → down (▼, suggests "collapse downward").
|
|
3671
|
-
var chevronSvg = '<svg class="sidebar-history-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 15 12 9 18 15"/></svg>';
|
|
3672
|
-
|
|
3673
|
-
var headerCls = "sidebar-history-header" + (expanded ? " expanded" : "");
|
|
3674
|
-
var header = '<button type="button" class="' + headerCls + '" id="claude-history-toggle" aria-expanded="' + expanded + '" aria-controls="sidebar-history-body" title="' + (expanded ? "收起历史消息" : "展开历史消息") + '">' +
|
|
3675
|
-
'<span class="sidebar-history-label">历史消息</span>' +
|
|
3676
|
-
'<span class="sidebar-history-right">' + badge + chevronSvg + '</span>' +
|
|
3677
|
-
'</button>';
|
|
3678
|
-
|
|
3741
|
+
var expanded = !!state.claudeHistoryExpanded;
|
|
3742
|
+
var chevron = expanded ? "▾" : "▸";
|
|
3743
|
+
var countContent = fullyLoaded ? (count > 999 ? "999+" : String(count)) : "···";
|
|
3744
|
+
var header = '<div class="session-group-title claude-history-toggle session-history-toggle" id="claude-history-toggle" role="button" tabindex="0" aria-expanded="' + expanded + '" title="' + (expanded ? "收起历史会话" : "展开历史会话") + '">' +
|
|
3745
|
+
'<span class="chevron">' + chevron + '</span> 历史会话 ' +
|
|
3746
|
+
'<span class="history-count' + (fullyLoaded ? '' : ' loading') + '">' + countContent + '</span>' +
|
|
3747
|
+
'</div>';
|
|
3679
3748
|
var body = expanded
|
|
3680
|
-
? '<div class="
|
|
3749
|
+
? '<div class="session-history-body">' + renderClaudeHistoryBodyContent(visibleHistory) + renderCodexHistoryBodyContent(codexVisible) + '</div>'
|
|
3681
3750
|
: '';
|
|
3682
|
-
|
|
3683
|
-
return header + body;
|
|
3751
|
+
return '<section class="session-group session-group--history">' + header + body + '</section>';
|
|
3684
3752
|
}
|
|
3685
3753
|
|
|
3686
3754
|
function renderClaudeHistoryBodyContent(visibleHistory) {
|
|
@@ -3688,7 +3756,9 @@
|
|
|
3688
3756
|
return '<div class="claude-history-loading">扫描历史会话中…</div>';
|
|
3689
3757
|
}
|
|
3690
3758
|
if (visibleHistory.length === 0) {
|
|
3691
|
-
|
|
3759
|
+
// Group is only rendered when there is content somewhere (Claude or
|
|
3760
|
+
// Codex); a Claude-empty/Codex-present case shows just the Codex list.
|
|
3761
|
+
return '';
|
|
3692
3762
|
}
|
|
3693
3763
|
var groups = {};
|
|
3694
3764
|
var groupOrder = [];
|
|
@@ -3714,14 +3784,6 @@
|
|
|
3714
3784
|
return toolbar + '<div class="sidebar-history-scroll">' + listHtml + '</div>';
|
|
3715
3785
|
}
|
|
3716
3786
|
|
|
3717
|
-
// Re-render only the docked history region in place. Called by
|
|
3718
|
-
// updateSessionsList() so existing callers (load complete, delete, etc.)
|
|
3719
|
-
// keep working without changes.
|
|
3720
|
-
function updateClaudeHistoryRegion() {
|
|
3721
|
-
var region = document.getElementById("sidebar-history-region");
|
|
3722
|
-
if (region) region.innerHTML = renderClaudeHistoryRegion();
|
|
3723
|
-
}
|
|
3724
|
-
|
|
3725
3787
|
function getVisibleClaudeHistorySessions() {
|
|
3726
3788
|
var managedIds = new Set();
|
|
3727
3789
|
state.sessions.forEach(function(s) {
|
|
@@ -5535,9 +5597,10 @@
|
|
|
5535
5597
|
var resumeButton = "";
|
|
5536
5598
|
var checkbox = renderManageCheckbox("sessions", session.id, "选择会话 " + session.command);
|
|
5537
5599
|
|
|
5538
|
-
if (session.provider === "claude" && session.claudeSessionId) {
|
|
5600
|
+
if ((session.provider === "claude" || session.provider === "codex") && session.claudeSessionId) {
|
|
5539
5601
|
if (session.status !== "running" && !state.sessionsManageMode && !isStructuredSession(session)) {
|
|
5540
|
-
|
|
5602
|
+
var resumeTitle = session.provider === "codex" ? "恢复 Codex 会话" : "恢复 Claude 会话";
|
|
5603
|
+
resumeButton = '<button class="session-action-btn" data-action="resume" data-session-id="' + session.id + '" type="button" aria-label="' + resumeTitle + '" title="' + resumeTitle + '"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 105.64-11.36L3 10"/></svg></button>';
|
|
5541
5604
|
}
|
|
5542
5605
|
}
|
|
5543
5606
|
|
|
@@ -5556,7 +5619,7 @@
|
|
|
5556
5619
|
// Title: summary or command
|
|
5557
5620
|
var titleHtml = session.summary
|
|
5558
5621
|
? '<div class="session-title">' + escapeHtml(session.summary) + '</div>'
|
|
5559
|
-
: '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>';
|
|
5622
|
+
: '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '').replace(/\s+resume\s+[0-9a-f-]+/, '') : session.command) + '</div>';
|
|
5560
5623
|
|
|
5561
5624
|
// Activity description for running sessions
|
|
5562
5625
|
var activityDesc = getSessionActivityDesc(session);
|
|
@@ -6120,15 +6183,9 @@
|
|
|
6120
6183
|
sessionsList.addEventListener("mouseout", handleCollapsedTileLeave);
|
|
6121
6184
|
initSwipeToDelete(sessionsList);
|
|
6122
6185
|
}
|
|
6123
|
-
//
|
|
6124
|
-
//
|
|
6125
|
-
// expand
|
|
6126
|
-
// same callbacks so behavior stays identical.
|
|
6127
|
-
var historyRegion = document.getElementById("sidebar-history-region");
|
|
6128
|
-
if (historyRegion) {
|
|
6129
|
-
historyRegion.addEventListener("click", handleSessionItemClick);
|
|
6130
|
-
historyRegion.addEventListener("keydown", handleSessionItemKeydown);
|
|
6131
|
-
}
|
|
6186
|
+
// History now renders inline as the final group inside #sessions-list,
|
|
6187
|
+
// so the delegated handlers above already cover its toggle / directory
|
|
6188
|
+
// expand-collapse / item clicks / clear-all — no separate region wiring.
|
|
6132
6189
|
window.addEventListener("scroll", hideCollapsedTileBubble, true);
|
|
6133
6190
|
window.addEventListener("resize", hideCollapsedTileBubble);
|
|
6134
6191
|
|
|
@@ -6191,6 +6248,8 @@
|
|
|
6191
6248
|
if (closeDrawerBtn) closeDrawerBtn.addEventListener("click", closeSessionsDrawer);
|
|
6192
6249
|
var collapseBtn = document.getElementById("sidebar-collapse-btn");
|
|
6193
6250
|
if (collapseBtn) collapseBtn.addEventListener("click", toggleSidebarCollapsed);
|
|
6251
|
+
var pinBtn = document.getElementById("sidebar-pin-btn");
|
|
6252
|
+
if (pinBtn) pinBtn.addEventListener("click", toggleSidebarPin);
|
|
6194
6253
|
var sidebarMoreBtn = document.getElementById("sidebar-more-btn");
|
|
6195
6254
|
var sidebarOverflow = document.getElementById("sidebar-overflow-menu");
|
|
6196
6255
|
if (sidebarMoreBtn && sidebarOverflow) {
|
|
@@ -6672,7 +6731,7 @@
|
|
|
6672
6731
|
topbarMoreBtn.setAttribute("aria-expanded", "false");
|
|
6673
6732
|
switch (action) {
|
|
6674
6733
|
case "copy-claude-session-id":
|
|
6675
|
-
copySelectedSessionField("claudeSessionId", "Claude 会话 ID 已复制");
|
|
6734
|
+
copySelectedSessionField("claudeSessionId", getSelectedSession() && getSelectedSession().provider === "codex" ? "Codex thread ID 已复制" : "Claude 会话 ID 已复制");
|
|
6676
6735
|
break;
|
|
6677
6736
|
case "copy-cwd":
|
|
6678
6737
|
copySelectedSessionField("cwd", "工作目录已复制");
|
|
@@ -7248,6 +7307,12 @@
|
|
|
7248
7307
|
|
|
7249
7308
|
var collapsedTile = target.closest(".sidebar-collapsed-tile");
|
|
7250
7309
|
if (collapsedTile && collapsedTile instanceof HTMLElement) {
|
|
7310
|
+
if (collapsedTile.dataset.collapsedNewSession) {
|
|
7311
|
+
event.preventDefault();
|
|
7312
|
+
event.stopPropagation();
|
|
7313
|
+
openSessionModal();
|
|
7314
|
+
return;
|
|
7315
|
+
}
|
|
7251
7316
|
if (collapsedTile.dataset.collapsedSessionId) {
|
|
7252
7317
|
event.preventDefault();
|
|
7253
7318
|
event.stopPropagation();
|
|
@@ -8576,7 +8641,7 @@
|
|
|
8576
8641
|
state.sessions = [];
|
|
8577
8642
|
state.claudeHistory = [];
|
|
8578
8643
|
state.claudeHistoryLoaded = false;
|
|
8579
|
-
state.claudeHistoryExpanded =
|
|
8644
|
+
state.claudeHistoryExpanded = false;
|
|
8580
8645
|
state.claudeHistoryExpandedDirs = {};
|
|
8581
8646
|
state.sessionsDrawerOpen = false;
|
|
8582
8647
|
render();
|
|
@@ -9513,10 +9578,8 @@
|
|
|
9513
9578
|
var countEl = document.getElementById("session-count");
|
|
9514
9579
|
if (listEl) listEl.innerHTML = renderSessionsListContent();
|
|
9515
9580
|
if (countEl) countEl.textContent = String(state.sessions.length);
|
|
9516
|
-
//
|
|
9517
|
-
//
|
|
9518
|
-
// delete, clear) don't need to know about it.
|
|
9519
|
-
updateClaudeHistoryRegion();
|
|
9581
|
+
// History renders inline inside #sessions-list now, so the line above
|
|
9582
|
+
// already refreshed it — no separate docked region to update.
|
|
9520
9583
|
if (typeof hideCollapsedTileBubble === "function") hideCollapsedTileBubble();
|
|
9521
9584
|
updateShellChrome();
|
|
9522
9585
|
// Re-render cross-session queue (container may have been destroyed by DOM rebuild)
|
|
@@ -9730,7 +9793,7 @@
|
|
|
9730
9793
|
// 与 renderAppShell 保持一致:手机端只允许窄条形态 anchored。
|
|
9731
9794
|
var isMobile = isMobileLayout();
|
|
9732
9795
|
var isCollapsed = !!state.sidebarPinned && !!state.sidebarCollapsed;
|
|
9733
|
-
var isAnchored = isCollapsed || (!!state.sidebarPinned
|
|
9796
|
+
var isAnchored = isCollapsed || (!isMobile && (!!state.sidebarPinned || !!state.sessionsDrawerOpen));
|
|
9734
9797
|
if (drawer) {
|
|
9735
9798
|
drawer.classList.toggle("pinned", isAnchored);
|
|
9736
9799
|
drawer.classList.toggle("collapsed", isCollapsed);
|
|
@@ -9739,6 +9802,13 @@
|
|
|
9739
9802
|
mainLayout.classList.toggle("sidebar-pinned", isAnchored);
|
|
9740
9803
|
mainLayout.classList.toggle("sidebar-collapsed", isCollapsed);
|
|
9741
9804
|
}
|
|
9805
|
+
var pinBtn = document.getElementById("sidebar-pin-btn");
|
|
9806
|
+
if (pinBtn) {
|
|
9807
|
+
pinBtn.classList.toggle("pinned", !!state.sidebarPinned);
|
|
9808
|
+
pinBtn.title = state.sidebarPinned ? "已固定常驻(点击解除锁定)" : "固定侧栏常驻";
|
|
9809
|
+
pinBtn.setAttribute("aria-label", state.sidebarPinned ? "解除固定常驻" : "固定侧栏常驻");
|
|
9810
|
+
pinBtn.setAttribute("aria-pressed", state.sidebarPinned ? "true" : "false");
|
|
9811
|
+
}
|
|
9742
9812
|
}
|
|
9743
9813
|
|
|
9744
9814
|
function updateDrawerState() {
|
|
@@ -9943,6 +10013,26 @@
|
|
|
9943
10013
|
scheduleTerminalRefitAfterPaddingTransition();
|
|
9944
10014
|
}
|
|
9945
10015
|
|
|
10016
|
+
// 常驻(图钉)开关:把侧栏「留在原地」并排停靠 ⟷ 悬浮抽屉。
|
|
10017
|
+
// - 常驻 ON:停靠并推开内容;配合「收起为窄条」可在全尺寸/窄条间切换。
|
|
10018
|
+
// - 常驻 OFF:变为悬浮抽屉,交互时自动收起(非常驻)。
|
|
10019
|
+
// 手机端不支持常驻(屏幕太窄),保持抽屉行为。
|
|
10020
|
+
// 「图钉」只切换「是否锁定常驻」,绝不收起 / 隐藏侧栏——这是用户两次反馈的核心:
|
|
10021
|
+
// 点图钉不该让侧栏消失,也不该把主区压到侧栏下面。
|
|
10022
|
+
// - 锁定(pinned):常驻停靠,不会被「关闭(X)」一键收走的语义区分开。
|
|
10023
|
+
// - 解除锁定(unpinned):仍然停靠可见(因为 isAnchored 把 open 也算停靠),
|
|
10024
|
+
// 只是变成「可被 X 关闭」的临时态。
|
|
10025
|
+
// 全尺寸 / 窄条由「收起为窄条」按钮控制,与图钉正交。
|
|
10026
|
+
function toggleSidebarPin() {
|
|
10027
|
+
if (isMobileLayout()) return;
|
|
10028
|
+
state.sidebarPinned = !state.sidebarPinned;
|
|
10029
|
+
// 关键:保持侧栏可见停靠,无论锁定与否,点图钉都不让它消失。
|
|
10030
|
+
state.sessionsDrawerOpen = true;
|
|
10031
|
+
try { localStorage.setItem("wand-sidebar-pinned", String(state.sidebarPinned)); } catch (e) {}
|
|
10032
|
+
updateLayoutState();
|
|
10033
|
+
scheduleTerminalRefitAfterPaddingTransition();
|
|
10034
|
+
}
|
|
10035
|
+
|
|
9946
10036
|
// 收窄按钮的图标/title/状态随 collapsed 切换。抽出来给轻量更新路径用,
|
|
9947
10037
|
// 避免为了换一个箭头方向就走全量 render()。
|
|
9948
10038
|
function updateSidebarCollapseButton() {
|
|
@@ -12805,7 +12895,7 @@
|
|
|
12805
12895
|
|
|
12806
12896
|
return ensureSessionReadyForInput(selectedSession).then(function(readySession) {
|
|
12807
12897
|
if (!readySession) {
|
|
12808
|
-
// ensureSessionReadyForInput /
|
|
12898
|
+
// ensureSessionReadyForInput / resumeSession 已经在失败路径里
|
|
12809
12899
|
// 自行 toast,这里不再重复提示,避免叠两条消息。
|
|
12810
12900
|
return null;
|
|
12811
12901
|
}
|
|
@@ -13599,14 +13689,14 @@
|
|
|
13599
13689
|
var isCodex = selectedSession && selectedSession.provider === "codex";
|
|
13600
13690
|
if (error && (error.errorCode === "SESSION_NOT_RUNNING" || error.errorCode === "SESSION_NO_PTY")) {
|
|
13601
13691
|
return isCodex
|
|
13602
|
-
? "Codex
|
|
13692
|
+
? "Codex 会话已结束;若存在 Codex 历史会话,将在你下次发送消息时自动恢复。"
|
|
13603
13693
|
: "会话已结束;若存在 Claude 历史会话,将在你下次发送消息时自动恢复。";
|
|
13604
13694
|
}
|
|
13605
13695
|
if (error && error.errorCode === "SESSION_NOT_FOUND") {
|
|
13606
13696
|
return "会话不存在,请重新选择或新建会话。";
|
|
13607
13697
|
}
|
|
13608
13698
|
return (error && error.message) || (isCodex
|
|
13609
|
-
? "Codex
|
|
13699
|
+
? "Codex 会话暂不可用;若存在 Codex 历史会话,将自动尝试恢复。"
|
|
13610
13700
|
: "会话暂不可用;若存在 Claude 历史会话,将自动尝试恢复。");
|
|
13611
13701
|
}
|
|
13612
13702
|
|
|
@@ -13647,10 +13737,10 @@
|
|
|
13647
13737
|
}
|
|
13648
13738
|
|
|
13649
13739
|
function canAutoResumeSession(session) {
|
|
13650
|
-
// 只要是 Claude provider + 非运行中 +
|
|
13740
|
+
// 只要是 Claude/Codex PTY provider + 非运行中 + 有可恢复历史 id,
|
|
13651
13741
|
// 就允许在用户发送时静默触发恢复。不再要求 messages 里同时
|
|
13652
13742
|
// 有 user + assistant 文本(slim 列表/截断历史会让该判断失真)。
|
|
13653
|
-
return !!(session && session.provider === "claude" && session.status !== "running" && session.claudeSessionId);
|
|
13743
|
+
return !!(session && !isStructuredSession(session) && (session.provider === "claude" || session.provider === "codex") && session.status !== "running" && session.claudeSessionId);
|
|
13654
13744
|
}
|
|
13655
13745
|
|
|
13656
13746
|
function ensureSessionReadyForInput(session, errorEl) {
|
|
@@ -13663,12 +13753,13 @@
|
|
|
13663
13753
|
return Promise.resolve(session);
|
|
13664
13754
|
}
|
|
13665
13755
|
if (!canAutoResumeSession(session)) {
|
|
13666
|
-
|
|
13756
|
+
var providerLabel = session && session.provider === "codex" ? "Codex" : "Claude";
|
|
13757
|
+
showToast("该会话没有可恢复的 " + providerLabel + " 历史上下文,请新建会话。", "error");
|
|
13667
13758
|
return Promise.resolve(null);
|
|
13668
13759
|
}
|
|
13669
13760
|
|
|
13670
13761
|
// 静默恢复:不再弹 "正在恢复历史会话…" 提示,让用户发送动作看起来无缝。
|
|
13671
|
-
return
|
|
13762
|
+
return resumeSession(session.id, errorEl).then(function(data) {
|
|
13672
13763
|
if (!data) return null;
|
|
13673
13764
|
updateSessionSnapshot(data);
|
|
13674
13765
|
updateSessionsList();
|
|
@@ -13941,24 +14032,24 @@
|
|
|
13941
14032
|
{ key: "down", label: "↓" },
|
|
13942
14033
|
{ key: "left", label: "←" }
|
|
13943
14034
|
];
|
|
13944
|
-
//
|
|
14035
|
+
// 外圈功能键:N 个均分扇区,i=0 正上方起顺时针。
|
|
14036
|
+
// 数组长度即扇区数 —— buildJoystickRingSvg / joystickHitTest 都是按
|
|
14037
|
+
// OUTER_KEYS.length 动态算角度,所以这里随便加减都不需要改几何。
|
|
14038
|
+
// 当前 4 键: 上=Enter 右=Ctrl+C 下=Esc 左=Shift+Tab。
|
|
14039
|
+
// 选这 4 个的原因: Claude / Codex 交互里只有方向键 + Enter + Esc +
|
|
14040
|
+
// Shift+Tab (back-tab) + Ctrl+C (abort) 有真实用途, 之前的 Tab /
|
|
14041
|
+
// Ctrl+Z/D/L 在结构化 / chat / PTY claude 里都拿不到效果, 留着只是
|
|
14042
|
+
// 占位 + 误点。
|
|
13945
14043
|
var JOYSTICK_OUTER_KEYS = [
|
|
13946
14044
|
{ key: "enter", label: "Enter" },
|
|
13947
|
-
{ key: "escape", label: "Esc" },
|
|
13948
|
-
{ key: "tab", label: "Tab" },
|
|
13949
|
-
{ key: "shift_tab", label: "Shift+Tab" },
|
|
13950
14045
|
{ key: "ctrl_c", label: "Ctrl+C" },
|
|
13951
|
-
{ key: "
|
|
13952
|
-
{ key: "
|
|
13953
|
-
{ key: "ctrl_l", label: "Ctrl+L" }
|
|
13954
|
-
];
|
|
13955
|
-
// 钉住面板四角翻页键
|
|
13956
|
-
var JOYSTICK_CORNER_KEYS = [
|
|
13957
|
-
{ key: "pageup", label: "PgUp" },
|
|
13958
|
-
{ key: "home", label: "Home" },
|
|
13959
|
-
{ key: "pagedown", label: "PgDn" },
|
|
13960
|
-
{ key: "end", label: "End" }
|
|
14046
|
+
{ key: "escape", label: "Esc" },
|
|
14047
|
+
{ key: "shift_tab", label: "Shift+Tab" }
|
|
13961
14048
|
];
|
|
14049
|
+
// 钉住面板四角翻页键 —— 已弃用 (PgUp/Home/PgDn/End 在 Claude TUI 里
|
|
14050
|
+
// 不是常用导航, 也跟终端历史回滚冲突)。留空数组让面板渲染逻辑自然
|
|
14051
|
+
// 跳过这一排, 不删数组以保 ringSvg 之外别处 reference 安全。
|
|
14052
|
+
var JOYSTICK_CORNER_KEYS = [];
|
|
13962
14053
|
|
|
13963
14054
|
var ignoredInteractiveTargetIds = new Set([
|
|
13964
14055
|
"mini-keyboard-fab",
|
|
@@ -14107,7 +14198,7 @@
|
|
|
14107
14198
|
: "Enter 发送 · Shift+Enter 换行";
|
|
14108
14199
|
}
|
|
14109
14200
|
}
|
|
14110
|
-
// 历史会话只要可自动恢复(Claude
|
|
14201
|
+
// 历史会话只要可自动恢复(Claude/Codex PTY + 有历史 id),输入框/发送按钮
|
|
14111
14202
|
// 就保持可用——发送时由 ensureSessionReadyForInput 透明完成恢复。
|
|
14112
14203
|
var canResumeOnSend = !structured && !isRunning && canAutoResumeSession(selectedSession);
|
|
14113
14204
|
if (composer) {
|
|
@@ -14642,9 +14733,15 @@
|
|
|
14642
14733
|
return resumeSession(sessionId).then(function(data) {
|
|
14643
14734
|
if (!data) return null;
|
|
14644
14735
|
if (data.claudeSessionId) {
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14736
|
+
if (data.provider === "codex") {
|
|
14737
|
+
state.codexHistory = state.codexHistory.filter(function(s) {
|
|
14738
|
+
return s.claudeSessionId !== data.claudeSessionId;
|
|
14739
|
+
});
|
|
14740
|
+
} else {
|
|
14741
|
+
state.claudeHistory = state.claudeHistory.filter(function(s) {
|
|
14742
|
+
return s.claudeSessionId !== data.claudeSessionId;
|
|
14743
|
+
});
|
|
14744
|
+
}
|
|
14648
14745
|
}
|
|
14649
14746
|
return activateSession(data).then(function() {
|
|
14650
14747
|
return data;
|
|
@@ -14947,7 +15044,8 @@
|
|
|
14947
15044
|
function handleInputBoxBlur() {
|
|
14948
15045
|
resetInputPanelViewportSpacing();
|
|
14949
15046
|
setTimeout(function() {
|
|
14950
|
-
|
|
15047
|
+
resetRootViewportScroll();
|
|
15048
|
+
syncAppViewportHeight(false);
|
|
14951
15049
|
// On mobile, force terminal refit + scroll after keyboard dismissal.
|
|
14952
15050
|
// The container height restores but terminal needs time to
|
|
14953
15051
|
// fill the expanded space, and the scroll position needs resetting.
|
|
@@ -14959,6 +15057,10 @@
|
|
|
14959
15057
|
maybeScrollTerminalToBottom("keyboard");
|
|
14960
15058
|
}
|
|
14961
15059
|
}, 100);
|
|
15060
|
+
setTimeout(function() {
|
|
15061
|
+
resetRootViewportScroll();
|
|
15062
|
+
syncAppViewportHeight(false);
|
|
15063
|
+
}, 360);
|
|
14962
15064
|
}
|
|
14963
15065
|
|
|
14964
15066
|
function adjustInputBoxSelection(inputBox) {
|
|
@@ -15669,18 +15771,74 @@
|
|
|
15669
15771
|
// adjustResize 不再自动 resize WebView 内容;同时仅给 input-panel
|
|
15670
15772
|
// 加 padding-bottom 只是把 panel 内部底部撑空,并不会让 panel 自身
|
|
15671
15773
|
// 上移。这里通过 CSS 变量驱动整层高度,是跨 WebView/Chrome/PWA 的
|
|
15672
|
-
//
|
|
15673
|
-
//
|
|
15674
|
-
function
|
|
15774
|
+
// 统一兜底。iOS 的 100dvh 在键盘动画后会短暂滞后,所以这里持续用
|
|
15775
|
+
// visualViewport 的实测高度驱动布局,桌面场景下该值基本等于窗口高度。
|
|
15776
|
+
function getRootViewportScrollTop(vv) {
|
|
15777
|
+
var values = [
|
|
15778
|
+
window.scrollY || window.pageYOffset || 0,
|
|
15779
|
+
document.documentElement ? document.documentElement.scrollTop || 0 : 0,
|
|
15780
|
+
document.body ? document.body.scrollTop || 0 : 0
|
|
15781
|
+
];
|
|
15782
|
+
if (vv) {
|
|
15783
|
+
// pageTop is the visual viewport's top edge in layout coordinates.
|
|
15784
|
+
// On iOS it captures the focus pan that can survive keyboard close.
|
|
15785
|
+
if (typeof vv.pageTop === "number") {
|
|
15786
|
+
values.push(vv.pageTop);
|
|
15787
|
+
} else if (typeof vv.offsetTop === "number") {
|
|
15788
|
+
values.push(vv.offsetTop);
|
|
15789
|
+
}
|
|
15790
|
+
}
|
|
15791
|
+
return Math.max.apply(Math, values);
|
|
15792
|
+
}
|
|
15793
|
+
|
|
15794
|
+
function resetRootViewportScroll() {
|
|
15795
|
+
try { window.scrollTo(0, 0); } catch (e) {}
|
|
15796
|
+
if (document.scrollingElement) document.scrollingElement.scrollTop = 0;
|
|
15797
|
+
if (document.documentElement) document.documentElement.scrollTop = 0;
|
|
15798
|
+
if (document.body) document.body.scrollTop = 0;
|
|
15799
|
+
}
|
|
15800
|
+
|
|
15801
|
+
function syncAppViewportHeight(isKeyboardOpen) {
|
|
15675
15802
|
var vv = window.visualViewport;
|
|
15676
15803
|
if (!vv) return;
|
|
15677
|
-
var diff = window.innerHeight - vv.height - vv.offsetTop;
|
|
15678
15804
|
var root = document.documentElement;
|
|
15679
|
-
|
|
15680
|
-
|
|
15681
|
-
|
|
15682
|
-
|
|
15805
|
+
var visualTop = window.__wandImeNative ? 0 : getRootViewportScrollTop(vv);
|
|
15806
|
+
// iOS Safari 上 100dvh 在键盘 / 地址栏切换后有更新延迟, 经常"卡"在
|
|
15807
|
+
// 上一刻的小值 -> body 比真实可见区还短一截, 输入框下方留出一大段
|
|
15808
|
+
// 奶油色 html 背景。改成直接拿 visualViewport.height 当 body 高度的
|
|
15809
|
+
// 权威值, 每帧实时跟随 (vv.resize/scroll 触发), 不再依赖 dvh。
|
|
15810
|
+
// 桌面浏览器上 vv.height ≈ window.innerHeight, 同样无副作用。
|
|
15811
|
+
// 之前的 diff > 50 阈值现在只用来判断"是不是真键盘上来了"以做
|
|
15812
|
+
// iOS html 滚动复位 (offsetTop hack), 不再控制 body 高度。
|
|
15813
|
+
//
|
|
15814
|
+
// 但 iOS 还有一个更隐蔽的状态: 键盘收起后 visual viewport 已经变高,
|
|
15815
|
+
// 根页面却仍停在键盘弹起时的 pageTop/scrollY。此时如果只写 vv.height,
|
|
15816
|
+
// .app-container 的底边落在 visualViewport 顶点之前, input-panel 会悬在
|
|
15817
|
+
// 屏幕底部上方。把 visualTop 临时加回高度, 再滚回 0; 后续 settle timer
|
|
15818
|
+
// 会用新的 visualTop=0 覆盖回来。
|
|
15819
|
+
root.style.setProperty('--app-viewport-height', Math.ceil(vv.height + Math.max(0, visualTop)) + 'px');
|
|
15820
|
+
// iOS Safari: 当 textarea 获得焦点 / 键盘弹起时, 浏览器会主动把
|
|
15821
|
+
// <html> 向上滚一段, 让焦点元素进可见区 —— 体现为 vv.offsetTop > 0。
|
|
15822
|
+
// 但 body 已经被收缩到 vv.height, 这一段 offsetTop 就变成 body 底部
|
|
15823
|
+
// (= .input-panel) 与键盘上沿之间的"空洞", 用户看到的就是
|
|
15824
|
+
// "输入框离键盘还有很远一截"。这里强行把 html 滚回 0, 让 body 底部
|
|
15825
|
+
// 重新贴回键盘上沿。Wand APK 内 (window.__wandImeNative=true) 走
|
|
15826
|
+
// 原生 IME 回调精确 resize, 这里跳过避免双重补偿。
|
|
15827
|
+
if (!window.__wandImeNative && (isKeyboardOpen || visualTop > 1)) {
|
|
15828
|
+
resetRootViewportScroll();
|
|
15829
|
+
}
|
|
15830
|
+
}
|
|
15831
|
+
|
|
15832
|
+
function isEditableFocusTarget(el) {
|
|
15833
|
+
if (!el) return false;
|
|
15834
|
+
var tag = el.tagName;
|
|
15835
|
+
if (tag === "TEXTAREA") return true;
|
|
15836
|
+
if (tag === "SELECT") return true;
|
|
15837
|
+
if (tag === "INPUT") {
|
|
15838
|
+
var type = (el.getAttribute("type") || "text").toLowerCase();
|
|
15839
|
+
return !/^(button|checkbox|color|file|hidden|image|radio|range|reset|submit)$/i.test(type);
|
|
15683
15840
|
}
|
|
15841
|
+
return !!el.isContentEditable;
|
|
15684
15842
|
}
|
|
15685
15843
|
|
|
15686
15844
|
// Visual viewport handling for better mobile keyboard support
|
|
@@ -15690,17 +15848,66 @@
|
|
|
15690
15848
|
var vv = window.visualViewport;
|
|
15691
15849
|
var lastHeight = vv.height;
|
|
15692
15850
|
var keyboardOpen = false;
|
|
15851
|
+
var lastViewportWidth = Math.max(window.innerWidth || 0, vv.width || 0);
|
|
15852
|
+
var largestViewportHeight = Math.max(window.innerHeight || 0, vv.height || 0);
|
|
15853
|
+
var viewportSettleTimers = [];
|
|
15854
|
+
|
|
15855
|
+
function getCurrentViewportHeightBaseline() {
|
|
15856
|
+
return Math.max(window.innerHeight || 0, vv.height || 0);
|
|
15857
|
+
}
|
|
15858
|
+
|
|
15859
|
+
function refreshViewportBaseline() {
|
|
15860
|
+
var width = Math.max(window.innerWidth || 0, vv.width || 0);
|
|
15861
|
+
var height = getCurrentViewportHeightBaseline();
|
|
15862
|
+
if (Math.abs(width - lastViewportWidth) > 8) {
|
|
15863
|
+
lastViewportWidth = width;
|
|
15864
|
+
largestViewportHeight = height;
|
|
15865
|
+
return;
|
|
15866
|
+
}
|
|
15867
|
+
if (height > largestViewportHeight) {
|
|
15868
|
+
largestViewportHeight = height;
|
|
15869
|
+
}
|
|
15870
|
+
}
|
|
15871
|
+
|
|
15872
|
+
function detectKeyboardOpen(inputBox, offsetBottom) {
|
|
15873
|
+
var activeEl = document.activeElement;
|
|
15874
|
+
var hasEditableFocus = activeEl === inputBox || isEditableFocusTarget(activeEl);
|
|
15875
|
+
var shrinkFromLargest = largestViewportHeight - vv.height;
|
|
15876
|
+
var innerShrinkFromLargest = largestViewportHeight - (window.innerHeight || vv.height || 0);
|
|
15877
|
+
if (offsetBottom > 80) return true;
|
|
15878
|
+
// iOS/Chrome iOS sometimes resize window.innerHeight together with
|
|
15879
|
+
// visualViewport.height, so offsetBottom stays near zero. The
|
|
15880
|
+
// focused-editable + baseline shrink path catches that case.
|
|
15881
|
+
if (hasEditableFocus && (shrinkFromLargest > 120 || innerShrinkFromLargest > 120)) return true;
|
|
15882
|
+
// During close animation focus can disappear before viewport height
|
|
15883
|
+
// is fully restored. Keep the "open" state until the shrink is small.
|
|
15884
|
+
if (keyboardOpen && (shrinkFromLargest > 80 || offsetBottom > 32)) return true;
|
|
15885
|
+
return false;
|
|
15886
|
+
}
|
|
15887
|
+
|
|
15888
|
+
function scheduleViewportSettle() {
|
|
15889
|
+
viewportSettleTimers.forEach(function(timer) { clearTimeout(timer); });
|
|
15890
|
+
viewportSettleTimers = [60, 180, 360, 620].map(function(delay) {
|
|
15891
|
+
return setTimeout(function() {
|
|
15892
|
+
if (!window.__wandImeNative) {
|
|
15893
|
+
resetRootViewportScroll();
|
|
15894
|
+
}
|
|
15895
|
+
syncAppViewportHeight(keyboardOpen);
|
|
15896
|
+
}, delay);
|
|
15897
|
+
});
|
|
15898
|
+
}
|
|
15693
15899
|
|
|
15694
15900
|
function updateViewport() {
|
|
15695
15901
|
if (!vv) return;
|
|
15696
15902
|
var inputBox = document.getElementById('input-box');
|
|
15697
15903
|
var offsetBottom = window.innerHeight - vv.height - vv.offsetTop;
|
|
15698
|
-
|
|
15904
|
+
refreshViewportBaseline();
|
|
15905
|
+
var isKeyboardOpen = detectKeyboardOpen(inputBox, offsetBottom);
|
|
15699
15906
|
var heightChanged = Math.abs(vv.height - lastHeight) > 8;
|
|
15700
15907
|
|
|
15701
15908
|
// 键盘开/关与视口尺寸变化时同步 --app-viewport-height,
|
|
15702
15909
|
// 让 body 高度跟随可见区域,input-panel 自然贴键盘上沿。
|
|
15703
|
-
syncAppViewportHeight();
|
|
15910
|
+
syncAppViewportHeight(isKeyboardOpen);
|
|
15704
15911
|
|
|
15705
15912
|
if (isKeyboardOpen && (!keyboardOpen || heightChanged) && shouldAdjustForKeyboard(vv, inputBox)) {
|
|
15706
15913
|
syncInputBoxScroll(inputBox);
|
|
@@ -15720,6 +15927,21 @@
|
|
|
15720
15927
|
// final scroll lands AFTER the animation settles.
|
|
15721
15928
|
var wasStickToBottom = state.terminalAutoFollow || isTerminalNearBottom();
|
|
15722
15929
|
ensureTerminalFit("keyboard-open", { forceReplay: true });
|
|
15930
|
+
// iOS Safari 二次复位: 第一次 syncAppViewportHeight 在键盘动画
|
|
15931
|
+
// 起始帧把 html 滚回 0, 但 iOS 在键盘动画收尾时还会再尝试一次
|
|
15932
|
+
// "把焦点元素拽进可见区", 把 html 重新推上去 —— 留下用户报的
|
|
15933
|
+
// "输入框距离键盘还有很长距离"。镜像 keyboard-close 的 200ms 兜底,
|
|
15934
|
+
// 等键盘动画完整跑完后再清一次 scrollTop + 重算 viewport 高度,
|
|
15935
|
+
// 让 input-panel 最终稳定贴在键盘上沿。
|
|
15936
|
+
// Wand APK (__wandImeNative=true) 跳过, 原生 IME callback 已经在
|
|
15937
|
+
// WebView 层精确 resize, 这里再 scroll 反而抖。
|
|
15938
|
+
if (!window.__wandImeNative) {
|
|
15939
|
+
setTimeout(function() {
|
|
15940
|
+
resetRootViewportScroll();
|
|
15941
|
+
syncAppViewportHeight(true);
|
|
15942
|
+
}, 220);
|
|
15943
|
+
}
|
|
15944
|
+
scheduleViewportSettle();
|
|
15723
15945
|
// Mirror the keyboard-close 200ms delay: by then the iOS / Android
|
|
15724
15946
|
// keyboard slide-in animation is done, vv.height is final, and
|
|
15725
15947
|
// scrollHeight reflects the post-replay grid. One more force
|
|
@@ -15742,8 +15964,8 @@
|
|
|
15742
15964
|
// window.scrollTo(0,0) 不跑,页面停在键盘抬起时被 iOS 推上去的
|
|
15743
15965
|
// 偏移位置,input-panel 看起来"没回到底"。
|
|
15744
15966
|
// 这里在 visualViewport 检测到键盘收起的瞬间直接强制复位一次,
|
|
15745
|
-
// 并把 --app-viewport-height
|
|
15746
|
-
//
|
|
15967
|
+
// 并把 --app-viewport-height 同步到键盘收起后的实测高度,确保
|
|
15968
|
+
// input-panel 重新贴屏幕底部。
|
|
15747
15969
|
//
|
|
15748
15970
|
// Android APK (window.__wandImeNative=true) 跳过这段 iOS hack —
|
|
15749
15971
|
// MainActivity 已经在 IME 动画 callback 里逐帧把 root setPadding,
|
|
@@ -15753,21 +15975,22 @@
|
|
|
15753
15975
|
var rootEl = document.documentElement;
|
|
15754
15976
|
var imeIsNative = !!window.__wandImeNative;
|
|
15755
15977
|
if (!imeIsNative) {
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
15978
|
+
// 不要 removeProperty('--app-viewport-height') —— 那样会让 body 退回
|
|
15979
|
+
// 到 100dvh, 而 iOS Safari 的 100dvh 在键盘动画跑完前经常还停留
|
|
15980
|
+
// 在小值, body 立刻短一截 -> 输入框下方露出大段奶油色 html 背景。
|
|
15981
|
+
// 直接让 syncAppViewportHeight 把它更新为新的 vv.height (键盘已收
|
|
15982
|
+
// 起所以是全可见高度), body 平滑过渡, 不出现空洞。
|
|
15983
|
+
syncAppViewportHeight(false);
|
|
15984
|
+
resetRootViewportScroll();
|
|
15761
15985
|
}
|
|
15986
|
+
scheduleViewportSettle();
|
|
15762
15987
|
setTimeout(function() {
|
|
15763
15988
|
if (!imeIsNative) {
|
|
15764
15989
|
// 二次复位:等键盘收起动画 + iOS 自身的回滚跑完后再清一次,
|
|
15765
15990
|
// 防止 iOS 在动画过程中又把 scrollTop 推上去。
|
|
15766
|
-
|
|
15767
|
-
if (
|
|
15768
|
-
|
|
15769
|
-
if (document.body) document.body.scrollTop = 0;
|
|
15770
|
-
syncAppViewportHeight();
|
|
15991
|
+
resetRootViewportScroll();
|
|
15992
|
+
if (rootEl) rootEl.scrollTop = 0;
|
|
15993
|
+
syncAppViewportHeight(false);
|
|
15771
15994
|
}
|
|
15772
15995
|
ensureTerminalFit("keyboard-close", { forceReplay: true });
|
|
15773
15996
|
// 同 handleInputBoxBlur:尊重 terminalAutoFollow,避免把上滚
|
|
@@ -15883,11 +16106,18 @@
|
|
|
15883
16106
|
// 不改动终端背景的 touch/scroll/wheel —— 单指空白处仍是原生滚动看历史。
|
|
15884
16107
|
|
|
15885
16108
|
function isJoystickAvailable() {
|
|
15886
|
-
// 触屏与桌面网页端都显示(球球用 Pointer Events
|
|
15887
|
-
|
|
16109
|
+
// 触屏与桌面网页端都显示(球球用 Pointer Events,鼠标拖拽同样可用)。
|
|
16110
|
+
// 不再用 currentView/isStructuredSession 关掉:
|
|
16111
|
+
// - chat 视图 (含 PTY Claude 的对话视图): 用户偶尔要给底层 PTY 发
|
|
16112
|
+
// 方向键 / Esc / Shift+Tab 选权限菜单, 但只能切到 terminal 视图才点
|
|
16113
|
+
// 摇杆 —— 现在 chat 视图直接可用。sendJoystickKey 已经走 /input
|
|
16114
|
+
// 接口, 服务端不挑视图。
|
|
16115
|
+
// - 结构化会话: 大多数键 (方向 / Tab) 在 SDK runner 里没真实 effect,
|
|
16116
|
+
// 但 Ctrl+C / Esc 都映射到 query.interrupt() 中断当前回复, 用户
|
|
16117
|
+
// 场景是"等不及当前回答, 想停掉重发"。sendJoystickKey 里按 session
|
|
16118
|
+
// 类型分支处理: PTY 走原本 sequence, 结构化只接受中断意图键。
|
|
15888
16119
|
var session = getSelectedSession();
|
|
15889
16120
|
if (!session) return false;
|
|
15890
|
-
if (isStructuredSession(session)) return false;
|
|
15891
16121
|
return true;
|
|
15892
16122
|
}
|
|
15893
16123
|
|
|
@@ -15934,16 +16164,21 @@
|
|
|
15934
16164
|
for (i = 0; i < JOYSTICK_OUTER_KEYS.length; i++) {
|
|
15935
16165
|
fnRow += keyBtn(JOYSTICK_OUTER_KEYS[i].key, JOYSTICK_OUTER_KEYS[i].label, "");
|
|
15936
16166
|
}
|
|
15937
|
-
|
|
15938
|
-
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
var
|
|
15942
|
-
return '<div class="wjp-title">遥控面板</div>' +
|
|
16167
|
+
// 角键 (PgUp/Home/PgDn/End) 与修饰键 (Ctrl/Alt) 排已被裁剪 ——
|
|
16168
|
+
// 当前外圈 4 键全是独立功能键 (Enter / Ctrl+C / Esc / Shift+Tab),
|
|
16169
|
+
// 没有"先按 Ctrl 再按字母"的复合组合, 所以修饰键 toggle 没意义。
|
|
16170
|
+
// CORNER_KEYS 为空时, 对应的 grid 不渲染, 面板高度自动收缩。
|
|
16171
|
+
var html = '<div class="wjp-title">遥控面板</div>' +
|
|
15943
16172
|
dpad +
|
|
15944
|
-
'<div class="wjp-grid wjp-fnkeys">' + fnRow + "</div>"
|
|
15945
|
-
|
|
15946
|
-
|
|
16173
|
+
'<div class="wjp-grid wjp-fnkeys">' + fnRow + "</div>";
|
|
16174
|
+
if (JOYSTICK_CORNER_KEYS.length > 0) {
|
|
16175
|
+
var cornerRow = "";
|
|
16176
|
+
for (i = 0; i < JOYSTICK_CORNER_KEYS.length; i++) {
|
|
16177
|
+
cornerRow += keyBtn(JOYSTICK_CORNER_KEYS[i].key, JOYSTICK_CORNER_KEYS[i].label, "");
|
|
16178
|
+
}
|
|
16179
|
+
html += '<div class="wjp-grid wjp-corners">' + cornerRow + "</div>";
|
|
16180
|
+
}
|
|
16181
|
+
return html;
|
|
15947
16182
|
}
|
|
15948
16183
|
|
|
15949
16184
|
function joystickPolar(r, deg) {
|
|
@@ -15993,12 +16228,17 @@
|
|
|
15993
16228
|
'<path d="' + joystickSectorPath(JOYSTICK_R0, JOYSTICK_R1, center - 45 + gap, center + 45 - gap) + '"/>' +
|
|
15994
16229
|
joystickLabelMarkup(k.label, lp.x, lp.y) + "</g>";
|
|
15995
16230
|
}
|
|
15996
|
-
|
|
16231
|
+
// 外圈扇区宽度跟随 OUTER_KEYS.length 动态计算: 4 键 → 90° 每片,
|
|
16232
|
+
// 8 键 → 45° 每片。half 是单片半宽 (扇区中心两侧各延半个 step)。
|
|
16233
|
+
var outerCount = JOYSTICK_OUTER_KEYS.length;
|
|
16234
|
+
var outerStep = outerCount > 0 ? 360 / outerCount : 360;
|
|
16235
|
+
var outerHalf = outerStep / 2;
|
|
16236
|
+
for (i = 0; i < outerCount; i++) {
|
|
15997
16237
|
k = JOYSTICK_OUTER_KEYS[i];
|
|
15998
|
-
center = -90 + i *
|
|
16238
|
+
center = -90 + i * outerStep;
|
|
15999
16239
|
lp = joystickPolar((JOYSTICK_R1 + JOYSTICK_R2) / 2, center);
|
|
16000
16240
|
svg += '<g class="wjr-sector wjr-outer" data-key="' + k.key + '">' +
|
|
16001
|
-
'<path d="' + joystickSectorPath(JOYSTICK_R1, JOYSTICK_R2, center -
|
|
16241
|
+
'<path d="' + joystickSectorPath(JOYSTICK_R1, JOYSTICK_R2, center - outerHalf + gap, center + outerHalf - gap) + '"/>' +
|
|
16002
16242
|
joystickLabelMarkup(k.label, lp.x, lp.y) + "</g>";
|
|
16003
16243
|
}
|
|
16004
16244
|
svg += '<circle class="wjr-hub" cx="0" cy="0" r="' + (JOYSTICK_R0 - 1) + '"/>';
|
|
@@ -16248,10 +16488,14 @@
|
|
|
16248
16488
|
if (Math.abs(dy) >= Math.abs(dx)) return { zone: "inner", key: dy < 0 ? "up" : "down" };
|
|
16249
16489
|
return { zone: "inner", key: dx < 0 ? "left" : "right" };
|
|
16250
16490
|
}
|
|
16251
|
-
// 外圈:
|
|
16491
|
+
// 外圈:OUTER_KEYS.length 等分扇区,正上方为 0,顺时针递增;
|
|
16492
|
+
// +halfStep 让扇区中心对准按钮 (原本 N=8 时是 +π/8)。
|
|
16252
16493
|
var ang = Math.atan2(dx, -dy);
|
|
16253
16494
|
if (ang < 0) ang += Math.PI * 2;
|
|
16254
|
-
var
|
|
16495
|
+
var outerCount = JOYSTICK_OUTER_KEYS.length;
|
|
16496
|
+
if (outerCount === 0) return { zone: "dead", key: null };
|
|
16497
|
+
var outerStepRad = (Math.PI * 2) / outerCount;
|
|
16498
|
+
var idx = Math.floor((ang + outerStepRad / 2) / outerStepRad) % outerCount;
|
|
16255
16499
|
return { zone: "outer", key: JOYSTICK_OUTER_KEYS[idx].key };
|
|
16256
16500
|
}
|
|
16257
16501
|
|
|
@@ -16356,6 +16600,24 @@
|
|
|
16356
16600
|
updateJoystickPanelUI();
|
|
16357
16601
|
return;
|
|
16358
16602
|
}
|
|
16603
|
+
var session = getSelectedSession();
|
|
16604
|
+
// ── 结构化会话分支 ──
|
|
16605
|
+
// SDK / claude -p 通道没有 PTY 可写, 把原始 escape 序列丢给
|
|
16606
|
+
// /api/sessions/:id/input 会被结构化 sendMessage 当成对话文本 (例如
|
|
16607
|
+
// 把 "\x1b[A" 作为 prompt 发出去), 既无效又污染上下文。
|
|
16608
|
+
// 这里按"中断意图"白名单转发: Ctrl+C / Esc → query.interrupt()。
|
|
16609
|
+
// 其他键 (方向 / Enter / Shift+Tab) 在结构化里没有合理 mapping, 静默
|
|
16610
|
+
// no-op, 同时震一下做反馈。
|
|
16611
|
+
if (session && isStructuredSession(session)) {
|
|
16612
|
+
if (key === "ctrl_c" || key === "escape") {
|
|
16613
|
+
interruptStructuredSessionFromJoystick(session, key);
|
|
16614
|
+
}
|
|
16615
|
+
// 不论是否真发出去, 都消化掉修饰键 + 更新 UI, 避免下次发送残留状态
|
|
16616
|
+
clearModifiers();
|
|
16617
|
+
updateJoystickPanelUI();
|
|
16618
|
+
return;
|
|
16619
|
+
}
|
|
16620
|
+
// ── PTY 会话原路径 ──
|
|
16359
16621
|
var seq = buildPtySequence(key, {
|
|
16360
16622
|
ctrl: state.modifiers.ctrl,
|
|
16361
16623
|
alt: state.modifiers.alt,
|
|
@@ -16367,6 +16629,44 @@
|
|
|
16367
16629
|
scheduleShortcutResync();
|
|
16368
16630
|
}
|
|
16369
16631
|
|
|
16632
|
+
// 摇杆触发的结构化会话中断: 复用 /api/structured-sessions/:id/messages
|
|
16633
|
+
// 的 interrupt=true 路径 (sendMessage 内部走 query.interrupt 优雅停止,
|
|
16634
|
+
// 失败 fallback 到 abortController.abort)。空 input + interrupt=true =
|
|
16635
|
+
// "停掉当前回复但不发新消息", 跟用户从摇杆按 Ctrl+C/Esc 的预期一致。
|
|
16636
|
+
function interruptStructuredSessionFromJoystick(session, key) {
|
|
16637
|
+
if (!session || !session.id) return;
|
|
16638
|
+
fetch("/api/structured-sessions/" + session.id + "/messages", {
|
|
16639
|
+
method: "POST",
|
|
16640
|
+
headers: { "Content-Type": "application/json" },
|
|
16641
|
+
credentials: "same-origin",
|
|
16642
|
+
body: JSON.stringify({ input: "", interrupt: true, preserveQueue: true }),
|
|
16643
|
+
})
|
|
16644
|
+
.then(function(res) {
|
|
16645
|
+
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(p) {
|
|
16646
|
+
throw new Error((p && p.error) || ("中断失败 (key=" + key + ")"));
|
|
16647
|
+
});
|
|
16648
|
+
return res.json();
|
|
16649
|
+
})
|
|
16650
|
+
.then(function(snapshot) {
|
|
16651
|
+
if (snapshot && snapshot.id) {
|
|
16652
|
+
updateSessionSnapshot(snapshot);
|
|
16653
|
+
if (snapshot.id === state.selectedId) {
|
|
16654
|
+
var refreshed = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
|
|
16655
|
+
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, snapshot.output, false));
|
|
16656
|
+
renderChat(true);
|
|
16657
|
+
if (typeof updateQueueBar === "function") updateQueueBar();
|
|
16658
|
+
}
|
|
16659
|
+
}
|
|
16660
|
+
})
|
|
16661
|
+
.catch(function(err) {
|
|
16662
|
+
// 已经在 SDK 内部完成 / 没有 pending query 时, 服务端会返回 400,
|
|
16663
|
+
// 这里静默吃掉, 避免给用户冒出"中断失败"toast (按了也是想停, 没东西可停就当成功)。
|
|
16664
|
+
if (window && window.console && err && err.message) {
|
|
16665
|
+
console.debug("[wand] joystick interrupt no-op:", err.message);
|
|
16666
|
+
}
|
|
16667
|
+
});
|
|
16668
|
+
}
|
|
16669
|
+
|
|
16370
16670
|
function toggleJoystickPanel() {
|
|
16371
16671
|
if (state.joystickPinnedOpen) closeJoystickPanel();
|
|
16372
16672
|
else openJoystickPanel();
|