@co0ontty/wand 1.30.3 → 1.31.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/README.md +39 -2
- package/dist/claude-sdk-runner.d.ts +31 -0
- package/dist/claude-sdk-runner.js +142 -0
- package/dist/cli.js +104 -0
- package/dist/git-quick-commit.js +18 -26
- package/dist/process-manager.d.ts +7 -0
- package/dist/process-manager.js +26 -2
- package/dist/prompt-optimizer.js +17 -26
- package/dist/server-session-routes.js +27 -1
- package/dist/server.js +1 -0
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +21 -3
- package/dist/tui/attach.js +7 -8
- package/dist/tui/commands.d.ts +24 -7
- package/dist/tui/commands.js +200 -86
- package/dist/tui/index.js +8 -8
- package/dist/tui/service-panel.js +3 -4
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +250 -53
- package/dist/web-ui/content/styles.css +345 -109
- package/package.json +1 -1
package/dist/tui/index.js
CHANGED
|
@@ -206,14 +206,14 @@ export function startTui(deps) {
|
|
|
206
206
|
layout.showToast("服务已安装,按 Shift+S 卸载", "warn", 2500);
|
|
207
207
|
return;
|
|
208
208
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
});
|
|
209
|
+
// 默认装 system-wide。非 root 时 installService 会返回明确错误,toast 自然展示。
|
|
210
|
+
const isRoot = typeof process.getuid === "function" ? process.getuid() === 0 : false;
|
|
211
|
+
const body = process.platform === "linux"
|
|
212
|
+
? `将写入 /etc/systemd/system/wand.service,systemctl enable --now,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,建议先 Ctrl+C 退出 TUI 再 sudo wand web 重新进。"}\n不想要 root?退出 TUI 跑 wand service:install --user (登出会被回收)。`
|
|
213
|
+
: process.platform === "darwin"
|
|
214
|
+
? `将写入 /Library/LaunchDaemons/com.wand.web.plist,launchctl load,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,建议先 Ctrl+C 退出 TUI 再 sudo wand web 重新进。"}`
|
|
215
|
+
: "当前平台暂不支持。";
|
|
216
|
+
const ok = await layout.confirm({ title: "注册为系统服务", body });
|
|
217
217
|
if (!ok)
|
|
218
218
|
return;
|
|
219
219
|
const r = await runOffMicrotask(() => installService({ configPath: deps.configPath }));
|
|
@@ -64,10 +64,9 @@ export function openServicePanel(deps) {
|
|
|
64
64
|
layout.showToast("服务已安装,按 u 先卸载再重装", "warn", 2500);
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
67
|
+
const isRoot = typeof process.getuid === "function" ? process.getuid() === 0 : false;
|
|
68
|
+
const body = `默认装 system-wide(/etc/systemd/system/ 或 /Library/LaunchDaemons/),开机自启、登出不停。${isRoot ? "" : "\n⚠ 需要 root,如果失败请退出 TUI 跑 sudo wand service:install。"}`;
|
|
69
|
+
const ok = await layout.confirm({ title: "注册服务", body });
|
|
71
70
|
if (!ok)
|
|
72
71
|
return;
|
|
73
72
|
handleResult("install", installService({ configPath }));
|
package/dist/types.d.ts
CHANGED
|
@@ -239,6 +239,8 @@ export interface CommandRequest {
|
|
|
239
239
|
cols?: number;
|
|
240
240
|
/** 创建会话时由前端测得的真实行数。 */
|
|
241
241
|
rows?: number;
|
|
242
|
+
/** 思考深度。null/缺省 视为 off(不启用思考)。 */
|
|
243
|
+
thinkingEffort?: "off" | "standard" | "deep" | "max" | null;
|
|
242
244
|
}
|
|
243
245
|
export interface InputRequest {
|
|
244
246
|
input?: string;
|
|
@@ -149,6 +149,12 @@
|
|
|
149
149
|
chatModel: (function() {
|
|
150
150
|
try { return localStorage.getItem("wand-chat-model") || ""; } catch (e) { return ""; }
|
|
151
151
|
})(),
|
|
152
|
+
chatThinking: (function() {
|
|
153
|
+
try {
|
|
154
|
+
var v = localStorage.getItem("wand-thinking-effort") || "off";
|
|
155
|
+
return (v === "off" || v === "standard" || v === "deep" || v === "max") ? v : "off";
|
|
156
|
+
} catch (e) { return "off"; }
|
|
157
|
+
})(),
|
|
152
158
|
availableModels: [],
|
|
153
159
|
availableCodexModels: [],
|
|
154
160
|
modelsRefreshing: false,
|
|
@@ -1724,13 +1730,26 @@
|
|
|
1724
1730
|
// tabindex="-1": 把这些控件移出 iOS Safari 的表单导航链,
|
|
1725
1731
|
// 这样 textarea 聚焦时键盘上方就不会出现 ⌃ ⌄ ✓ 表单辅助栏。
|
|
1726
1732
|
'<input type="file" id="file-upload-input" multiple tabindex="-1" style="position:absolute;width:1px;height:1px;opacity:0;overflow:hidden;clip:rect(0,0,0,0);pointer-events:none">' +
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
'
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1733
|
+
// 三件套 (Mode / Model / Thinking) 同属"会话设置"层:视觉上统一为同一种 pill 风格,
|
|
1734
|
+
// 由 CSS .composer-pill-group 内的轻量分隔感传达"归类"。
|
|
1735
|
+
'<span class="composer-pill-group" role="group" aria-label="会话设置">' +
|
|
1736
|
+
'<select id="chat-mode-select" class="composer-pill composer-pill-select chat-mode-select" tabindex="-1" title="新会话模式 · 托管 / 全权限 自动启用批准">' +
|
|
1737
|
+
renderModeOptions(preferredTool, composerMode) +
|
|
1738
|
+
'</select>' +
|
|
1739
|
+
'<select id="chat-model-select" class="composer-pill composer-pill-select chat-mode-select chat-model-select" tabindex="-1" title="切换模型(对运行中会话发送 /model,对新会话作为 --model 启动)">' +
|
|
1740
|
+
renderChatModelOptions(getEffectiveModel(selectedSession), selectedSession) +
|
|
1741
|
+
'</select>' +
|
|
1742
|
+
// 思考深度 trigger:与 mode / model 同形(border + chevron),保证视觉一致。
|
|
1743
|
+
// 可见层只有「档位文字」,原生 <select> 透明叠在上面,依然能调起 iOS 滚轮选择。
|
|
1744
|
+
'<span class="composer-pill composer-pill-select chat-thinking-trigger" title="思考深度(structured 立即生效;PTY 仅作用于通过 chat 视图发送的消息)">' +
|
|
1745
|
+
'<span class="chat-thinking-label" id="chat-thinking-label">' + escapeHtml(getThinkingLabel(getEffectiveThinking(selectedSession))) + '</span>' +
|
|
1746
|
+
'<select id="chat-thinking-select" class="chat-thinking-hidden-select" tabindex="-1" aria-label="思考深度">' +
|
|
1747
|
+
renderChatThinkingOptions(getEffectiveThinking(selectedSession)) +
|
|
1748
|
+
'</select>' +
|
|
1749
|
+
'</span>' +
|
|
1750
|
+
'</span>' +
|
|
1751
|
+
renderAutoApproveChip(selectedSession) +
|
|
1752
|
+
'<button id="terminal-interactive-toggle-top" class="composer-pill composer-pill-chip composer-interactive-toggle' + (state.terminalInteractive ? " active" : "") + '" type="button" title="切换终端交互模式">⌨</button>' +
|
|
1734
1753
|
'<span class="permission-actions hidden" id="permission-actions">' +
|
|
1735
1754
|
'<span class="permission-actions-divider"></span>' +
|
|
1736
1755
|
'<span class="permission-actions-label" id="permission-actions-label">等待授权</span>' +
|
|
@@ -1740,7 +1759,9 @@
|
|
|
1740
1759
|
renderApprovalStatsBadge() +
|
|
1741
1760
|
'</div>' +
|
|
1742
1761
|
'<div class="input-composer-right">' +
|
|
1743
|
-
|
|
1762
|
+
// queue-counter:当 queuedMessages 不为空时显示,提示用户后面还堆了几条。
|
|
1763
|
+
// 比小角标更显眼一点——加图标 + 强对比色,避免 v1.30.3 那一版用户没看见。
|
|
1764
|
+
'<span id="queue-counter" class="queue-counter hidden" title="正在排队的输入条数"><span class="queue-counter-dot"></span><span class="queue-counter-text">队列 0</span></span>' +
|
|
1744
1765
|
'<span class="input-hint' + (state.terminalInteractive ? ' terminal-interactive-hint' : state.currentView === "terminal" ? " hidden" : "") + '">' + (state.terminalInteractive ? '终端交互中 · Ctrl+C 中断 · Ctrl+L 清屏' : 'Enter 发送 · Shift+Enter 换行') + '</span>' +
|
|
1745
1766
|
renderInlineKeyboard() +
|
|
1746
1767
|
'<button id="stop-button" class="btn-circle btn-circle-stop' + (state.selectedId ? "" : " hidden") + '" title="停止">' +
|
|
@@ -1748,8 +1769,10 @@
|
|
|
1748
1769
|
'</button>' +
|
|
1749
1770
|
// 结构化模式且正在出 token 时显示:中断当前回复、立刻发送新输入。
|
|
1750
1771
|
// 默认走 #send-input-button → 排队;想插队的人显式按这颗。
|
|
1751
|
-
|
|
1752
|
-
|
|
1772
|
+
// 用 pill 形态 + 文字 + 脉动,让用户一眼就看到「立即发送」这条快捷路径。
|
|
1773
|
+
'<button id="interrupt-send-button" class="btn-pill btn-pill-interrupt hidden" type="button" title="中断当前回复并立即发送新输入(Cmd/Ctrl+Enter)" aria-label="立即发送">' +
|
|
1774
|
+
'<svg class="btn-pill-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg>' +
|
|
1775
|
+
'<span class="btn-pill-label">立即</span>' +
|
|
1753
1776
|
'</button>' +
|
|
1754
1777
|
'<button id="send-input-button" class="btn-circle btn-circle-send" title="发送">' +
|
|
1755
1778
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>' +
|
|
@@ -1757,14 +1780,20 @@
|
|
|
1757
1780
|
'</div>' +
|
|
1758
1781
|
'</div>' +
|
|
1759
1782
|
renderExpandedShortcutsRow() +
|
|
1760
|
-
// Session info bar at bottom —
|
|
1761
|
-
//
|
|
1783
|
+
// Session info bar at bottom — 仅保留信息类徽章(Claude session id / exit code)。
|
|
1784
|
+
// 自动批准已从这里移到主 pill 行(renderAutoApproveChip)。
|
|
1762
1785
|
(selectedSession
|
|
1763
|
-
?
|
|
1764
|
-
|
|
1765
|
-
(selectedSession.provider === "claude" && selectedSession.claudeSessionId
|
|
1766
|
-
|
|
1767
|
-
|
|
1786
|
+
? (function() {
|
|
1787
|
+
var bits = "";
|
|
1788
|
+
if (selectedSession.provider === "claude" && selectedSession.claudeSessionId) {
|
|
1789
|
+
bits += '<span id="claude-session-id-badge" class="claude-session-id-badge" data-claude-id="' + escapeHtml(selectedSession.claudeSessionId) + '" title="点击复制 Claude 会话 ID">☁ ' + escapeHtml(selectedSession.claudeSessionId.slice(0, 8)) + '</span>';
|
|
1790
|
+
}
|
|
1791
|
+
if (!isStructuredSession(selectedSession) && selectedSession.exitCode !== undefined && selectedSession.exitCode !== null) {
|
|
1792
|
+
if (bits) bits += '<span class="session-info-separator">|</span>';
|
|
1793
|
+
bits += '<span id="session-exit-display" class="session-exit-display">退出码=' + selectedSession.exitCode + '</span>';
|
|
1794
|
+
}
|
|
1795
|
+
return bits ? '<div class="input-session-info-bar">' + bits + '</div>' : '';
|
|
1796
|
+
})()
|
|
1768
1797
|
: '') +
|
|
1769
1798
|
'</div>' +
|
|
1770
1799
|
'<p id="action-error" class="error-message hidden"></p>' +
|
|
@@ -5988,6 +6017,10 @@
|
|
|
5988
6017
|
if (modelSelect) modelSelect.addEventListener("change", function() {
|
|
5989
6018
|
onChatModelChange(this.value);
|
|
5990
6019
|
});
|
|
6020
|
+
var thinkingSelect = document.getElementById("chat-thinking-select");
|
|
6021
|
+
if (thinkingSelect) thinkingSelect.addEventListener("change", function() {
|
|
6022
|
+
onChatThinkingChange(this.value);
|
|
6023
|
+
});
|
|
5991
6024
|
|
|
5992
6025
|
var sessionModal = document.getElementById("session-modal");
|
|
5993
6026
|
if (sessionModal) sessionModal.addEventListener("click", function(e) {
|
|
@@ -7996,7 +8029,7 @@
|
|
|
7996
8029
|
// 结构化会话在出 token 时,输入框仍然可用——告诉用户默认行为是排队,
|
|
7997
8030
|
// 想插队请按右侧的 » 按钮。短语保持单行不换行。
|
|
7998
8031
|
if (isStructuredSession(session) && session.structuredState && session.structuredState.inFlight) {
|
|
7999
|
-
return "
|
|
8032
|
+
return "回复中…Enter 排队 · 旁边的「» 立即」按钮中断并立即发送";
|
|
8000
8033
|
}
|
|
8001
8034
|
return "";
|
|
8002
8035
|
}
|
|
@@ -8129,10 +8162,109 @@
|
|
|
8129
8162
|
|
|
8130
8163
|
function syncComposerModelSelect(session) {
|
|
8131
8164
|
var select = document.getElementById("chat-model-select");
|
|
8165
|
+
if (select) {
|
|
8166
|
+
var effective = getEffectiveModel(session);
|
|
8167
|
+
select.innerHTML = renderChatModelOptions(effective, session);
|
|
8168
|
+
select.value = effective;
|
|
8169
|
+
}
|
|
8170
|
+
// thinking 选择器与 model 选择器属于同一组"会话级设置",
|
|
8171
|
+
// 任何刷新 model 的时机也应该同步刷新 thinking,避免漂移。
|
|
8172
|
+
syncComposerThinkingSelect(session);
|
|
8173
|
+
}
|
|
8174
|
+
|
|
8175
|
+
// ── 思考深度 (thinkingEffort) —— 与 model 选择三件套对称 ──
|
|
8176
|
+
|
|
8177
|
+
// 标签直接用 Claude CLI 原生 magic word:think / think hard / ultrathink。
|
|
8178
|
+
// 这样用户一眼能对上官方文档里的思考强度档位,PTY 模式下也是这几个词被注入到 prompt 前缀。
|
|
8179
|
+
var THINKING_LEVELS = [
|
|
8180
|
+
{ id: "off", label: "off", hint: "不启用思考(CLI 无前缀;SDK 关闭 thinking;Codex --reasoning-effort minimal)" },
|
|
8181
|
+
{ id: "standard", label: "think", hint: "Claude CLI: think · SDK budget 4096 · Codex low" },
|
|
8182
|
+
{ id: "deep", label: "think hard", hint: "Claude CLI: think hard · SDK budget 16000 · Codex medium" },
|
|
8183
|
+
{ id: "max", label: "ultrathink", hint: "Claude CLI: ultrathink · SDK budget 31999 · Codex high" }
|
|
8184
|
+
];
|
|
8185
|
+
|
|
8186
|
+
function getThinkingLabel(id) {
|
|
8187
|
+
for (var i = 0; i < THINKING_LEVELS.length; i++) {
|
|
8188
|
+
if (THINKING_LEVELS[i].id === id) return THINKING_LEVELS[i].label;
|
|
8189
|
+
}
|
|
8190
|
+
return THINKING_LEVELS[0].label;
|
|
8191
|
+
}
|
|
8192
|
+
|
|
8193
|
+
function getEffectiveThinking(session) {
|
|
8194
|
+
if (session && session.thinkingEffort) return session.thinkingEffort;
|
|
8195
|
+
if (state.chatThinking) return state.chatThinking;
|
|
8196
|
+
return "off";
|
|
8197
|
+
}
|
|
8198
|
+
|
|
8199
|
+
function renderChatThinkingOptions(selected) {
|
|
8200
|
+
var v = selected || "off";
|
|
8201
|
+
var html = "";
|
|
8202
|
+
for (var i = 0; i < THINKING_LEVELS.length; i++) {
|
|
8203
|
+
var lvl = THINKING_LEVELS[i];
|
|
8204
|
+
html += '<option value="' + escapeHtml(lvl.id) + '"' + (lvl.id === v ? ' selected' : '') + ' title="' + escapeHtml(lvl.hint) + '">' + escapeHtml(lvl.label) + '</option>';
|
|
8205
|
+
}
|
|
8206
|
+
return html;
|
|
8207
|
+
}
|
|
8208
|
+
|
|
8209
|
+
function syncComposerThinkingSelect(session) {
|
|
8210
|
+
var select = document.getElementById("chat-thinking-select");
|
|
8132
8211
|
if (!select) return;
|
|
8133
|
-
var effective =
|
|
8134
|
-
select.innerHTML =
|
|
8212
|
+
var effective = getEffectiveThinking(session);
|
|
8213
|
+
select.innerHTML = renderChatThinkingOptions(effective);
|
|
8135
8214
|
select.value = effective;
|
|
8215
|
+
var labelEl = document.getElementById("chat-thinking-label");
|
|
8216
|
+
if (labelEl) labelEl.textContent = getThinkingLabel(effective);
|
|
8217
|
+
}
|
|
8218
|
+
|
|
8219
|
+
function onChatThinkingChange(value) {
|
|
8220
|
+
var normalized = (value || "off").trim();
|
|
8221
|
+
if (normalized !== "off" && normalized !== "standard" && normalized !== "deep" && normalized !== "max") {
|
|
8222
|
+
normalized = "off";
|
|
8223
|
+
}
|
|
8224
|
+
state.chatThinking = normalized;
|
|
8225
|
+
try { localStorage.setItem("wand-thinking-effort", normalized); } catch (e) {}
|
|
8226
|
+
var labelEl = document.getElementById("chat-thinking-label");
|
|
8227
|
+
if (labelEl) labelEl.textContent = getThinkingLabel(normalized);
|
|
8228
|
+
var session = getSelectedSession();
|
|
8229
|
+
if (!session) return;
|
|
8230
|
+
fetch("/api/sessions/" + encodeURIComponent(session.id) + "/thinking-effort", {
|
|
8231
|
+
method: "POST",
|
|
8232
|
+
headers: { "Content-Type": "application/json" },
|
|
8233
|
+
credentials: "same-origin",
|
|
8234
|
+
body: JSON.stringify({ thinkingEffort: normalized })
|
|
8235
|
+
})
|
|
8236
|
+
.then(function(res) { return res.json(); })
|
|
8237
|
+
.then(function(data) {
|
|
8238
|
+
if (data && data.error) {
|
|
8239
|
+
showToast(data.error, "error");
|
|
8240
|
+
return;
|
|
8241
|
+
}
|
|
8242
|
+
if (data && data.id) {
|
|
8243
|
+
updateSessionSnapshot(data);
|
|
8244
|
+
if (typeof showToast === "function") {
|
|
8245
|
+
showToast("已切换思考深度 → " + getThinkingLabel(normalized), "success");
|
|
8246
|
+
}
|
|
8247
|
+
}
|
|
8248
|
+
})
|
|
8249
|
+
.catch(function() { showToast("切换思考深度失败", "error"); });
|
|
8250
|
+
}
|
|
8251
|
+
|
|
8252
|
+
// 自动批准 chip:与原 .auto-approve-indicator 等价,但用统一的 .composer-pill 风格放主行。
|
|
8253
|
+
// Codex 会话固定全权限不可切;结构化 Claude 会话后端 toggle-auto-approve 路由会拒绝。
|
|
8254
|
+
// 当会话已经处于 managed / full-access 模式时,"自动批准"语义已经由模式表达,
|
|
8255
|
+
// 重复显示一个独立 chip 只会占用空间又制造歧义 —— 此时直接折叠掉。
|
|
8256
|
+
function isAutoApproveImpliedByMode(session) {
|
|
8257
|
+
if (!session) return false;
|
|
8258
|
+
var m = session.mode;
|
|
8259
|
+
return m === "managed" || m === "full-access";
|
|
8260
|
+
}
|
|
8261
|
+
function renderAutoApproveChip(session) {
|
|
8262
|
+
if (!session) return "";
|
|
8263
|
+
if (isAutoApproveImpliedByMode(session)) return "";
|
|
8264
|
+
var enabled = !!session.autoApprovePermissions;
|
|
8265
|
+
return enabled
|
|
8266
|
+
? '<span id="auto-approve-toggle" class="composer-pill composer-pill-chip auto-approve-indicator active" title="自动批准已启用 — 点击关闭">🛡 自动</span>'
|
|
8267
|
+
: '<span id="auto-approve-toggle" class="composer-pill composer-pill-chip auto-approve-indicator" title="自动批准已关闭 — 点击开启">🛡 手动</span>';
|
|
8136
8268
|
}
|
|
8137
8269
|
|
|
8138
8270
|
function fetchAvailableModels() {
|
|
@@ -8370,6 +8502,7 @@
|
|
|
8370
8502
|
function createStructuredSession(prompt, cwdOverride, modeOverride, worktreeEnabled) {
|
|
8371
8503
|
var provider = state.sessionTool === "codex" ? "codex" : "claude";
|
|
8372
8504
|
var modelPref = state.chatModel || (state.config && state.config.defaultModel) || "";
|
|
8505
|
+
var thinkingPref = state.chatThinking || "off";
|
|
8373
8506
|
var payload = {
|
|
8374
8507
|
cwd: cwdOverride || getEffectiveCwd(),
|
|
8375
8508
|
mode: modeOverride || state.chatMode || (state.config && state.config.defaultMode) || "default",
|
|
@@ -8377,7 +8510,8 @@
|
|
|
8377
8510
|
runner: provider === "codex" ? "codex-cli-exec" : ((state.config && state.config.structuredRunner === "sdk") ? "claude-sdk" : (state.structuredRunner || "claude-cli-print")),
|
|
8378
8511
|
prompt: prompt || undefined,
|
|
8379
8512
|
worktreeEnabled: worktreeEnabled === true,
|
|
8380
|
-
model: modelPref || undefined
|
|
8513
|
+
model: modelPref || undefined,
|
|
8514
|
+
thinkingEffort: thinkingPref
|
|
8381
8515
|
};
|
|
8382
8516
|
console.log("[WAND] createStructuredSession payload:", JSON.stringify(payload));
|
|
8383
8517
|
return fetch("/api/structured-sessions", {
|
|
@@ -11916,8 +12050,14 @@
|
|
|
11916
12050
|
return Promise.resolve();
|
|
11917
12051
|
}
|
|
11918
12052
|
|
|
11919
|
-
//
|
|
11920
|
-
|
|
12053
|
+
// 防止同一会话「快速双击 / 重复触发」。原来这是个布尔 flag,绑在 fetch 的
|
|
12054
|
+
// promise 上 —— 但 structured-sessions/:id/messages 的 POST 对首条消息会 await
|
|
12055
|
+
// 整段流式 streaming,flag 会被卡到回复完才释放。结果:用户点发送 → 服务端
|
|
12056
|
+
// 流式 30s 不响应 → 这 30s 里再点发送全被这里静默 drop,看起来"排队 / 立即发送
|
|
12057
|
+
// 都没效果"。改成时间戳 + 短窗口(350ms)只挡真正的连击。idempotencyKey 已经
|
|
12058
|
+
// 在后端兜底防 webview 网络层重发,这里的 hot-path 守门只需要应付 UI 双触发。
|
|
12059
|
+
var _structuredLastSubmitAt = {};
|
|
12060
|
+
var DUPLICATE_SUBMIT_WINDOW_MS = 350;
|
|
11921
12061
|
|
|
11922
12062
|
function postStructuredInput(input, inputBox, session, opts) {
|
|
11923
12063
|
opts = opts || {};
|
|
@@ -11931,11 +12071,15 @@
|
|
|
11931
12071
|
showToast("会话不存在,请重新选择或新建会话。", "error");
|
|
11932
12072
|
return Promise.resolve();
|
|
11933
12073
|
}
|
|
11934
|
-
//
|
|
11935
|
-
|
|
11936
|
-
|
|
12074
|
+
// 短窗口内的连击当作重复点击丢掉;正常间隔的两次提交(哪怕第一次还在流式)
|
|
12075
|
+
// 都放行,让 queue / interrupt 真正生效。
|
|
12076
|
+
var nowTs = Date.now();
|
|
12077
|
+
var lastTs = _structuredLastSubmitAt[session.id] || 0;
|
|
12078
|
+
if (nowTs - lastTs < DUPLICATE_SUBMIT_WINDOW_MS) {
|
|
12079
|
+
console.log("[wand] postStructuredInput: duplicate submit (within " + DUPLICATE_SUBMIT_WINDOW_MS + "ms) ignored for session", session.id);
|
|
11937
12080
|
return Promise.resolve();
|
|
11938
12081
|
}
|
|
12082
|
+
_structuredLastSubmitAt[session.id] = nowTs;
|
|
11939
12083
|
|
|
11940
12084
|
var sessionInFlight = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
11941
12085
|
var isInterrupting = sessionInFlight && requestedInterrupt;
|
|
@@ -11960,6 +12104,9 @@
|
|
|
11960
12104
|
updateInputHint("已加入排队,等待当前回复完成…");
|
|
11961
12105
|
renderChat(true);
|
|
11962
12106
|
updateStructuredQueueCounter();
|
|
12107
|
+
// 乐观 toast:原本只在 POST 完成后才提示,Claude 流式拖太久时用户根本
|
|
12108
|
+
// 看不到反馈,会误判"点了没反应"。点击瞬间就给一条短提示。
|
|
12109
|
+
showToast(nextQueue.length > 1 ? ("已加入排队(共 " + nextQueue.length + " 条等待)") : "已加入排队,等当前回复完成会自动发送。", "info");
|
|
11963
12110
|
} else {
|
|
11964
12111
|
// 普通发送 / interrupt 发送:照旧乐观推 user turn + inFlight=true
|
|
11965
12112
|
var userTurn = { role: "user", content: [{ type: "text", text: input }] };
|
|
@@ -11978,6 +12125,11 @@
|
|
|
11978
12125
|
}), userMsgs);
|
|
11979
12126
|
updateInputHint(isInterrupting ? "已中断,正在处理新消息…" : "思考中…");
|
|
11980
12127
|
renderChat(true);
|
|
12128
|
+
// 中断模式:乐观给一条提示,让用户立刻知道"中断成功了",否则跟 queue 一样会
|
|
12129
|
+
// 觉得"点了没反应"。原 toast 在 then() 里,等 SIGTERM/HTTP roundtrip 完才出。
|
|
12130
|
+
if (isInterrupting) {
|
|
12131
|
+
showToast("已中断上一条回复,正在处理新消息…", "info");
|
|
12132
|
+
}
|
|
11981
12133
|
}
|
|
11982
12134
|
|
|
11983
12135
|
if (inputBox) {
|
|
@@ -12001,7 +12153,6 @@
|
|
|
12001
12153
|
|
|
12002
12154
|
// 用 session.id(参数绑定,in-flight 期间不变)而不是 state.selectedId
|
|
12003
12155
|
// 拼 URL,避免用户切到别的会话后 fetch 落到错误 sessionId。
|
|
12004
|
-
_structuredSubmittingSessions[session.id] = true;
|
|
12005
12156
|
return fetch("/api/structured-sessions/" + session.id + "/messages", {
|
|
12006
12157
|
method: "POST",
|
|
12007
12158
|
headers: { "Content-Type": "application/json" },
|
|
@@ -12020,7 +12171,6 @@
|
|
|
12020
12171
|
return res.json();
|
|
12021
12172
|
})
|
|
12022
12173
|
.then(function(snapshot) {
|
|
12023
|
-
_structuredSubmittingSessions[session.id] = false;
|
|
12024
12174
|
if (snapshot && snapshot.error) {
|
|
12025
12175
|
throw new Error(snapshot.error);
|
|
12026
12176
|
}
|
|
@@ -12035,18 +12185,12 @@
|
|
|
12035
12185
|
state.currentMessages = buildMessagesForRender(refreshedSession, getPreferredMessages(refreshedSession, snapshot.output, false));
|
|
12036
12186
|
renderChat(true);
|
|
12037
12187
|
updateStructuredQueueCounter();
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
} else if (isQueueing) {
|
|
12041
|
-
var qLen = Array.isArray(refreshedSession.queuedMessages) ? refreshedSession.queuedMessages.length : 0;
|
|
12042
|
-
showToast(qLen > 1 ? ("已加入排队(共 " + qLen + " 条等待)") : "已加入排队,等待当前回复完成。", "info");
|
|
12043
|
-
}
|
|
12188
|
+
// toast 已在 click 时乐观 fire(见 isQueueing / isInterrupting 分支),
|
|
12189
|
+
// 这里不再重复推送,避免同一动作出两条一样的 toast。
|
|
12044
12190
|
}
|
|
12045
12191
|
}
|
|
12046
12192
|
})
|
|
12047
12193
|
.catch(function(error) {
|
|
12048
|
-
_structuredSubmittingSessions[session.id] = false;
|
|
12049
|
-
|
|
12050
12194
|
// duplicate_idempotency_key:服务端识别出 WebView 底层重发的副本,
|
|
12051
12195
|
// 直接拦截不处理。这里**不**回滚乐观更新——第一次的请求实际上已经
|
|
12052
12196
|
// 被服务端接收并处理(或正在处理),ws 推送会带回真实状态;如果在
|
|
@@ -12110,7 +12254,15 @@
|
|
|
12110
12254
|
var counter = document.getElementById("queue-counter");
|
|
12111
12255
|
var count = getSelectedStructuredQueuedInputs().length;
|
|
12112
12256
|
if (counter) {
|
|
12113
|
-
counter
|
|
12257
|
+
// counter 现在是 dot + text 双节点结构,只更新文字节点;如果用 textContent
|
|
12258
|
+
// 直接覆盖会把内嵌的 .queue-counter-dot 也一并干掉,CSS 上做的脉动小红点就没了。
|
|
12259
|
+
var textNode = counter.querySelector(".queue-counter-text");
|
|
12260
|
+
var label = count > 0 ? ("排队 " + count + " 条") : "队列 0";
|
|
12261
|
+
if (textNode) {
|
|
12262
|
+
textNode.textContent = label;
|
|
12263
|
+
} else {
|
|
12264
|
+
counter.textContent = label;
|
|
12265
|
+
}
|
|
12114
12266
|
if (count > 0) {
|
|
12115
12267
|
counter.classList.remove("hidden");
|
|
12116
12268
|
} else {
|
|
@@ -13087,7 +13239,8 @@
|
|
|
13087
13239
|
command: command,
|
|
13088
13240
|
cwd: cwd || "",
|
|
13089
13241
|
mode: state.chatMode || state.config.defaultMode || "default",
|
|
13090
|
-
model: modelPref || undefined
|
|
13242
|
+
model: modelPref || undefined,
|
|
13243
|
+
thinkingEffort: state.chatThinking || undefined
|
|
13091
13244
|
}))
|
|
13092
13245
|
})
|
|
13093
13246
|
.then(function(res) { return res.json(); })
|
|
@@ -13229,7 +13382,8 @@
|
|
|
13229
13382
|
cwd: defaultCwd,
|
|
13230
13383
|
mode: mode,
|
|
13231
13384
|
initialInput: value,
|
|
13232
|
-
model: modelPref || undefined
|
|
13385
|
+
model: modelPref || undefined,
|
|
13386
|
+
thinkingEffort: state.chatThinking || undefined
|
|
13233
13387
|
}))
|
|
13234
13388
|
})
|
|
13235
13389
|
.then(function(res) { return res.json(); })
|
|
@@ -13267,7 +13421,8 @@
|
|
|
13267
13421
|
cwd: defaultCwd,
|
|
13268
13422
|
mode: mode,
|
|
13269
13423
|
initialInput: value || undefined,
|
|
13270
|
-
model: modelPref || undefined
|
|
13424
|
+
model: modelPref || undefined,
|
|
13425
|
+
thinkingEffort: state.chatThinking || undefined
|
|
13271
13426
|
}))
|
|
13272
13427
|
})
|
|
13273
13428
|
.then(function(res) { return res.json(); })
|
|
@@ -14183,7 +14338,27 @@
|
|
|
14183
14338
|
// Without an immediate refit, any chunk arriving while the keyboard
|
|
14184
14339
|
// animates in renders against the old grid and tears the screen.
|
|
14185
14340
|
if (!keyboardOpen && isKeyboardOpen) {
|
|
14341
|
+
// Snapshot bottom-pinned intent BEFORE the layout starts shifting.
|
|
14342
|
+
// visualViewport.resize fires while the keyboard is still mid-
|
|
14343
|
+
// animation on iOS; if we wait until ensureTerminalFit's rAF
|
|
14344
|
+
// body executes, clientHeight has already shrunk and the user
|
|
14345
|
+
// who was visibly at the bottom registers as "scrolled up".
|
|
14346
|
+
// We pass the snapshot through to a delayed catch-up so the
|
|
14347
|
+
// final scroll lands AFTER the animation settles.
|
|
14348
|
+
var wasStickToBottom = state.terminalAutoFollow || isTerminalNearBottom();
|
|
14186
14349
|
ensureTerminalFit("keyboard-open", { forceReplay: true });
|
|
14350
|
+
// Mirror the keyboard-close 200ms delay: by then the iOS / Android
|
|
14351
|
+
// keyboard slide-in animation is done, vv.height is final, and
|
|
14352
|
+
// scrollHeight reflects the post-replay grid. One more force
|
|
14353
|
+
// scroll closes the gap between "we scrolled during animation
|
|
14354
|
+
// when scrollHeight was still in flux" and "user expects to see
|
|
14355
|
+
// the bottom now that the keyboard has fully settled".
|
|
14356
|
+
if (wasStickToBottom) {
|
|
14357
|
+
setTimeout(function() {
|
|
14358
|
+
if (!state.terminal) return;
|
|
14359
|
+
maybeScrollTerminalToBottom("force");
|
|
14360
|
+
}, 220);
|
|
14361
|
+
}
|
|
14187
14362
|
}
|
|
14188
14363
|
|
|
14189
14364
|
// Keyboard just closed — force terminal refit and scroll to bottom
|
|
@@ -14552,6 +14727,21 @@
|
|
|
14552
14727
|
ensureTerminalFitWithRetry(reason || "fit-retry", { forceReplay: forceReplay });
|
|
14553
14728
|
return false;
|
|
14554
14729
|
}
|
|
14730
|
+
// Snapshot stick-to-bottom intent NOW, before any layout work.
|
|
14731
|
+
// Two concrete bugs this guards against:
|
|
14732
|
+
// 1. Mobile keyboard opens → visualViewport shrinks → terminal
|
|
14733
|
+
// clientHeight drops while scrollTop stays put → by the time
|
|
14734
|
+
// the rAF body runs, isTerminalNearBottom() reads false even
|
|
14735
|
+
// though the user was visibly pinned to the bottom a frame ago.
|
|
14736
|
+
// 2. Any softResyncTerminal triggered below does resetTerminal()
|
|
14737
|
+
// (scrollTop snaps to 0) then re-writes; the wterm element
|
|
14738
|
+
// can fire intermediate scroll events that flip
|
|
14739
|
+
// terminalAutoFollow to false before we get a chance to
|
|
14740
|
+
// scroll back.
|
|
14741
|
+
// Both failure modes left users mid-buffer after a resize. We
|
|
14742
|
+
// capture the intent up front and use "force" below to bypass
|
|
14743
|
+
// the (now-poisoned) flag check inside maybeScrollTerminalToBottom.
|
|
14744
|
+
var shouldStickToBottom = state.terminalAutoFollow || isTerminalNearBottom();
|
|
14555
14745
|
var prevCols = state.terminal.cols;
|
|
14556
14746
|
var prevRows = state.terminal.rows;
|
|
14557
14747
|
requestAnimationFrame(function() {
|
|
@@ -14571,8 +14761,8 @@
|
|
|
14571
14761
|
if (!didResize && forceReplay && state.terminalOutput) {
|
|
14572
14762
|
softResyncTerminal({ skipFit: true });
|
|
14573
14763
|
}
|
|
14574
|
-
if (
|
|
14575
|
-
maybeScrollTerminalToBottom("
|
|
14764
|
+
if (shouldStickToBottom) {
|
|
14765
|
+
maybeScrollTerminalToBottom("force");
|
|
14576
14766
|
}
|
|
14577
14767
|
});
|
|
14578
14768
|
});
|
|
@@ -14629,9 +14819,15 @@
|
|
|
14629
14819
|
|
|
14630
14820
|
function syncTerminalSize() {
|
|
14631
14821
|
if (!state.terminal) return;
|
|
14632
|
-
|
|
14633
|
-
|
|
14634
|
-
|
|
14822
|
+
// Force-scroll (vs the weaker maybeScrollTerminalToBottom("resize"))
|
|
14823
|
+
// for the same reason ensureTerminalFit does: between this entry
|
|
14824
|
+
// and the actual scroll, the wterm DOM may fire scroll events that
|
|
14825
|
+
// poison terminalAutoFollow. Capturing intent now + force-scrolling
|
|
14826
|
+
// keeps a user who was visibly at the bottom pinned there across
|
|
14827
|
+
// window/orientation/viewport resizes.
|
|
14828
|
+
var shouldStickToBottom = state.terminalAutoFollow || isTerminalNearBottom();
|
|
14829
|
+
if (shouldStickToBottom) {
|
|
14830
|
+
maybeScrollTerminalToBottom("force");
|
|
14635
14831
|
}
|
|
14636
14832
|
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
14637
14833
|
}
|
|
@@ -15427,21 +15623,22 @@
|
|
|
15427
15623
|
|
|
15428
15624
|
function updateAutoApproveIndicator() {
|
|
15429
15625
|
var toggle = document.getElementById("auto-approve-toggle");
|
|
15430
|
-
if (!toggle) return;
|
|
15431
15626
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
toggle
|
|
15627
|
+
// 当模式(managed / full-access)已隐含自动批准,chip 应该不存在;如果上一次渲染留下来了,
|
|
15628
|
+
// 在这里清理掉,避免视觉上还有冗余 chip。
|
|
15629
|
+
if (isAutoApproveImpliedByMode(selectedSession)) {
|
|
15630
|
+
if (toggle && toggle.parentNode) toggle.parentNode.removeChild(toggle);
|
|
15436
15631
|
return;
|
|
15437
15632
|
}
|
|
15633
|
+
if (!toggle) return;
|
|
15634
|
+
var base = "composer-pill composer-pill-chip auto-approve-indicator";
|
|
15438
15635
|
var enabled = selectedSession && selectedSession.autoApprovePermissions;
|
|
15439
15636
|
if (enabled) {
|
|
15440
|
-
toggle.className = "
|
|
15637
|
+
toggle.className = base + " active";
|
|
15441
15638
|
toggle.title = "自动批准已启用 — 点击关闭";
|
|
15442
|
-
toggle.textContent = "🛡
|
|
15639
|
+
toggle.textContent = "🛡 自动";
|
|
15443
15640
|
} else {
|
|
15444
|
-
toggle.className =
|
|
15641
|
+
toggle.className = base;
|
|
15445
15642
|
toggle.title = "自动批准已关闭 — 点击开启";
|
|
15446
15643
|
toggle.textContent = "🛡 手动";
|
|
15447
15644
|
}
|