@co0ontty/wand 1.37.0 → 1.39.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.
- package/dist/process-manager.d.ts +16 -0
- package/dist/process-manager.js +179 -0
- package/dist/server-session-routes.js +110 -0
- package/dist/structured-session-manager.d.ts +48 -0
- package/dist/structured-session-manager.js +293 -30
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +464 -77
- package/dist/web-ui/content/styles.css +110 -49
- package/package.json +1 -1
|
@@ -370,6 +370,10 @@
|
|
|
370
370
|
sessionsManageMode: false,
|
|
371
371
|
selectedSessionIds: {},
|
|
372
372
|
selectedClaudeHistoryIds: {},
|
|
373
|
+
codexHistory: [],
|
|
374
|
+
codexHistoryLoaded: false,
|
|
375
|
+
codexHistoryExpandedDirs: {},
|
|
376
|
+
selectedCodexHistoryIds: {},
|
|
373
377
|
askUserSelections: {}, // { toolUseId: { 0: [optIdx...], submitted: false } }
|
|
374
378
|
queueEpoch: 0, // Monotonic counter for queue state freshness
|
|
375
379
|
pendingAttachments: [], // [{ file, previewUrl, name, size }]
|
|
@@ -1908,7 +1912,7 @@
|
|
|
1908
1912
|
'</div>' +
|
|
1909
1913
|
'<div class="file-search-box">' +
|
|
1910
1914
|
'<span class="file-search-icon">' + wandFileIcon("search", { size: 14 }) + '</span>' +
|
|
1911
|
-
'<input type="text" id="file-search-input" class="file-search-input" placeholder="搜索当前目录…" autocomplete="off" />' +
|
|
1915
|
+
'<input type="text" id="file-search-input" class="file-search-input" placeholder="搜索当前目录…" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />' +
|
|
1912
1916
|
'<button class="file-search-clear" id="file-search-clear" type="button" aria-label="清除搜索" title="清除">' +
|
|
1913
1917
|
wandFileIcon("x", { size: 13 }) +
|
|
1914
1918
|
'</button>' +
|
|
@@ -2091,7 +2095,7 @@
|
|
|
2091
2095
|
'<div id="folder-breadcrumb" class="folder-breadcrumb"></div>' +
|
|
2092
2096
|
'<div class="folder-picker">' +
|
|
2093
2097
|
'<span class="folder-picker-icon">' + iconSvg("folder", { size: 15, strokeWidth: 1.7 }) + '</span>' +
|
|
2094
|
-
'<input type="text" id="folder-picker-input" class="folder-picker-input" value="" placeholder="输入或选择工作目录..." autocomplete="off" />' +
|
|
2098
|
+
'<input type="text" id="folder-picker-input" class="folder-picker-input" value="" placeholder="输入或选择工作目录..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />' +
|
|
2095
2099
|
'</div>' +
|
|
2096
2100
|
'<div id="folder-picker-dropdown" class="folder-picker-dropdown hidden"></div>' +
|
|
2097
2101
|
'<div id="folder-picker-validation" class="folder-picker-validation"></div>' +
|
|
@@ -2358,6 +2362,10 @@
|
|
|
2358
2362
|
state.quickCommitForm.makeTag = tagCb.checked;
|
|
2359
2363
|
var row = document.getElementById("quick-commit-tag-row");
|
|
2360
2364
|
if (row) row.classList.toggle("hidden", !tagCb.checked);
|
|
2365
|
+
if (tagCb.checked) {
|
|
2366
|
+
var input = document.getElementById("quick-commit-tag");
|
|
2367
|
+
if (input) setTimeout(function() { input.focus(); }, 0);
|
|
2368
|
+
}
|
|
2361
2369
|
});
|
|
2362
2370
|
var tagInput = document.getElementById("quick-commit-tag");
|
|
2363
2371
|
if (tagInput) tagInput.addEventListener("input", function() {
|
|
@@ -2485,9 +2493,8 @@
|
|
|
2485
2493
|
state.quickCommitForm.customMessage = aiMessage;
|
|
2486
2494
|
}
|
|
2487
2495
|
var currentTag = (state.quickCommitForm.tag || "").trim();
|
|
2488
|
-
if (!currentTag && aiTag) {
|
|
2496
|
+
if (state.quickCommitForm.makeTag && !currentTag && aiTag) {
|
|
2489
2497
|
state.quickCommitForm.tag = aiTag;
|
|
2490
|
-
state.quickCommitForm.makeTag = true;
|
|
2491
2498
|
}
|
|
2492
2499
|
})
|
|
2493
2500
|
.catch(function(error) {
|
|
@@ -2848,19 +2855,21 @@
|
|
|
2848
2855
|
'<div class="qc-files-wrap">' + fileRows + '</div>' +
|
|
2849
2856
|
'<div class="qc-message-row" id="quick-commit-message-row">' +
|
|
2850
2857
|
'<div class="qc-message-header"><label class="field-label" for="quick-commit-message">commit message</label>' +
|
|
2851
|
-
'<
|
|
2858
|
+
'<div class="qc-ai-controls">' +
|
|
2859
|
+
'<button type="button" id="quick-commit-ai-btn" class="btn btn-ghost btn-sm"' + (state.quickCommitGenerating ? ' disabled' : '') + '>' + (state.quickCommitGenerating ? '生成中…' : 'AI 生成') + '</button>' +
|
|
2860
|
+
'<label class="qc-ai-tag-toggle" title="开启后,AI 会一并建议 tag,提交时会为本次 commit 打 tag">' +
|
|
2861
|
+
'<span class="qc-ai-tag-label">含 tag</span>' +
|
|
2862
|
+
'<span class="qc-switch qc-switch--compact">' +
|
|
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>' +
|
|
2852
2868
|
'</div>' +
|
|
2853
2869
|
'<textarea id="quick-commit-message" class="field-input" rows="2" placeholder="输入 commit message 或点击 AI 生成"' + (state.quickCommitSubmitting ? ' disabled' : '') + '>' + escapeHtml(f.customMessage || "") + '</textarea>' +
|
|
2854
2870
|
'</div>' +
|
|
2855
|
-
'<div class="qc-checkbox-row">' +
|
|
2856
|
-
'<label class="qc-checkbox-label" for="quick-commit-make-tag">同时为本次 commit 打 tag</label>' +
|
|
2857
|
-
'<label class="qc-switch">' +
|
|
2858
|
-
'<input type="checkbox" id="quick-commit-make-tag" class="switch-toggle"' + (f.makeTag ? ' checked' : '') + '>' +
|
|
2859
|
-
'<span class="switch-slider"></span>' +
|
|
2860
|
-
'</label>' +
|
|
2861
|
-
'</div>' +
|
|
2862
2871
|
'<div class="qc-tag-row' + (f.makeTag ? '' : ' hidden') + '" id="quick-commit-tag-row">' +
|
|
2863
|
-
'<input type="text" id="quick-commit-tag" class="field-input" placeholder="输入 tag
|
|
2872
|
+
'<input type="text" id="quick-commit-tag" class="field-input" placeholder="输入 tag 名称;留空则提交时由 AI 自动生成" value="' + escapeHtml(f.tag || "") + '"' + (state.quickCommitSubmitting ? ' disabled' : '') + '>' +
|
|
2864
2873
|
'</div>' +
|
|
2865
2874
|
(state.quickCommitError ? '<p class="error-message">' + escapeHtml(state.quickCommitError) + '</p>' : '') +
|
|
2866
2875
|
'<div class="qc-section-actions">' +
|
|
@@ -3529,9 +3538,6 @@
|
|
|
3529
3538
|
|
|
3530
3539
|
function renderCollapsedSessionTiles() {
|
|
3531
3540
|
var entries = getRecentEntries();
|
|
3532
|
-
if (entries.length === 0) {
|
|
3533
|
-
return '<div class="sidebar-collapsed-empty" title="无会话">—</div>';
|
|
3534
|
-
}
|
|
3535
3541
|
var tiles = entries.map(function(e, i) {
|
|
3536
3542
|
var idx = i + 1;
|
|
3537
3543
|
if (e.kind === "session") {
|
|
@@ -3545,7 +3551,11 @@
|
|
|
3545
3551
|
var hTitle = preview + " · " + formatHistoryTime(h.timestamp);
|
|
3546
3552
|
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>';
|
|
3547
3553
|
}).join("");
|
|
3548
|
-
|
|
3554
|
+
// 窄条底部固定一个「+」快速新建会话方块,替代被隐藏的 footer 新会话入口。
|
|
3555
|
+
var addTile = '<button class="sidebar-collapsed-tile add" type="button" data-collapsed-new-session="1" title="新建会话" aria-label="新建会话">' +
|
|
3556
|
+
'<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>' +
|
|
3557
|
+
'</button>';
|
|
3558
|
+
return '<div class="sidebar-collapsed-tiles">' + tiles + addTile + '</div>';
|
|
3549
3559
|
}
|
|
3550
3560
|
|
|
3551
3561
|
function renderSessionsListContent() {
|
|
@@ -3640,7 +3650,9 @@
|
|
|
3640
3650
|
var visibleHistory = getClaudeHistoryRegionItems();
|
|
3641
3651
|
var expanded = !!state.claudeHistoryExpanded;
|
|
3642
3652
|
var loaded = !!state.claudeHistoryLoaded;
|
|
3643
|
-
var
|
|
3653
|
+
var codexVisible = getVisibleCodexHistorySessions();
|
|
3654
|
+
var codexLoaded = !!state.codexHistoryLoaded;
|
|
3655
|
+
var count = (loaded ? visibleHistory.length : 0) + (codexLoaded ? codexVisible.length : 0);
|
|
3644
3656
|
|
|
3645
3657
|
var badgeCls = "history-bubble";
|
|
3646
3658
|
var badgeContent;
|
|
@@ -3666,7 +3678,7 @@
|
|
|
3666
3678
|
'</button>';
|
|
3667
3679
|
|
|
3668
3680
|
var body = expanded
|
|
3669
|
-
? '<div class="sidebar-history-body" id="sidebar-history-body">' + renderClaudeHistoryBodyContent(visibleHistory) + '</div>'
|
|
3681
|
+
? '<div class="sidebar-history-body" id="sidebar-history-body">' + renderClaudeHistoryBodyContent(visibleHistory) + renderCodexHistoryBodyContent(codexVisible) + '</div>'
|
|
3670
3682
|
: '';
|
|
3671
3683
|
|
|
3672
3684
|
return header + body;
|
|
@@ -3897,6 +3909,44 @@
|
|
|
3897
3909
|
});
|
|
3898
3910
|
}
|
|
3899
3911
|
|
|
3912
|
+
function renderCodexHistoryDirectoryHeader(cwd, cwdShort, count, isExpanded) {
|
|
3913
|
+
var chevron = isExpanded ? "\u25be" : "\u25b8";
|
|
3914
|
+
return '<div class="claude-history-directory-header codex-history-directory-header" data-action="toggle-codex-history-directory" data-cwd="' + escapeHtml(cwd) + '" role="button" tabindex="0">' +
|
|
3915
|
+
'<div class="session-group-title claude-history-directory-title">' +
|
|
3916
|
+
'<span class="chevron">' + chevron + '</span>' +
|
|
3917
|
+
'<span class="claude-history-directory-label">' + escapeHtml(cwdShort) + ' (' + count + ')</span>' +
|
|
3918
|
+
'</div>' +
|
|
3919
|
+
'</div>';
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
function renderCodexHistoryBodyContent(visibleHistory) {
|
|
3923
|
+
if (!state.codexHistoryLoaded) {
|
|
3924
|
+
return '<div class="claude-history-loading">\u626b\u63cf Codex \u5386\u53f2\u4f1a\u8bdd\u4e2d\u2026</div>';
|
|
3925
|
+
}
|
|
3926
|
+
if (visibleHistory.length === 0) {
|
|
3927
|
+
return '';
|
|
3928
|
+
}
|
|
3929
|
+
var groups = {};
|
|
3930
|
+
var groupOrder = [];
|
|
3931
|
+
visibleHistory.forEach(function(s) {
|
|
3932
|
+
if (!groups[s.cwd]) {
|
|
3933
|
+
groups[s.cwd] = [];
|
|
3934
|
+
groupOrder.push(s.cwd);
|
|
3935
|
+
}
|
|
3936
|
+
groups[s.cwd].push(s);
|
|
3937
|
+
});
|
|
3938
|
+
var listHtml = '<div class="sidebar-history-section-label">Codex</div>';
|
|
3939
|
+
groupOrder.forEach(function(cwd) {
|
|
3940
|
+
var cwdShort = cwd.split("/").filter(Boolean).slice(-3).join("/");
|
|
3941
|
+
var isDirExpanded = !!state.codexHistoryExpandedDirs[cwd];
|
|
3942
|
+
listHtml += renderCodexHistoryDirectoryHeader(cwd, cwdShort, groups[cwd].length, isDirExpanded);
|
|
3943
|
+
if (isDirExpanded) {
|
|
3944
|
+
listHtml += groups[cwd].map(function(session) { return renderClaudeHistoryItem(session, "codex"); }).join("");
|
|
3945
|
+
}
|
|
3946
|
+
});
|
|
3947
|
+
return '<div class="sidebar-history-scroll codex-history-scroll">' + listHtml + '</div>';
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3900
3950
|
function renderClaudeHistoryDirectoryHeader(cwd, cwdShort, count, isExpanded) {
|
|
3901
3951
|
var chevron = isExpanded ? "▾" : "▸";
|
|
3902
3952
|
return '<div class="claude-history-directory-header" data-action="toggle-history-directory" data-cwd="' + escapeHtml(cwd) + '" role="button" tabindex="0">' +
|
|
@@ -3910,19 +3960,23 @@
|
|
|
3910
3960
|
}
|
|
3911
3961
|
|
|
3912
3962
|
function renderClaudeHistoryItem(session, kind) {
|
|
3963
|
+
var isCodex = kind === "codex";
|
|
3964
|
+
var rAct = isCodex ? "resume-codex-history" : "resume-history";
|
|
3965
|
+
var dAct = isCodex ? "delete-codex-history" : "delete-history";
|
|
3966
|
+
var selMap = isCodex ? state.selectedCodexHistoryIds : state.selectedClaudeHistoryIds;
|
|
3913
3967
|
var shortId = session.claudeSessionId.slice(0, 8);
|
|
3914
3968
|
var preview = session.firstUserMessage || "(空会话)";
|
|
3915
3969
|
var timeStr = formatHistoryTime(session.timestamp);
|
|
3916
3970
|
var checkbox = renderManageCheckbox(kind, session.claudeSessionId, "选择历史会话 " + preview);
|
|
3917
3971
|
var deleteButton = state.sessionsManageMode ? '' :
|
|
3918
|
-
'<button class="session-action-btn delete-btn" data-action="
|
|
3972
|
+
'<button class="session-action-btn delete-btn" data-action="' + dAct + '" data-claude-session-id="' +
|
|
3919
3973
|
session.claudeSessionId + '" type="button" aria-label="删除会话" title="隐藏此历史会话"><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="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>';
|
|
3920
3974
|
var resumeButton = state.sessionsManageMode ? '' :
|
|
3921
|
-
'<button class="session-action-btn" data-action="
|
|
3975
|
+
'<button class="session-action-btn" data-action="' + rAct + '" data-claude-session-id="' +
|
|
3922
3976
|
session.claudeSessionId + '" data-cwd="' + escapeHtml(session.cwd) +
|
|
3923
|
-
'" type="button" aria-label="恢复会话" title="恢复此 Claude 历史会话"><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>';
|
|
3977
|
+
'" type="button" aria-label="恢复会话" title="' + (isCodex ? "恢复此 Codex 历史会话" : "恢复此 Claude 历史会话") + '"><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>';
|
|
3924
3978
|
|
|
3925
|
-
return '<div class="session-item claude-history-item' + (state.sessionsManageMode &&
|
|
3979
|
+
return '<div class="session-item claude-history-item' + (state.sessionsManageMode && selMap[session.claudeSessionId] ? ' selected' : '') + '" data-claude-history-id="' + session.claudeSessionId + '" data-cwd="' + escapeHtml(session.cwd) + '" role="button" tabindex="0">' +
|
|
3926
3980
|
'<div class="session-item-content">' +
|
|
3927
3981
|
'<div class="session-item-row">' +
|
|
3928
3982
|
checkbox +
|
|
@@ -3980,6 +4034,7 @@
|
|
|
3980
4034
|
// 且在已加载时立即 resolve。
|
|
3981
4035
|
var _claudeHistoryLoadingPromise = null;
|
|
3982
4036
|
function ensureClaudeHistoryLoaded() {
|
|
4037
|
+
ensureCodexHistoryLoaded();
|
|
3983
4038
|
if (state.claudeHistoryLoaded) return Promise.resolve();
|
|
3984
4039
|
if (_claudeHistoryLoadingPromise) return _claudeHistoryLoadingPromise;
|
|
3985
4040
|
_claudeHistoryLoadingPromise = loadClaudeHistory().then(function() {
|
|
@@ -3990,6 +4045,46 @@
|
|
|
3990
4045
|
return _claudeHistoryLoadingPromise;
|
|
3991
4046
|
}
|
|
3992
4047
|
|
|
4048
|
+
function loadCodexHistory() {
|
|
4049
|
+
return fetch("/api/codex-history", { credentials: "same-origin" })
|
|
4050
|
+
.then(function(res) {
|
|
4051
|
+
if (!res.ok) return [];
|
|
4052
|
+
return res.json();
|
|
4053
|
+
})
|
|
4054
|
+
.then(function(sessions) {
|
|
4055
|
+
state.codexHistory = sessions || [];
|
|
4056
|
+
state.codexHistoryLoaded = true;
|
|
4057
|
+
updateSessionsList();
|
|
4058
|
+
})
|
|
4059
|
+
.catch(function() {
|
|
4060
|
+
state.codexHistoryLoaded = true;
|
|
4061
|
+
state.codexHistory = [];
|
|
4062
|
+
updateSessionsList();
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
var _codexHistoryLoadingPromise = null;
|
|
4067
|
+
function ensureCodexHistoryLoaded() {
|
|
4068
|
+
if (state.codexHistoryLoaded) return Promise.resolve();
|
|
4069
|
+
if (_codexHistoryLoadingPromise) return _codexHistoryLoadingPromise;
|
|
4070
|
+
_codexHistoryLoadingPromise = loadCodexHistory().then(function() {
|
|
4071
|
+
_codexHistoryLoadingPromise = null;
|
|
4072
|
+
}, function() {
|
|
4073
|
+
_codexHistoryLoadingPromise = null;
|
|
4074
|
+
});
|
|
4075
|
+
return _codexHistoryLoadingPromise;
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
function getVisibleCodexHistorySessions() {
|
|
4079
|
+
var managedIds = new Set();
|
|
4080
|
+
state.sessions.forEach(function(s) {
|
|
4081
|
+
if (s.claudeSessionId) managedIds.add(s.claudeSessionId);
|
|
4082
|
+
});
|
|
4083
|
+
return state.codexHistory.filter(function(s) {
|
|
4084
|
+
return s.hasConversation && !s.managedByWand && !managedIds.has(s.claudeSessionId);
|
|
4085
|
+
});
|
|
4086
|
+
}
|
|
4087
|
+
|
|
3993
4088
|
function isMobileLayout() {
|
|
3994
4089
|
return window.innerWidth <= 768;
|
|
3995
4090
|
}
|
|
@@ -7154,6 +7249,12 @@
|
|
|
7154
7249
|
|
|
7155
7250
|
var collapsedTile = target.closest(".sidebar-collapsed-tile");
|
|
7156
7251
|
if (collapsedTile && collapsedTile instanceof HTMLElement) {
|
|
7252
|
+
if (collapsedTile.dataset.collapsedNewSession) {
|
|
7253
|
+
event.preventDefault();
|
|
7254
|
+
event.stopPropagation();
|
|
7255
|
+
openSessionModal();
|
|
7256
|
+
return;
|
|
7257
|
+
}
|
|
7157
7258
|
if (collapsedTile.dataset.collapsedSessionId) {
|
|
7158
7259
|
event.preventDefault();
|
|
7159
7260
|
event.stopPropagation();
|
|
@@ -7244,6 +7345,14 @@
|
|
|
7244
7345
|
handleResumeAction(actionButton);
|
|
7245
7346
|
} else if (actionButton.dataset.action === "resume-history" && actionButton.dataset.claudeSessionId) {
|
|
7246
7347
|
handleResumeHistoryAction(actionButton);
|
|
7348
|
+
} else if (actionButton.dataset.action === "resume-codex-history" && actionButton.dataset.claudeSessionId) {
|
|
7349
|
+
handleResumeCodexHistoryAction(actionButton);
|
|
7350
|
+
} else if (actionButton.dataset.action === "delete-codex-history" && actionButton.dataset.claudeSessionId) {
|
|
7351
|
+
handleDeleteCodexHistoryAction(actionButton);
|
|
7352
|
+
} else if (actionButton.dataset.action === "toggle-codex-history-directory" && actionButton.dataset.cwd) {
|
|
7353
|
+
var codexDirCwd = actionButton.dataset.cwd;
|
|
7354
|
+
state.codexHistoryExpandedDirs[codexDirCwd] = !state.codexHistoryExpandedDirs[codexDirCwd];
|
|
7355
|
+
updateSessionsList();
|
|
7247
7356
|
} else if (actionButton.dataset.action === "worktree-merge" && actionButton.dataset.sessionId) {
|
|
7248
7357
|
openWorktreeMergeModal(actionButton.dataset.sessionId);
|
|
7249
7358
|
} else if (actionButton.dataset.action === "worktree-cleanup" && actionButton.dataset.sessionId) {
|
|
@@ -13839,24 +13948,24 @@
|
|
|
13839
13948
|
{ key: "down", label: "↓" },
|
|
13840
13949
|
{ key: "left", label: "←" }
|
|
13841
13950
|
];
|
|
13842
|
-
//
|
|
13951
|
+
// 外圈功能键:N 个均分扇区,i=0 正上方起顺时针。
|
|
13952
|
+
// 数组长度即扇区数 —— buildJoystickRingSvg / joystickHitTest 都是按
|
|
13953
|
+
// OUTER_KEYS.length 动态算角度,所以这里随便加减都不需要改几何。
|
|
13954
|
+
// 当前 4 键: 上=Enter 右=Ctrl+C 下=Esc 左=Shift+Tab。
|
|
13955
|
+
// 选这 4 个的原因: Claude / Codex 交互里只有方向键 + Enter + Esc +
|
|
13956
|
+
// Shift+Tab (back-tab) + Ctrl+C (abort) 有真实用途, 之前的 Tab /
|
|
13957
|
+
// Ctrl+Z/D/L 在结构化 / chat / PTY claude 里都拿不到效果, 留着只是
|
|
13958
|
+
// 占位 + 误点。
|
|
13843
13959
|
var JOYSTICK_OUTER_KEYS = [
|
|
13844
13960
|
{ key: "enter", label: "Enter" },
|
|
13845
|
-
{ key: "escape", label: "Esc" },
|
|
13846
|
-
{ key: "tab", label: "Tab" },
|
|
13847
|
-
{ key: "shift_tab", label: "Shift+Tab" },
|
|
13848
13961
|
{ key: "ctrl_c", label: "Ctrl+C" },
|
|
13849
|
-
{ key: "
|
|
13850
|
-
{ key: "
|
|
13851
|
-
{ key: "ctrl_l", label: "Ctrl+L" }
|
|
13852
|
-
];
|
|
13853
|
-
// 钉住面板四角翻页键
|
|
13854
|
-
var JOYSTICK_CORNER_KEYS = [
|
|
13855
|
-
{ key: "pageup", label: "PgUp" },
|
|
13856
|
-
{ key: "home", label: "Home" },
|
|
13857
|
-
{ key: "pagedown", label: "PgDn" },
|
|
13858
|
-
{ key: "end", label: "End" }
|
|
13962
|
+
{ key: "escape", label: "Esc" },
|
|
13963
|
+
{ key: "shift_tab", label: "Shift+Tab" }
|
|
13859
13964
|
];
|
|
13965
|
+
// 钉住面板四角翻页键 —— 已弃用 (PgUp/Home/PgDn/End 在 Claude TUI 里
|
|
13966
|
+
// 不是常用导航, 也跟终端历史回滚冲突)。留空数组让面板渲染逻辑自然
|
|
13967
|
+
// 跳过这一排, 不删数组以保 ringSvg 之外别处 reference 安全。
|
|
13968
|
+
var JOYSTICK_CORNER_KEYS = [];
|
|
13860
13969
|
|
|
13861
13970
|
var ignoredInteractiveTargetIds = new Set([
|
|
13862
13971
|
"mini-keyboard-fab",
|
|
@@ -14644,6 +14753,81 @@
|
|
|
14644
14753
|
});
|
|
14645
14754
|
}
|
|
14646
14755
|
|
|
14756
|
+
function handleResumeCodexHistoryAction(actionButton) {
|
|
14757
|
+
var threadId = actionButton.dataset.claudeSessionId;
|
|
14758
|
+
var cwd = actionButton.dataset.cwd;
|
|
14759
|
+
console.log("[WAND] handleResumeCodexHistoryAction threadId:", threadId, "cwd:", cwd);
|
|
14760
|
+
if (!threadId) return;
|
|
14761
|
+
actionButton.disabled = true;
|
|
14762
|
+
resumeCodexHistorySession(threadId, cwd)
|
|
14763
|
+
.then(function(data) {
|
|
14764
|
+
if (data && data.id) {
|
|
14765
|
+
state.codexHistory = state.codexHistory.filter(function(s) {
|
|
14766
|
+
return s.claudeSessionId !== threadId;
|
|
14767
|
+
});
|
|
14768
|
+
state.selectedId = data.id;
|
|
14769
|
+
persistSelectedId();
|
|
14770
|
+
state.drafts[data.id] = "";
|
|
14771
|
+
activateSession(data).then(function() {
|
|
14772
|
+
dismissDrawerIfOverlay();
|
|
14773
|
+
});
|
|
14774
|
+
}
|
|
14775
|
+
})
|
|
14776
|
+
.finally(function() {
|
|
14777
|
+
actionButton.disabled = false;
|
|
14778
|
+
});
|
|
14779
|
+
}
|
|
14780
|
+
|
|
14781
|
+
function resumeCodexHistorySession(threadId, cwd) {
|
|
14782
|
+
return fetch("/api/codex-sessions/" + encodeURIComponent(threadId) + "/resume", {
|
|
14783
|
+
method: "POST",
|
|
14784
|
+
headers: { "Content-Type": "application/json" },
|
|
14785
|
+
credentials: "same-origin",
|
|
14786
|
+
body: JSON.stringify(withTerminalDimensions({
|
|
14787
|
+
mode: state.chatMode || (state.config && state.config.defaultMode) || "default",
|
|
14788
|
+
cwd: cwd
|
|
14789
|
+
}))
|
|
14790
|
+
})
|
|
14791
|
+
.then(function(res) { return res.json(); })
|
|
14792
|
+
.then(function(data) {
|
|
14793
|
+
if (data.error) {
|
|
14794
|
+
showToast(data.error, "error");
|
|
14795
|
+
return null;
|
|
14796
|
+
}
|
|
14797
|
+
return data;
|
|
14798
|
+
})
|
|
14799
|
+
.catch(function(error) {
|
|
14800
|
+
showToast((error && error.message) || "无法恢复历史会话。", "error");
|
|
14801
|
+
return null;
|
|
14802
|
+
});
|
|
14803
|
+
}
|
|
14804
|
+
|
|
14805
|
+
function handleDeleteCodexHistoryAction(actionButton) {
|
|
14806
|
+
var threadId = actionButton.dataset.claudeSessionId;
|
|
14807
|
+
if (!threadId) return;
|
|
14808
|
+
console.log("[WAND] handleDeleteCodexHistoryAction threadId:", threadId);
|
|
14809
|
+
var item = actionButton.closest(".claude-history-item");
|
|
14810
|
+
if (item) item.style.opacity = "0.5";
|
|
14811
|
+
fetch("/api/codex-history/" + encodeURIComponent(threadId), {
|
|
14812
|
+
method: "DELETE",
|
|
14813
|
+
credentials: "same-origin"
|
|
14814
|
+
})
|
|
14815
|
+
.then(function(res) { return res.json(); })
|
|
14816
|
+
.then(function(data) {
|
|
14817
|
+
if (data && data.ok) {
|
|
14818
|
+
state.codexHistory = state.codexHistory.filter(function(s) {
|
|
14819
|
+
return s.claudeSessionId !== threadId;
|
|
14820
|
+
});
|
|
14821
|
+
updateSessionsList();
|
|
14822
|
+
} else if (item) {
|
|
14823
|
+
item.style.opacity = "1";
|
|
14824
|
+
}
|
|
14825
|
+
})
|
|
14826
|
+
.catch(function() {
|
|
14827
|
+
if (item) item.style.opacity = "1";
|
|
14828
|
+
});
|
|
14829
|
+
}
|
|
14830
|
+
|
|
14647
14831
|
function handleResumeHistoryAction(actionButton) {
|
|
14648
14832
|
var claudeSessionId = actionButton.dataset.claudeSessionId;
|
|
14649
14833
|
var cwd = actionButton.dataset.cwd;
|
|
@@ -14770,7 +14954,8 @@
|
|
|
14770
14954
|
function handleInputBoxBlur() {
|
|
14771
14955
|
resetInputPanelViewportSpacing();
|
|
14772
14956
|
setTimeout(function() {
|
|
14773
|
-
|
|
14957
|
+
resetRootViewportScroll();
|
|
14958
|
+
syncAppViewportHeight(false);
|
|
14774
14959
|
// On mobile, force terminal refit + scroll after keyboard dismissal.
|
|
14775
14960
|
// The container height restores but terminal needs time to
|
|
14776
14961
|
// fill the expanded space, and the scroll position needs resetting.
|
|
@@ -14782,6 +14967,10 @@
|
|
|
14782
14967
|
maybeScrollTerminalToBottom("keyboard");
|
|
14783
14968
|
}
|
|
14784
14969
|
}, 100);
|
|
14970
|
+
setTimeout(function() {
|
|
14971
|
+
resetRootViewportScroll();
|
|
14972
|
+
syncAppViewportHeight(false);
|
|
14973
|
+
}, 360);
|
|
14785
14974
|
}
|
|
14786
14975
|
|
|
14787
14976
|
function adjustInputBoxSelection(inputBox) {
|
|
@@ -15492,18 +15681,74 @@
|
|
|
15492
15681
|
// adjustResize 不再自动 resize WebView 内容;同时仅给 input-panel
|
|
15493
15682
|
// 加 padding-bottom 只是把 panel 内部底部撑空,并不会让 panel 自身
|
|
15494
15683
|
// 上移。这里通过 CSS 变量驱动整层高度,是跨 WebView/Chrome/PWA 的
|
|
15495
|
-
//
|
|
15496
|
-
//
|
|
15497
|
-
function
|
|
15684
|
+
// 统一兜底。iOS 的 100dvh 在键盘动画后会短暂滞后,所以这里持续用
|
|
15685
|
+
// visualViewport 的实测高度驱动布局,桌面场景下该值基本等于窗口高度。
|
|
15686
|
+
function getRootViewportScrollTop(vv) {
|
|
15687
|
+
var values = [
|
|
15688
|
+
window.scrollY || window.pageYOffset || 0,
|
|
15689
|
+
document.documentElement ? document.documentElement.scrollTop || 0 : 0,
|
|
15690
|
+
document.body ? document.body.scrollTop || 0 : 0
|
|
15691
|
+
];
|
|
15692
|
+
if (vv) {
|
|
15693
|
+
// pageTop is the visual viewport's top edge in layout coordinates.
|
|
15694
|
+
// On iOS it captures the focus pan that can survive keyboard close.
|
|
15695
|
+
if (typeof vv.pageTop === "number") {
|
|
15696
|
+
values.push(vv.pageTop);
|
|
15697
|
+
} else if (typeof vv.offsetTop === "number") {
|
|
15698
|
+
values.push(vv.offsetTop);
|
|
15699
|
+
}
|
|
15700
|
+
}
|
|
15701
|
+
return Math.max.apply(Math, values);
|
|
15702
|
+
}
|
|
15703
|
+
|
|
15704
|
+
function resetRootViewportScroll() {
|
|
15705
|
+
try { window.scrollTo(0, 0); } catch (e) {}
|
|
15706
|
+
if (document.scrollingElement) document.scrollingElement.scrollTop = 0;
|
|
15707
|
+
if (document.documentElement) document.documentElement.scrollTop = 0;
|
|
15708
|
+
if (document.body) document.body.scrollTop = 0;
|
|
15709
|
+
}
|
|
15710
|
+
|
|
15711
|
+
function syncAppViewportHeight(isKeyboardOpen) {
|
|
15498
15712
|
var vv = window.visualViewport;
|
|
15499
15713
|
if (!vv) return;
|
|
15500
|
-
var diff = window.innerHeight - vv.height - vv.offsetTop;
|
|
15501
15714
|
var root = document.documentElement;
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
|
|
15505
|
-
|
|
15715
|
+
var visualTop = window.__wandImeNative ? 0 : getRootViewportScrollTop(vv);
|
|
15716
|
+
// iOS Safari 上 100dvh 在键盘 / 地址栏切换后有更新延迟, 经常"卡"在
|
|
15717
|
+
// 上一刻的小值 -> body 比真实可见区还短一截, 输入框下方留出一大段
|
|
15718
|
+
// 奶油色 html 背景。改成直接拿 visualViewport.height 当 body 高度的
|
|
15719
|
+
// 权威值, 每帧实时跟随 (vv.resize/scroll 触发), 不再依赖 dvh。
|
|
15720
|
+
// 桌面浏览器上 vv.height ≈ window.innerHeight, 同样无副作用。
|
|
15721
|
+
// 之前的 diff > 50 阈值现在只用来判断"是不是真键盘上来了"以做
|
|
15722
|
+
// iOS html 滚动复位 (offsetTop hack), 不再控制 body 高度。
|
|
15723
|
+
//
|
|
15724
|
+
// 但 iOS 还有一个更隐蔽的状态: 键盘收起后 visual viewport 已经变高,
|
|
15725
|
+
// 根页面却仍停在键盘弹起时的 pageTop/scrollY。此时如果只写 vv.height,
|
|
15726
|
+
// .app-container 的底边落在 visualViewport 顶点之前, input-panel 会悬在
|
|
15727
|
+
// 屏幕底部上方。把 visualTop 临时加回高度, 再滚回 0; 后续 settle timer
|
|
15728
|
+
// 会用新的 visualTop=0 覆盖回来。
|
|
15729
|
+
root.style.setProperty('--app-viewport-height', Math.ceil(vv.height + Math.max(0, visualTop)) + 'px');
|
|
15730
|
+
// iOS Safari: 当 textarea 获得焦点 / 键盘弹起时, 浏览器会主动把
|
|
15731
|
+
// <html> 向上滚一段, 让焦点元素进可见区 —— 体现为 vv.offsetTop > 0。
|
|
15732
|
+
// 但 body 已经被收缩到 vv.height, 这一段 offsetTop 就变成 body 底部
|
|
15733
|
+
// (= .input-panel) 与键盘上沿之间的"空洞", 用户看到的就是
|
|
15734
|
+
// "输入框离键盘还有很远一截"。这里强行把 html 滚回 0, 让 body 底部
|
|
15735
|
+
// 重新贴回键盘上沿。Wand APK 内 (window.__wandImeNative=true) 走
|
|
15736
|
+
// 原生 IME 回调精确 resize, 这里跳过避免双重补偿。
|
|
15737
|
+
if (!window.__wandImeNative && (isKeyboardOpen || visualTop > 1)) {
|
|
15738
|
+
resetRootViewportScroll();
|
|
15739
|
+
}
|
|
15740
|
+
}
|
|
15741
|
+
|
|
15742
|
+
function isEditableFocusTarget(el) {
|
|
15743
|
+
if (!el) return false;
|
|
15744
|
+
var tag = el.tagName;
|
|
15745
|
+
if (tag === "TEXTAREA") return true;
|
|
15746
|
+
if (tag === "SELECT") return true;
|
|
15747
|
+
if (tag === "INPUT") {
|
|
15748
|
+
var type = (el.getAttribute("type") || "text").toLowerCase();
|
|
15749
|
+
return !/^(button|checkbox|color|file|hidden|image|radio|range|reset|submit)$/i.test(type);
|
|
15506
15750
|
}
|
|
15751
|
+
return !!el.isContentEditable;
|
|
15507
15752
|
}
|
|
15508
15753
|
|
|
15509
15754
|
// Visual viewport handling for better mobile keyboard support
|
|
@@ -15513,17 +15758,66 @@
|
|
|
15513
15758
|
var vv = window.visualViewport;
|
|
15514
15759
|
var lastHeight = vv.height;
|
|
15515
15760
|
var keyboardOpen = false;
|
|
15761
|
+
var lastViewportWidth = Math.max(window.innerWidth || 0, vv.width || 0);
|
|
15762
|
+
var largestViewportHeight = Math.max(window.innerHeight || 0, vv.height || 0);
|
|
15763
|
+
var viewportSettleTimers = [];
|
|
15764
|
+
|
|
15765
|
+
function getCurrentViewportHeightBaseline() {
|
|
15766
|
+
return Math.max(window.innerHeight || 0, vv.height || 0);
|
|
15767
|
+
}
|
|
15768
|
+
|
|
15769
|
+
function refreshViewportBaseline() {
|
|
15770
|
+
var width = Math.max(window.innerWidth || 0, vv.width || 0);
|
|
15771
|
+
var height = getCurrentViewportHeightBaseline();
|
|
15772
|
+
if (Math.abs(width - lastViewportWidth) > 8) {
|
|
15773
|
+
lastViewportWidth = width;
|
|
15774
|
+
largestViewportHeight = height;
|
|
15775
|
+
return;
|
|
15776
|
+
}
|
|
15777
|
+
if (height > largestViewportHeight) {
|
|
15778
|
+
largestViewportHeight = height;
|
|
15779
|
+
}
|
|
15780
|
+
}
|
|
15781
|
+
|
|
15782
|
+
function detectKeyboardOpen(inputBox, offsetBottom) {
|
|
15783
|
+
var activeEl = document.activeElement;
|
|
15784
|
+
var hasEditableFocus = activeEl === inputBox || isEditableFocusTarget(activeEl);
|
|
15785
|
+
var shrinkFromLargest = largestViewportHeight - vv.height;
|
|
15786
|
+
var innerShrinkFromLargest = largestViewportHeight - (window.innerHeight || vv.height || 0);
|
|
15787
|
+
if (offsetBottom > 80) return true;
|
|
15788
|
+
// iOS/Chrome iOS sometimes resize window.innerHeight together with
|
|
15789
|
+
// visualViewport.height, so offsetBottom stays near zero. The
|
|
15790
|
+
// focused-editable + baseline shrink path catches that case.
|
|
15791
|
+
if (hasEditableFocus && (shrinkFromLargest > 120 || innerShrinkFromLargest > 120)) return true;
|
|
15792
|
+
// During close animation focus can disappear before viewport height
|
|
15793
|
+
// is fully restored. Keep the "open" state until the shrink is small.
|
|
15794
|
+
if (keyboardOpen && (shrinkFromLargest > 80 || offsetBottom > 32)) return true;
|
|
15795
|
+
return false;
|
|
15796
|
+
}
|
|
15797
|
+
|
|
15798
|
+
function scheduleViewportSettle() {
|
|
15799
|
+
viewportSettleTimers.forEach(function(timer) { clearTimeout(timer); });
|
|
15800
|
+
viewportSettleTimers = [60, 180, 360, 620].map(function(delay) {
|
|
15801
|
+
return setTimeout(function() {
|
|
15802
|
+
if (!window.__wandImeNative) {
|
|
15803
|
+
resetRootViewportScroll();
|
|
15804
|
+
}
|
|
15805
|
+
syncAppViewportHeight(keyboardOpen);
|
|
15806
|
+
}, delay);
|
|
15807
|
+
});
|
|
15808
|
+
}
|
|
15516
15809
|
|
|
15517
15810
|
function updateViewport() {
|
|
15518
15811
|
if (!vv) return;
|
|
15519
15812
|
var inputBox = document.getElementById('input-box');
|
|
15520
15813
|
var offsetBottom = window.innerHeight - vv.height - vv.offsetTop;
|
|
15521
|
-
|
|
15814
|
+
refreshViewportBaseline();
|
|
15815
|
+
var isKeyboardOpen = detectKeyboardOpen(inputBox, offsetBottom);
|
|
15522
15816
|
var heightChanged = Math.abs(vv.height - lastHeight) > 8;
|
|
15523
15817
|
|
|
15524
15818
|
// 键盘开/关与视口尺寸变化时同步 --app-viewport-height,
|
|
15525
15819
|
// 让 body 高度跟随可见区域,input-panel 自然贴键盘上沿。
|
|
15526
|
-
syncAppViewportHeight();
|
|
15820
|
+
syncAppViewportHeight(isKeyboardOpen);
|
|
15527
15821
|
|
|
15528
15822
|
if (isKeyboardOpen && (!keyboardOpen || heightChanged) && shouldAdjustForKeyboard(vv, inputBox)) {
|
|
15529
15823
|
syncInputBoxScroll(inputBox);
|
|
@@ -15543,6 +15837,21 @@
|
|
|
15543
15837
|
// final scroll lands AFTER the animation settles.
|
|
15544
15838
|
var wasStickToBottom = state.terminalAutoFollow || isTerminalNearBottom();
|
|
15545
15839
|
ensureTerminalFit("keyboard-open", { forceReplay: true });
|
|
15840
|
+
// iOS Safari 二次复位: 第一次 syncAppViewportHeight 在键盘动画
|
|
15841
|
+
// 起始帧把 html 滚回 0, 但 iOS 在键盘动画收尾时还会再尝试一次
|
|
15842
|
+
// "把焦点元素拽进可见区", 把 html 重新推上去 —— 留下用户报的
|
|
15843
|
+
// "输入框距离键盘还有很长距离"。镜像 keyboard-close 的 200ms 兜底,
|
|
15844
|
+
// 等键盘动画完整跑完后再清一次 scrollTop + 重算 viewport 高度,
|
|
15845
|
+
// 让 input-panel 最终稳定贴在键盘上沿。
|
|
15846
|
+
// Wand APK (__wandImeNative=true) 跳过, 原生 IME callback 已经在
|
|
15847
|
+
// WebView 层精确 resize, 这里再 scroll 反而抖。
|
|
15848
|
+
if (!window.__wandImeNative) {
|
|
15849
|
+
setTimeout(function() {
|
|
15850
|
+
resetRootViewportScroll();
|
|
15851
|
+
syncAppViewportHeight(true);
|
|
15852
|
+
}, 220);
|
|
15853
|
+
}
|
|
15854
|
+
scheduleViewportSettle();
|
|
15546
15855
|
// Mirror the keyboard-close 200ms delay: by then the iOS / Android
|
|
15547
15856
|
// keyboard slide-in animation is done, vv.height is final, and
|
|
15548
15857
|
// scrollHeight reflects the post-replay grid. One more force
|
|
@@ -15565,8 +15874,8 @@
|
|
|
15565
15874
|
// window.scrollTo(0,0) 不跑,页面停在键盘抬起时被 iOS 推上去的
|
|
15566
15875
|
// 偏移位置,input-panel 看起来"没回到底"。
|
|
15567
15876
|
// 这里在 visualViewport 检测到键盘收起的瞬间直接强制复位一次,
|
|
15568
|
-
// 并把 --app-viewport-height
|
|
15569
|
-
//
|
|
15877
|
+
// 并把 --app-viewport-height 同步到键盘收起后的实测高度,确保
|
|
15878
|
+
// input-panel 重新贴屏幕底部。
|
|
15570
15879
|
//
|
|
15571
15880
|
// Android APK (window.__wandImeNative=true) 跳过这段 iOS hack —
|
|
15572
15881
|
// MainActivity 已经在 IME 动画 callback 里逐帧把 root setPadding,
|
|
@@ -15576,21 +15885,22 @@
|
|
|
15576
15885
|
var rootEl = document.documentElement;
|
|
15577
15886
|
var imeIsNative = !!window.__wandImeNative;
|
|
15578
15887
|
if (!imeIsNative) {
|
|
15579
|
-
|
|
15580
|
-
|
|
15581
|
-
|
|
15582
|
-
|
|
15583
|
-
|
|
15888
|
+
// 不要 removeProperty('--app-viewport-height') —— 那样会让 body 退回
|
|
15889
|
+
// 到 100dvh, 而 iOS Safari 的 100dvh 在键盘动画跑完前经常还停留
|
|
15890
|
+
// 在小值, body 立刻短一截 -> 输入框下方露出大段奶油色 html 背景。
|
|
15891
|
+
// 直接让 syncAppViewportHeight 把它更新为新的 vv.height (键盘已收
|
|
15892
|
+
// 起所以是全可见高度), body 平滑过渡, 不出现空洞。
|
|
15893
|
+
syncAppViewportHeight(false);
|
|
15894
|
+
resetRootViewportScroll();
|
|
15584
15895
|
}
|
|
15896
|
+
scheduleViewportSettle();
|
|
15585
15897
|
setTimeout(function() {
|
|
15586
15898
|
if (!imeIsNative) {
|
|
15587
15899
|
// 二次复位:等键盘收起动画 + iOS 自身的回滚跑完后再清一次,
|
|
15588
15900
|
// 防止 iOS 在动画过程中又把 scrollTop 推上去。
|
|
15589
|
-
|
|
15590
|
-
if (
|
|
15591
|
-
|
|
15592
|
-
if (document.body) document.body.scrollTop = 0;
|
|
15593
|
-
syncAppViewportHeight();
|
|
15901
|
+
resetRootViewportScroll();
|
|
15902
|
+
if (rootEl) rootEl.scrollTop = 0;
|
|
15903
|
+
syncAppViewportHeight(false);
|
|
15594
15904
|
}
|
|
15595
15905
|
ensureTerminalFit("keyboard-close", { forceReplay: true });
|
|
15596
15906
|
// 同 handleInputBoxBlur:尊重 terminalAutoFollow,避免把上滚
|
|
@@ -15706,11 +16016,18 @@
|
|
|
15706
16016
|
// 不改动终端背景的 touch/scroll/wheel —— 单指空白处仍是原生滚动看历史。
|
|
15707
16017
|
|
|
15708
16018
|
function isJoystickAvailable() {
|
|
15709
|
-
// 触屏与桌面网页端都显示(球球用 Pointer Events
|
|
15710
|
-
|
|
16019
|
+
// 触屏与桌面网页端都显示(球球用 Pointer Events,鼠标拖拽同样可用)。
|
|
16020
|
+
// 不再用 currentView/isStructuredSession 关掉:
|
|
16021
|
+
// - chat 视图 (含 PTY Claude 的对话视图): 用户偶尔要给底层 PTY 发
|
|
16022
|
+
// 方向键 / Esc / Shift+Tab 选权限菜单, 但只能切到 terminal 视图才点
|
|
16023
|
+
// 摇杆 —— 现在 chat 视图直接可用。sendJoystickKey 已经走 /input
|
|
16024
|
+
// 接口, 服务端不挑视图。
|
|
16025
|
+
// - 结构化会话: 大多数键 (方向 / Tab) 在 SDK runner 里没真实 effect,
|
|
16026
|
+
// 但 Ctrl+C / Esc 都映射到 query.interrupt() 中断当前回复, 用户
|
|
16027
|
+
// 场景是"等不及当前回答, 想停掉重发"。sendJoystickKey 里按 session
|
|
16028
|
+
// 类型分支处理: PTY 走原本 sequence, 结构化只接受中断意图键。
|
|
15711
16029
|
var session = getSelectedSession();
|
|
15712
16030
|
if (!session) return false;
|
|
15713
|
-
if (isStructuredSession(session)) return false;
|
|
15714
16031
|
return true;
|
|
15715
16032
|
}
|
|
15716
16033
|
|
|
@@ -15757,16 +16074,21 @@
|
|
|
15757
16074
|
for (i = 0; i < JOYSTICK_OUTER_KEYS.length; i++) {
|
|
15758
16075
|
fnRow += keyBtn(JOYSTICK_OUTER_KEYS[i].key, JOYSTICK_OUTER_KEYS[i].label, "");
|
|
15759
16076
|
}
|
|
15760
|
-
|
|
15761
|
-
|
|
15762
|
-
|
|
15763
|
-
|
|
15764
|
-
var
|
|
15765
|
-
return '<div class="wjp-title">遥控面板</div>' +
|
|
16077
|
+
// 角键 (PgUp/Home/PgDn/End) 与修饰键 (Ctrl/Alt) 排已被裁剪 ——
|
|
16078
|
+
// 当前外圈 4 键全是独立功能键 (Enter / Ctrl+C / Esc / Shift+Tab),
|
|
16079
|
+
// 没有"先按 Ctrl 再按字母"的复合组合, 所以修饰键 toggle 没意义。
|
|
16080
|
+
// CORNER_KEYS 为空时, 对应的 grid 不渲染, 面板高度自动收缩。
|
|
16081
|
+
var html = '<div class="wjp-title">遥控面板</div>' +
|
|
15766
16082
|
dpad +
|
|
15767
|
-
'<div class="wjp-grid wjp-fnkeys">' + fnRow + "</div>"
|
|
15768
|
-
|
|
15769
|
-
|
|
16083
|
+
'<div class="wjp-grid wjp-fnkeys">' + fnRow + "</div>";
|
|
16084
|
+
if (JOYSTICK_CORNER_KEYS.length > 0) {
|
|
16085
|
+
var cornerRow = "";
|
|
16086
|
+
for (i = 0; i < JOYSTICK_CORNER_KEYS.length; i++) {
|
|
16087
|
+
cornerRow += keyBtn(JOYSTICK_CORNER_KEYS[i].key, JOYSTICK_CORNER_KEYS[i].label, "");
|
|
16088
|
+
}
|
|
16089
|
+
html += '<div class="wjp-grid wjp-corners">' + cornerRow + "</div>";
|
|
16090
|
+
}
|
|
16091
|
+
return html;
|
|
15770
16092
|
}
|
|
15771
16093
|
|
|
15772
16094
|
function joystickPolar(r, deg) {
|
|
@@ -15816,12 +16138,17 @@
|
|
|
15816
16138
|
'<path d="' + joystickSectorPath(JOYSTICK_R0, JOYSTICK_R1, center - 45 + gap, center + 45 - gap) + '"/>' +
|
|
15817
16139
|
joystickLabelMarkup(k.label, lp.x, lp.y) + "</g>";
|
|
15818
16140
|
}
|
|
15819
|
-
|
|
16141
|
+
// 外圈扇区宽度跟随 OUTER_KEYS.length 动态计算: 4 键 → 90° 每片,
|
|
16142
|
+
// 8 键 → 45° 每片。half 是单片半宽 (扇区中心两侧各延半个 step)。
|
|
16143
|
+
var outerCount = JOYSTICK_OUTER_KEYS.length;
|
|
16144
|
+
var outerStep = outerCount > 0 ? 360 / outerCount : 360;
|
|
16145
|
+
var outerHalf = outerStep / 2;
|
|
16146
|
+
for (i = 0; i < outerCount; i++) {
|
|
15820
16147
|
k = JOYSTICK_OUTER_KEYS[i];
|
|
15821
|
-
center = -90 + i *
|
|
16148
|
+
center = -90 + i * outerStep;
|
|
15822
16149
|
lp = joystickPolar((JOYSTICK_R1 + JOYSTICK_R2) / 2, center);
|
|
15823
16150
|
svg += '<g class="wjr-sector wjr-outer" data-key="' + k.key + '">' +
|
|
15824
|
-
'<path d="' + joystickSectorPath(JOYSTICK_R1, JOYSTICK_R2, center -
|
|
16151
|
+
'<path d="' + joystickSectorPath(JOYSTICK_R1, JOYSTICK_R2, center - outerHalf + gap, center + outerHalf - gap) + '"/>' +
|
|
15825
16152
|
joystickLabelMarkup(k.label, lp.x, lp.y) + "</g>";
|
|
15826
16153
|
}
|
|
15827
16154
|
svg += '<circle class="wjr-hub" cx="0" cy="0" r="' + (JOYSTICK_R0 - 1) + '"/>';
|
|
@@ -16071,10 +16398,14 @@
|
|
|
16071
16398
|
if (Math.abs(dy) >= Math.abs(dx)) return { zone: "inner", key: dy < 0 ? "up" : "down" };
|
|
16072
16399
|
return { zone: "inner", key: dx < 0 ? "left" : "right" };
|
|
16073
16400
|
}
|
|
16074
|
-
// 外圈:
|
|
16401
|
+
// 外圈:OUTER_KEYS.length 等分扇区,正上方为 0,顺时针递增;
|
|
16402
|
+
// +halfStep 让扇区中心对准按钮 (原本 N=8 时是 +π/8)。
|
|
16075
16403
|
var ang = Math.atan2(dx, -dy);
|
|
16076
16404
|
if (ang < 0) ang += Math.PI * 2;
|
|
16077
|
-
var
|
|
16405
|
+
var outerCount = JOYSTICK_OUTER_KEYS.length;
|
|
16406
|
+
if (outerCount === 0) return { zone: "dead", key: null };
|
|
16407
|
+
var outerStepRad = (Math.PI * 2) / outerCount;
|
|
16408
|
+
var idx = Math.floor((ang + outerStepRad / 2) / outerStepRad) % outerCount;
|
|
16078
16409
|
return { zone: "outer", key: JOYSTICK_OUTER_KEYS[idx].key };
|
|
16079
16410
|
}
|
|
16080
16411
|
|
|
@@ -16179,6 +16510,24 @@
|
|
|
16179
16510
|
updateJoystickPanelUI();
|
|
16180
16511
|
return;
|
|
16181
16512
|
}
|
|
16513
|
+
var session = getSelectedSession();
|
|
16514
|
+
// ── 结构化会话分支 ──
|
|
16515
|
+
// SDK / claude -p 通道没有 PTY 可写, 把原始 escape 序列丢给
|
|
16516
|
+
// /api/sessions/:id/input 会被结构化 sendMessage 当成对话文本 (例如
|
|
16517
|
+
// 把 "\x1b[A" 作为 prompt 发出去), 既无效又污染上下文。
|
|
16518
|
+
// 这里按"中断意图"白名单转发: Ctrl+C / Esc → query.interrupt()。
|
|
16519
|
+
// 其他键 (方向 / Enter / Shift+Tab) 在结构化里没有合理 mapping, 静默
|
|
16520
|
+
// no-op, 同时震一下做反馈。
|
|
16521
|
+
if (session && isStructuredSession(session)) {
|
|
16522
|
+
if (key === "ctrl_c" || key === "escape") {
|
|
16523
|
+
interruptStructuredSessionFromJoystick(session, key);
|
|
16524
|
+
}
|
|
16525
|
+
// 不论是否真发出去, 都消化掉修饰键 + 更新 UI, 避免下次发送残留状态
|
|
16526
|
+
clearModifiers();
|
|
16527
|
+
updateJoystickPanelUI();
|
|
16528
|
+
return;
|
|
16529
|
+
}
|
|
16530
|
+
// ── PTY 会话原路径 ──
|
|
16182
16531
|
var seq = buildPtySequence(key, {
|
|
16183
16532
|
ctrl: state.modifiers.ctrl,
|
|
16184
16533
|
alt: state.modifiers.alt,
|
|
@@ -16190,6 +16539,44 @@
|
|
|
16190
16539
|
scheduleShortcutResync();
|
|
16191
16540
|
}
|
|
16192
16541
|
|
|
16542
|
+
// 摇杆触发的结构化会话中断: 复用 /api/structured-sessions/:id/messages
|
|
16543
|
+
// 的 interrupt=true 路径 (sendMessage 内部走 query.interrupt 优雅停止,
|
|
16544
|
+
// 失败 fallback 到 abortController.abort)。空 input + interrupt=true =
|
|
16545
|
+
// "停掉当前回复但不发新消息", 跟用户从摇杆按 Ctrl+C/Esc 的预期一致。
|
|
16546
|
+
function interruptStructuredSessionFromJoystick(session, key) {
|
|
16547
|
+
if (!session || !session.id) return;
|
|
16548
|
+
fetch("/api/structured-sessions/" + session.id + "/messages", {
|
|
16549
|
+
method: "POST",
|
|
16550
|
+
headers: { "Content-Type": "application/json" },
|
|
16551
|
+
credentials: "same-origin",
|
|
16552
|
+
body: JSON.stringify({ input: "", interrupt: true, preserveQueue: true }),
|
|
16553
|
+
})
|
|
16554
|
+
.then(function(res) {
|
|
16555
|
+
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(p) {
|
|
16556
|
+
throw new Error((p && p.error) || ("中断失败 (key=" + key + ")"));
|
|
16557
|
+
});
|
|
16558
|
+
return res.json();
|
|
16559
|
+
})
|
|
16560
|
+
.then(function(snapshot) {
|
|
16561
|
+
if (snapshot && snapshot.id) {
|
|
16562
|
+
updateSessionSnapshot(snapshot);
|
|
16563
|
+
if (snapshot.id === state.selectedId) {
|
|
16564
|
+
var refreshed = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
|
|
16565
|
+
state.currentMessages = buildMessagesForRender(refreshed, getPreferredMessages(refreshed, snapshot.output, false));
|
|
16566
|
+
renderChat(true);
|
|
16567
|
+
if (typeof updateQueueBar === "function") updateQueueBar();
|
|
16568
|
+
}
|
|
16569
|
+
}
|
|
16570
|
+
})
|
|
16571
|
+
.catch(function(err) {
|
|
16572
|
+
// 已经在 SDK 内部完成 / 没有 pending query 时, 服务端会返回 400,
|
|
16573
|
+
// 这里静默吃掉, 避免给用户冒出"中断失败"toast (按了也是想停, 没东西可停就当成功)。
|
|
16574
|
+
if (window && window.console && err && err.message) {
|
|
16575
|
+
console.debug("[wand] joystick interrupt no-op:", err.message);
|
|
16576
|
+
}
|
|
16577
|
+
});
|
|
16578
|
+
}
|
|
16579
|
+
|
|
16193
16580
|
function toggleJoystickPanel() {
|
|
16194
16581
|
if (state.joystickPinnedOpen) closeJoystickPanel();
|
|
16195
16582
|
else openJoystickPanel();
|