@co0ontty/wand 1.21.4 → 1.21.7
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/claude-pty-bridge.d.ts +4 -9
- package/dist/claude-pty-bridge.js +6 -16
- package/dist/cli.js +44 -18
- package/dist/config.d.ts +34 -0
- package/dist/config.js +165 -1
- package/dist/process-manager.js +2 -2
- package/dist/pty-text-utils.d.ts +6 -0
- package/dist/pty-text-utils.js +6 -0
- package/dist/server-session-routes.js +9 -3
- package/dist/server.js +48 -47
- package/dist/session-logger.d.ts +3 -1
- package/dist/session-logger.js +29 -16
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +29 -0
- package/dist/structured-session-manager.d.ts +33 -0
- package/dist/structured-session-manager.js +616 -31
- package/dist/types.d.ts +3 -1
- package/dist/web-ui/content/scripts.js +301 -181
- package/dist/web-ui/content/styles.css +1471 -254
- package/dist/ws-broadcast.d.ts +6 -0
- package/dist/ws-broadcast.js +25 -38
- package/package.json +2 -3
|
@@ -1277,7 +1277,7 @@
|
|
|
1277
1277
|
'<button id="sidebar-pin-btn" class="btn btn-ghost btn-sm sidebar-pin-toggle' + (state.sidebarPinned ? ' pinned' : '') + '" type="button" title="' + (state.sidebarPinned ? '取消固定侧栏' : '固定侧栏') + '">' +
|
|
1278
1278
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>' +
|
|
1279
1279
|
'</button>' +
|
|
1280
|
-
'<button id="close-drawer-button" class="btn btn-ghost btn-
|
|
1280
|
+
'<button id="close-drawer-button" class="btn btn-ghost btn-icon sidebar-close drawer-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>' +
|
|
1281
1281
|
'</div>' +
|
|
1282
1282
|
'</div>' +
|
|
1283
1283
|
'<div class="sidebar-body">' +
|
|
@@ -1467,19 +1467,15 @@
|
|
|
1467
1467
|
'</div>' +
|
|
1468
1468
|
'</div>' +
|
|
1469
1469
|
renderExpandedShortcutsRow() +
|
|
1470
|
-
// Session info bar at bottom
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
'<
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
'<span id="session-status-display" class="session-status-display">' + (selectedSession ? getSessionStatusLabel(selectedSession) : '-') + '</span>' +
|
|
1480
|
-
(selectedSession && selectedSession.provider === "claude" && selectedSession.claudeSessionId ? '<span class="session-info-separator">|</span><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>' : '') +
|
|
1481
|
-
(selectedSession && !isStructuredSession(selectedSession) ? '<span class="session-info-separator">|</span><span id="session-exit-display" class="session-exit-display">退出码=' + (selectedSession.exitCode !== undefined ? selectedSession.exitCode : 'n/a') + '</span>' : '') +
|
|
1482
|
-
'</div>' +
|
|
1470
|
+
// Session info bar at bottom — only keeps unique controls/info
|
|
1471
|
+
// (cwd / mode / status / kind are already shown in topbar or composer dropdown)
|
|
1472
|
+
(selectedSession
|
|
1473
|
+
? '<div class="input-session-info-bar">' +
|
|
1474
|
+
(selectedSession.autoApprovePermissions ? '<span id="auto-approve-toggle" class="auto-approve-indicator active" title="自动批准已启用 — 点击关闭">🛡 自动批准</span>' : '<span id="auto-approve-toggle" class="auto-approve-indicator" title="自动批准已关闭 — 点击开启">🛡 手动</span>') +
|
|
1475
|
+
(selectedSession.provider === "claude" && selectedSession.claudeSessionId ? '<span class="session-info-separator">|</span><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>' : '') +
|
|
1476
|
+
(!isStructuredSession(selectedSession) && selectedSession.exitCode !== undefined && selectedSession.exitCode !== null ? '<span class="session-info-separator">|</span><span id="session-exit-display" class="session-exit-display">退出码=' + selectedSession.exitCode + '</span>' : '') +
|
|
1477
|
+
'</div>'
|
|
1478
|
+
: '') +
|
|
1483
1479
|
'</div>' +
|
|
1484
1480
|
'<p id="action-error" class="error-message hidden"></p>' +
|
|
1485
1481
|
'</div>' +
|
|
@@ -1488,7 +1484,7 @@
|
|
|
1488
1484
|
'<div class="modal folder-picker-modal">' +
|
|
1489
1485
|
'<div class="modal-header">' +
|
|
1490
1486
|
'<h2 class="modal-title">选择工作目录</h2>' +
|
|
1491
|
-
'<button id="close-folder-picker" class="btn btn-ghost btn-icon"
|
|
1487
|
+
'<button id="close-folder-picker" 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>' +
|
|
1492
1488
|
'</div>' +
|
|
1493
1489
|
'<div class="modal-body">' +
|
|
1494
1490
|
'<div class="folder-picker-quick-row">' +
|
|
@@ -1794,7 +1790,7 @@
|
|
|
1794
1790
|
'<h2 id="quick-commit-title" class="modal-title">快捷提交</h2>' +
|
|
1795
1791
|
'<p class="modal-subtitle">' + escapeHtml((s.branch || "(no branch)") + ' · ' + (s.modifiedCount || 0) + ' 个改动') + '</p>' +
|
|
1796
1792
|
'</div>' +
|
|
1797
|
-
'<button id="quick-commit-close-btn" class="btn btn-ghost btn-icon" type="button" aria-label="关闭"
|
|
1793
|
+
'<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>' +
|
|
1798
1794
|
'</div>' +
|
|
1799
1795
|
'<div class="modal-body">' +
|
|
1800
1796
|
'<div class="qc-files-wrap">' + fileRows + '</div>' +
|
|
@@ -1804,17 +1800,23 @@
|
|
|
1804
1800
|
'</div>' +
|
|
1805
1801
|
'<textarea id="quick-commit-message" class="field-input" rows="2" placeholder="输入 commit message 或点击 AI 生成">' + escapeHtml(f.customMessage || "") + '</textarea>' +
|
|
1806
1802
|
'</div>' +
|
|
1807
|
-
'<
|
|
1808
|
-
'<
|
|
1809
|
-
'<
|
|
1810
|
-
|
|
1803
|
+
'<div class="qc-checkbox-row">' +
|
|
1804
|
+
'<label class="qc-checkbox-label" for="quick-commit-make-tag">提交后打 tag' + (s.latestTag ? '(当前:' + escapeHtml(s.latestTag) + ')' : '') + '</label>' +
|
|
1805
|
+
'<label class="qc-switch">' +
|
|
1806
|
+
'<input type="checkbox" id="quick-commit-make-tag" class="switch-toggle"' + (f.makeTag ? ' checked' : '') + '>' +
|
|
1807
|
+
'<span class="switch-slider"></span>' +
|
|
1808
|
+
'</label>' +
|
|
1809
|
+
'</div>' +
|
|
1811
1810
|
'<div class="qc-tag-row' + (f.makeTag ? '' : ' hidden') + '" id="quick-commit-tag-row">' +
|
|
1812
1811
|
'<input type="text" id="quick-commit-tag" class="field-input" placeholder="留空自动 bump patch' + (s.suggestedNextTag ? '(如 ' + escapeHtml(s.suggestedNextTag) + ')' : '') + '" value="' + escapeHtml(f.tag || "") + '">' +
|
|
1813
1812
|
'</div>' +
|
|
1814
|
-
'<
|
|
1815
|
-
'<
|
|
1816
|
-
'<
|
|
1817
|
-
|
|
1813
|
+
'<div class="qc-checkbox-row">' +
|
|
1814
|
+
'<label class="qc-checkbox-label" for="quick-commit-push">提交后 push 到远端</label>' +
|
|
1815
|
+
'<label class="qc-switch">' +
|
|
1816
|
+
'<input type="checkbox" id="quick-commit-push" class="switch-toggle"' + (f.push ? ' checked' : '') + '>' +
|
|
1817
|
+
'<span class="switch-slider"></span>' +
|
|
1818
|
+
'</label>' +
|
|
1819
|
+
'</div>' +
|
|
1818
1820
|
'<p id="quick-commit-error" class="error-message' + (state.quickCommitError ? '' : ' hidden') + '">' + escapeHtml(state.quickCommitError || "") + '</p>' +
|
|
1819
1821
|
'<div class="worktree-merge-actions">' +
|
|
1820
1822
|
'<button id="quick-commit-cancel-btn" class="btn btn-secondary" type="button">取消</button>' +
|
|
@@ -1833,7 +1835,7 @@
|
|
|
1833
1835
|
'<h2 class="modal-title">合并 Worktree</h2>' +
|
|
1834
1836
|
'<p class="modal-subtitle">检查当前任务分支并快捷合并到主分支。</p>' +
|
|
1835
1837
|
'</div>' +
|
|
1836
|
-
'<button id="close-worktree-merge-button" class="btn btn-ghost btn-icon"
|
|
1838
|
+
'<button id="close-worktree-merge-button" 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>' +
|
|
1837
1839
|
'</div>' +
|
|
1838
1840
|
'<div class="modal-body">' +
|
|
1839
1841
|
'<div id="worktree-merge-content" class="worktree-merge-content"></div>' +
|
|
@@ -1855,7 +1857,7 @@
|
|
|
1855
1857
|
'<h2 class="modal-title">设置</h2>' +
|
|
1856
1858
|
'<p class="settings-modal-subtitle">调整应用配置、通知、安全和显示偏好</p>' +
|
|
1857
1859
|
'</div>' +
|
|
1858
|
-
'<button id="close-settings-button" class="btn btn-ghost btn-icon"
|
|
1860
|
+
'<button id="close-settings-button" 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>' +
|
|
1859
1861
|
'</div>' +
|
|
1860
1862
|
'<div class="modal-body settings-modal-body">' +
|
|
1861
1863
|
'<div class="settings-layout">' +
|
|
@@ -2067,10 +2069,10 @@
|
|
|
2067
2069
|
'<span class="settings-value" id="notification-permission-status">-</span>' +
|
|
2068
2070
|
'</div>' +
|
|
2069
2071
|
'<div class="settings-update-actions">' +
|
|
2070
|
-
'<button id="notification-request-btn" class="btn btn-
|
|
2071
|
-
'<button id="notification-reset-btn" class="btn btn-
|
|
2072
|
+
'<button id="notification-request-btn" class="btn btn-primary btn-sm hidden" type="button">授权通知</button>' +
|
|
2073
|
+
'<button id="notification-reset-btn" class="btn btn-ghost btn-sm hidden" type="button">重新授权</button>' +
|
|
2072
2074
|
'<button id="notification-test-btn" class="btn btn-secondary btn-sm" type="button">发送测试通知</button>' +
|
|
2073
|
-
'<button id="notification-test-delay-btn" class="btn btn-
|
|
2075
|
+
'<button id="notification-test-delay-btn" class="btn btn-ghost btn-sm" type="button">10 秒后发送</button>' +
|
|
2074
2076
|
'</div>' +
|
|
2075
2077
|
'<p id="notification-test-message" class="hint hidden"></p>' +
|
|
2076
2078
|
'</div>' +
|
|
@@ -2092,9 +2094,15 @@
|
|
|
2092
2094
|
'<input id="cfg-port" type="number" class="field-input" placeholder="8443" min="1" max="65535" />' +
|
|
2093
2095
|
'</div>' +
|
|
2094
2096
|
'</div>' +
|
|
2095
|
-
'<div class="
|
|
2096
|
-
'<
|
|
2097
|
-
|
|
2097
|
+
'<div class="settings-toggle-row">' +
|
|
2098
|
+
'<div class="settings-toggle-text">' +
|
|
2099
|
+
'<label class="settings-toggle-title" for="cfg-https">启用 HTTPS</label>' +
|
|
2100
|
+
'<span class="settings-toggle-desc">使用自签名证书加密浏览器到服务的连接,host 为非 127.0.0.1 时建议开启。</span>' +
|
|
2101
|
+
'</div>' +
|
|
2102
|
+
'<label class="settings-switch">' +
|
|
2103
|
+
'<input id="cfg-https" type="checkbox" class="switch-toggle" />' +
|
|
2104
|
+
'<span class="switch-slider"></span>' +
|
|
2105
|
+
'</label>' +
|
|
2098
2106
|
'</div>' +
|
|
2099
2107
|
'<div class="field-row">' +
|
|
2100
2108
|
'<div class="field">' +
|
|
@@ -2126,6 +2134,14 @@
|
|
|
2126
2134
|
'</div>' +
|
|
2127
2135
|
'</div>' +
|
|
2128
2136
|
'<p class="field-hint" style="margin-top:-4px;">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
|
|
2137
|
+
'<div class="field">' +
|
|
2138
|
+
'<label class="field-label" for="cfg-structured-runner">结构化会话 Runner</label>' +
|
|
2139
|
+
'<select id="cfg-structured-runner" class="field-input">' +
|
|
2140
|
+
'<option value="sdk">SDK(@anthropic-ai/claude-agent-sdk,默认)</option>' +
|
|
2141
|
+
'<option value="cli">CLI(spawn claude -p)</option>' +
|
|
2142
|
+
'</select>' +
|
|
2143
|
+
'<p class="field-hint" style="margin-top:4px;">SDK 模式使用官方 Agent SDK 替代 CLI subprocess,接口更整洁,功能等价。保存后对新建会话立即生效。</p>' +
|
|
2144
|
+
'</div>' +
|
|
2129
2145
|
'<div class="field">' +
|
|
2130
2146
|
'<label class="field-label" for="cfg-default-model">默认模型</label>' +
|
|
2131
2147
|
'<div class="settings-row-with-action">' +
|
|
@@ -2342,7 +2358,10 @@
|
|
|
2342
2358
|
if (!state.sessionsManageMode) {
|
|
2343
2359
|
return '<div class="session-manage-bar">' +
|
|
2344
2360
|
'<span class="sidebar-intro">最近的会话记录</span>' +
|
|
2345
|
-
'<button class="session-manage-toggle" data-action="toggle-manage-mode" type="button"
|
|
2361
|
+
'<button class="btn btn-ghost btn-xs session-manage-toggle" data-action="toggle-manage-mode" type="button">' +
|
|
2362
|
+
'<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>' +
|
|
2363
|
+
'<span>管理</span>' +
|
|
2364
|
+
'</button>' +
|
|
2346
2365
|
'</div>';
|
|
2347
2366
|
}
|
|
2348
2367
|
|
|
@@ -2356,13 +2375,19 @@
|
|
|
2356
2375
|
var selectAllAction = allSelected ? "clear-selection" : "select-all-visible";
|
|
2357
2376
|
var selectAllDisabled = selectable === 0 ? ' disabled' : '';
|
|
2358
2377
|
|
|
2378
|
+
// Linear-style toolbar:
|
|
2379
|
+
// [N selected] ───────── [全选] [清空] [delete (danger)] [完成 (primary)]
|
|
2359
2380
|
return '<div class="session-manage-bar active">' +
|
|
2360
|
-
'<div class="session-manage-summary"
|
|
2381
|
+
'<div class="session-manage-summary">' +
|
|
2382
|
+
'<span class="session-manage-count">' + totalCount + '</span>' +
|
|
2383
|
+
'<span class="session-manage-summary-label">已选择</span>' +
|
|
2384
|
+
'</div>' +
|
|
2361
2385
|
'<div class="session-manage-actions">' +
|
|
2362
|
-
'<button class="
|
|
2363
|
-
'<button class="
|
|
2364
|
-
'<
|
|
2365
|
-
'<button class="
|
|
2386
|
+
'<button class="btn btn-ghost btn-xs" data-action="' + selectAllAction + '" type="button"' + selectAllDisabled + '>' + selectAllLabel + '</button>' +
|
|
2387
|
+
'<button class="btn btn-ghost btn-xs" data-action="clear-selection" type="button"' + (hasAny ? '' : ' disabled') + '>清空</button>' +
|
|
2388
|
+
'<span class="session-manage-divider"></span>' +
|
|
2389
|
+
'<button class="btn btn-danger btn-xs" data-action="delete-selected" type="button"' + (hasAny ? '' : ' disabled') + '>删除</button>' +
|
|
2390
|
+
'<button class="btn btn-primary btn-xs" data-action="toggle-manage-mode" type="button">完成</button>' +
|
|
2366
2391
|
'</div>' +
|
|
2367
2392
|
'</div>';
|
|
2368
2393
|
}
|
|
@@ -2408,7 +2433,7 @@
|
|
|
2408
2433
|
? ' <span class="history-count">' + visibleHistory.length + '</span>'
|
|
2409
2434
|
: '';
|
|
2410
2435
|
var clearAllButton = state.claudeHistoryExpanded && state.claudeHistoryLoaded && visibleHistory.length > 0
|
|
2411
|
-
? '<button class="
|
|
2436
|
+
? '<button class="btn btn-danger btn-xs session-history-clear" data-action="clear-all-history" type="button">清空</button>'
|
|
2412
2437
|
: '';
|
|
2413
2438
|
var header = '<div class="session-group-title claude-history-toggle" id="claude-history-toggle">' +
|
|
2414
2439
|
'<span class="chevron">' + chevron + '</span> Claude 历史' + countBadge +
|
|
@@ -2618,7 +2643,7 @@
|
|
|
2618
2643
|
'<div class="session-group-title claude-history-directory-title">' +
|
|
2619
2644
|
'<span class="chevron">' + chevron + '</span>' +
|
|
2620
2645
|
'<span class="claude-history-directory-label">' + escapeHtml(cwdShort) + ' (' + count + ')</span>' +
|
|
2621
|
-
'<button class="
|
|
2646
|
+
'<button class="btn btn-danger btn-xs claude-history-directory-clear-btn" data-action="delete-history-directory" data-cwd="' +
|
|
2622
2647
|
escapeHtml(cwd) + '" type="button" aria-label="清空此目录的历史会话" title="清空此目录的历史会话">清空此目录</button>' +
|
|
2623
2648
|
'</div>' +
|
|
2624
2649
|
'</div>';
|
|
@@ -3093,11 +3118,14 @@
|
|
|
3093
3118
|
// Code blocks with syntax highlighting
|
|
3094
3119
|
escaped = escaped.replace(/```(\w*)\n([\s\S]*?)```/g, function(_, lang, code) {
|
|
3095
3120
|
var highlighted = highlightCodePreview(code.trim(), lang);
|
|
3096
|
-
|
|
3121
|
+
var protectedHighlighted = highlighted.replace(/_/g, '_').replace(/\*/g, '*');
|
|
3122
|
+
return '<pre><code class="language-' + lang + '">' + protectedHighlighted + '</code></pre>';
|
|
3097
3123
|
});
|
|
3098
3124
|
|
|
3099
3125
|
// Inline code
|
|
3100
|
-
escaped = escaped.replace(/`([^`]+)`/g,
|
|
3126
|
+
escaped = escaped.replace(/`([^`]+)`/g, function(_, code) {
|
|
3127
|
+
return '<code>' + code.replace(/_/g, '_').replace(/\*/g, '*') + '</code>';
|
|
3128
|
+
});
|
|
3101
3129
|
|
|
3102
3130
|
// Headers
|
|
3103
3131
|
escaped = escaped.replace(/^######\s+(.*)$/gm, '<h6>$1</h6>');
|
|
@@ -3111,9 +3139,9 @@
|
|
|
3111
3139
|
escaped = escaped.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
3112
3140
|
escaped = escaped.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
3113
3141
|
escaped = escaped.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
3114
|
-
escaped = escaped.replace(/___(
|
|
3115
|
-
escaped = escaped.replace(/__(
|
|
3116
|
-
escaped = escaped.replace(/_(
|
|
3142
|
+
escaped = escaped.replace(/(^|[^\w])___(\S(?:[^\n]*?\S)?)___(?!\w)/g, '$1<strong><em>$2</em></strong>');
|
|
3143
|
+
escaped = escaped.replace(/(^|[^\w])__(\S(?:[^\n]*?\S)?)__(?!\w)/g, '$1<strong>$2</strong>');
|
|
3144
|
+
escaped = escaped.replace(/(^|[^\w])_(\S(?:[^\n_]*?\S)?)_(?!\w)/g, '$1<em>$2</em>');
|
|
3117
3145
|
|
|
3118
3146
|
// Strikethrough
|
|
3119
3147
|
escaped = escaped.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
|
@@ -3450,9 +3478,17 @@
|
|
|
3450
3478
|
}
|
|
3451
3479
|
|
|
3452
3480
|
function renderWorktreeToggle(enabled) {
|
|
3453
|
-
return '<label class="session-inline-toggle" for="session-worktree-toggle">' +
|
|
3454
|
-
'<
|
|
3481
|
+
return '<label class="session-inline-toggle" for="session-worktree-toggle" title="为该会话创建独立的 git worktree 分支">' +
|
|
3482
|
+
'<svg class="session-inline-toggle-icon" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
|
3483
|
+
'<circle cx="6" cy="6" r="2.2"/>' +
|
|
3484
|
+
'<circle cx="18" cy="6" r="2.2"/>' +
|
|
3485
|
+
'<circle cx="12" cy="18" r="2.2"/>' +
|
|
3486
|
+
'<path d="M6 8.2v3.4a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8.2"/>' +
|
|
3487
|
+
'<path d="M12 13.6v2.2"/>' +
|
|
3488
|
+
'</svg>' +
|
|
3455
3489
|
'<span class="session-inline-toggle-label">Worktree 模式</span>' +
|
|
3490
|
+
'<input id="session-worktree-toggle" type="checkbox" class="switch-toggle"' + (enabled ? ' checked' : '') + ' />' +
|
|
3491
|
+
'<span class="switch-slider" aria-hidden="true"></span>' +
|
|
3456
3492
|
'</label>';
|
|
3457
3493
|
}
|
|
3458
3494
|
|
|
@@ -3481,7 +3517,7 @@
|
|
|
3481
3517
|
'<h2 class="modal-title">新对话</h2>' +
|
|
3482
3518
|
'<p class="modal-subtitle">启动 Claude 或 Codex 会话,选择 provider、会话类型、模式和工作目录。</p>' +
|
|
3483
3519
|
'</div>' +
|
|
3484
|
-
'<button id="close-modal-button" class="btn btn-ghost btn-icon"
|
|
3520
|
+
'<button id="close-modal-button" 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>' +
|
|
3485
3521
|
'</div>' +
|
|
3486
3522
|
'<div class="modal-body">' +
|
|
3487
3523
|
'<div class="field">' +
|
|
@@ -4028,9 +4064,21 @@
|
|
|
4028
4064
|
// Volume slider
|
|
4029
4065
|
var notifVolumeEl = document.getElementById("cfg-notif-volume");
|
|
4030
4066
|
var notifVolumeVal = document.getElementById("cfg-notif-volume-val");
|
|
4067
|
+
// Helper to keep the iOS-style range fill in sync with the input value
|
|
4068
|
+
var _syncRangeFill = function(el) {
|
|
4069
|
+
if (!el) return;
|
|
4070
|
+
var minVal = Number(el.min || 0);
|
|
4071
|
+
var maxVal = Number(el.max || 100);
|
|
4072
|
+
var curVal = Number(el.value || 0);
|
|
4073
|
+
var pct = maxVal > minVal
|
|
4074
|
+
? Math.max(0, Math.min(100, ((curVal - minVal) / (maxVal - minVal)) * 100))
|
|
4075
|
+
: 0;
|
|
4076
|
+
el.style.setProperty("--range-fill", pct + "%");
|
|
4077
|
+
};
|
|
4031
4078
|
if (notifVolumeEl) {
|
|
4032
4079
|
notifVolumeEl.value = state.notifVolume;
|
|
4033
4080
|
if (notifVolumeVal) notifVolumeVal.textContent = state.notifVolume + "%";
|
|
4081
|
+
_syncRangeFill(notifVolumeEl);
|
|
4034
4082
|
// Hide if sound is off
|
|
4035
4083
|
var volField = document.getElementById("notif-volume-field");
|
|
4036
4084
|
if (volField) volField.style.display = state.notifSound ? "" : "none";
|
|
@@ -4038,6 +4086,7 @@
|
|
|
4038
4086
|
notifVolumeEl.addEventListener("input", function() {
|
|
4039
4087
|
state.notifVolume = parseInt(notifVolumeEl.value, 10);
|
|
4040
4088
|
if (notifVolumeVal) notifVolumeVal.textContent = state.notifVolume + "%";
|
|
4089
|
+
_syncRangeFill(notifVolumeEl);
|
|
4041
4090
|
try { localStorage.setItem("wand-notif-volume", String(state.notifVolume)); } catch (e) {}
|
|
4042
4091
|
// Also sync to native bridge if available
|
|
4043
4092
|
if (_hasNativeBridge && typeof WandNative.setNotificationVolume === "function") {
|
|
@@ -5413,40 +5462,17 @@
|
|
|
5413
5462
|
}
|
|
5414
5463
|
stripWideFillerForCopy();
|
|
5415
5464
|
|
|
5416
|
-
//
|
|
5417
|
-
//
|
|
5418
|
-
//
|
|
5419
|
-
//
|
|
5420
|
-
// 服务端(src/ws-broadcast.ts、src/process-manager.ts):
|
|
5421
|
-
// OUTPUT_DEBOUNCE_MS = 16 PTY data → ws 推送 debounce
|
|
5422
|
-
// OUTPUT_MAX_SIZE = 200_000 record.output ring buffer 字节上限
|
|
5423
|
-
//
|
|
5424
|
-
// 客户端(本文件):
|
|
5425
|
-
// CLIENT_OUTPUT_MAX / CLIENT_OUTPUT_TRIM_AT state.terminalOutput 窗口
|
|
5426
|
-
// RESYNC_THROTTLE_MS = 400 chunk → softResync 节流最小间隔
|
|
5427
|
-
// RESYNC_TAIL_MS = 350 节流尾巴 timer 等待
|
|
5428
|
-
// RESYNC_BUDGET_* 5s 内 resync 频次告警阈值
|
|
5429
|
-
// CHAT_RENDER_LIVE_MS = 150 活跃流时 renderChat debounce
|
|
5430
|
-
// CHAT_RENDER_IDLE_MS = 30 空闲时 renderChat debounce
|
|
5431
|
-
// PENDING_INPUT_TTL_MS = 5000 ws 离线输入队列 TTL
|
|
5432
|
-
// PENDING_INPUT_MAX = 100 离线队列长度上限
|
|
5433
|
-
//
|
|
5434
|
-
// 调参时的关键不变式:
|
|
5435
|
-
// OUTPUT_DEBOUNCE_MS < CHAT_RENDER_IDLE_MS ≤ CHAT_RENDER_LIVE_MS
|
|
5436
|
-
// RESYNC_TAIL_MS ≤ RESYNC_THROTTLE_MS
|
|
5437
|
-
// 否则会出现"上游推得比下游消化得快但下游 timer 还没到期"的堵塞。
|
|
5465
|
+
// PTY 链路节流不变式:
|
|
5466
|
+
// 服务端 OUTPUT_DEBOUNCE_MS < CHAT_RENDER_IDLE_MS ≤ CHAT_RENDER_LIVE_MS
|
|
5467
|
+
// RESYNC_TAIL_MS ≤ RESYNC_THROTTLE_MS
|
|
5468
|
+
// 违反这两条会出现"上游推得比下游消化得快但下游 timer 还没到期"的堵塞。
|
|
5438
5469
|
var CHAT_RENDER_LIVE_MS = 150;
|
|
5439
5470
|
var CHAT_RENDER_IDLE_MS = 30;
|
|
5440
5471
|
|
|
5441
|
-
//
|
|
5442
|
-
//
|
|
5443
|
-
//
|
|
5444
|
-
//
|
|
5445
|
-
//
|
|
5446
|
-
// 服务端 record.output 用 appendWindow(..., 200_000) 限了 200KB,这里
|
|
5447
|
-
// 客户端给一个稍宽的上限做兜底;超过就按行边界裁掉头部,行边界处
|
|
5448
|
-
// ANSI 状态机一定是 idle 状态,重放结果与未裁等价。找不到行边界时
|
|
5449
|
-
// 退化到字节切,并避开 UTF-16 半截、ANSI 半截。
|
|
5472
|
+
// state.terminalOutput 仅作 softResyncTerminal 的重放源(wterm 有自己的
|
|
5473
|
+
// scrollback),所以必须限长,否则长跑会话每次 resync 都喂几 MB 给 wterm。
|
|
5474
|
+
// 裁切优先在行边界(ANSI 状态机此时一定 idle,重放等价),找不到再按字节切
|
|
5475
|
+
// 并避开 UTF-16 半截 / ANSI 半截。
|
|
5450
5476
|
var CLIENT_OUTPUT_MAX = 256 * 1024;
|
|
5451
5477
|
var CLIENT_OUTPUT_TRIM_AT = 320 * 1024;
|
|
5452
5478
|
function clampClientTerminalOutput(buf) {
|
|
@@ -5510,17 +5536,13 @@
|
|
|
5510
5536
|
resetWideParserState();
|
|
5511
5537
|
}
|
|
5512
5538
|
|
|
5513
|
-
//
|
|
5514
|
-
//
|
|
5515
|
-
//
|
|
5516
|
-
//
|
|
5517
|
-
//
|
|
5518
|
-
//
|
|
5519
|
-
//
|
|
5520
|
-
//
|
|
5521
|
-
// 重放整 buffer 而非截短:alt screen 切换 / 滚动区 / 字符集等模式开关
|
|
5522
|
-
// 依赖从 buffer 开头开始消费。从中间切会丢失这些状态机指令,治标变
|
|
5523
|
-
// 造反。H1 已经把 buffer 限到 256KB,对 wterm WASM 来说这是 ms 级开销。
|
|
5539
|
+
// Reset wterm WASM grid and replay the full output buffer to clear stale
|
|
5540
|
+
// DOM rows left by CSI cursor-jump sequences (Claude permission menus etc.).
|
|
5541
|
+
// Replays the *whole* buffer because alt-screen / scroll-region / charset
|
|
5542
|
+
// mode switches must be consumed from the start; cutting the middle drops
|
|
5543
|
+
// those state-machine instructions and corrupts the grid.
|
|
5544
|
+
// Pass { skipFit: true } when the caller already sized the grid (e.g.
|
|
5545
|
+
// wterm.onResize fired this resync) — otherwise ensureTerminalFit recurses.
|
|
5524
5546
|
var _resyncStatsWindowStart = 0;
|
|
5525
5547
|
var _resyncStatsCount = 0;
|
|
5526
5548
|
var _resyncLastWarnAt = 0;
|
|
@@ -5566,22 +5588,13 @@
|
|
|
5566
5588
|
}, typeof delayMs === "number" ? delayMs : 150);
|
|
5567
5589
|
}
|
|
5568
5590
|
|
|
5569
|
-
// Claude CLI 的 permission 菜单 /
|
|
5570
|
-
//
|
|
5571
|
-
//
|
|
5572
|
-
// DOM 行经常残留或错位,导致新写入的内容被堆到 grid 顶部 ——
|
|
5573
|
-
// 用户体感就是"明明在改菜单,结果跑到最上面去了"。
|
|
5574
|
-
//
|
|
5575
|
-
// 兜底策略是"重置 wterm 状态机 + 重放整 buffer"(softResyncTerminal)。
|
|
5576
|
-
// 这里的关键是触发时机:旧实现用 350ms debounce,但用户实际持续
|
|
5577
|
-
// 按方向键时,每次按键都会让 PTY 回流一次原地重绘 chunk,timer
|
|
5578
|
-
// 被反复 reset,永远等不到静默期,softResync 实际从不触发——
|
|
5579
|
-
// 这是这个保护机制的根本逻辑错误。
|
|
5591
|
+
// Claude CLI 的 permission 菜单 / 选择列表在方向键下会发原地重绘序列
|
|
5592
|
+
// (CSI A-D / J / K / H / f)。wterm 在这种高频原地重绘下 DOM 行容易残留
|
|
5593
|
+
// 或错位,必须用 softResyncTerminal 兜底。
|
|
5580
5594
|
//
|
|
5581
|
-
//
|
|
5582
|
-
//
|
|
5583
|
-
//
|
|
5584
|
-
// 时由尾巴 timer 收尾。不依赖按键停顿这种永远不发生的条件。
|
|
5595
|
+
// 触发用 leading + tail 节流而非 debounce:用户持续按键时每次 chunk 都会
|
|
5596
|
+
// reset debounce timer,永远等不到静默期。leading 立即 resync、窗口内
|
|
5597
|
+
// 用尾巴 timer 收尾,不依赖按键停顿。
|
|
5585
5598
|
var IN_PLACE_REDRAW_RE = /\x1b\[\d*(?:;\d*)?[ABCDfHJK]/;
|
|
5586
5599
|
var RESYNC_THROTTLE_MS = 400;
|
|
5587
5600
|
var RESYNC_TAIL_MS = 350;
|
|
@@ -6204,7 +6217,7 @@
|
|
|
6204
6217
|
cwd: cwdOverride || getEffectiveCwd(),
|
|
6205
6218
|
mode: modeOverride || state.chatMode || (state.config && state.config.defaultMode) || "default",
|
|
6206
6219
|
provider: provider,
|
|
6207
|
-
runner: provider === "codex" ? "codex-cli-exec" : (state.structuredRunner || "claude-cli-print"),
|
|
6220
|
+
runner: provider === "codex" ? "codex-cli-exec" : ((state.config && state.config.structuredRunner === "sdk") ? "claude-sdk" : (state.structuredRunner || "claude-cli-print")),
|
|
6208
6221
|
prompt: prompt || undefined,
|
|
6209
6222
|
worktreeEnabled: worktreeEnabled === true,
|
|
6210
6223
|
model: modelPref || undefined
|
|
@@ -6371,19 +6384,28 @@
|
|
|
6371
6384
|
return block && block.__processing;
|
|
6372
6385
|
});
|
|
6373
6386
|
})());
|
|
6374
|
-
var preserveLocalStructuredProgress = (localSession.sessionKind === "structured")
|
|
6375
|
-
&& !!localStructuredState
|
|
6376
|
-
&& localStructuredState.inFlight === true
|
|
6377
|
-
&& (!serverStructuredState || serverStructuredState.inFlight !== true)
|
|
6378
|
-
&& localHasPendingAssistant
|
|
6379
|
-
&& !!localStructuredState.activeRequestId
|
|
6380
|
-
&& (!serverStructuredState || !serverStructuredState.activeRequestId || serverStructuredState.activeRequestId === localStructuredState.activeRequestId);
|
|
6381
6387
|
var localMessages = Array.isArray(localSession.messages)
|
|
6382
6388
|
? (structuredSession ? stripRenderOnlyStructuredMessages(localSession.messages) : localSession.messages)
|
|
6383
6389
|
: [];
|
|
6384
6390
|
var serverMessages = Array.isArray(serverSession.messages)
|
|
6385
6391
|
? (structuredSession ? stripRenderOnlyStructuredMessages(serverSession.messages) : serverSession.messages)
|
|
6386
6392
|
: [];
|
|
6393
|
+
// 服务端已经返回了完整的 assistant 回复(非 __processing 占位)时,
|
|
6394
|
+
// 不应再保留本地的 inFlight=true 状态,否则用户会看到"思考中"转圈永远不停。
|
|
6395
|
+
var serverHasCompletedAssistant = serverMessages.length > 0 && (function() {
|
|
6396
|
+
var last = serverMessages[serverMessages.length - 1];
|
|
6397
|
+
return last && last.role === "assistant" && Array.isArray(last.content)
|
|
6398
|
+
&& !last.content.some(function(b) { return b && b.__processing; });
|
|
6399
|
+
})();
|
|
6400
|
+
var preserveLocalStructuredProgress = (localSession.sessionKind === "structured")
|
|
6401
|
+
&& !!localStructuredState
|
|
6402
|
+
&& localStructuredState.inFlight === true
|
|
6403
|
+
&& (!serverStructuredState || serverStructuredState.inFlight !== true)
|
|
6404
|
+
&& localHasPendingAssistant
|
|
6405
|
+
&& !!localStructuredState.activeRequestId
|
|
6406
|
+
&& !!serverStructuredState && !!serverStructuredState.activeRequestId
|
|
6407
|
+
&& serverStructuredState.activeRequestId === localStructuredState.activeRequestId
|
|
6408
|
+
&& !serverHasCompletedAssistant;
|
|
6387
6409
|
var preserveLocalMessages = localMessages.length > serverMessages.length
|
|
6388
6410
|
|| (localMessages.length > 0 && serverMessages.length > 0
|
|
6389
6411
|
&& JSON.stringify(localMessages[localMessages.length - 1]) !== JSON.stringify(serverMessages[serverMessages.length - 1])
|
|
@@ -6839,6 +6861,8 @@
|
|
|
6839
6861
|
updateDrawerState();
|
|
6840
6862
|
var modal = document.getElementById("session-modal");
|
|
6841
6863
|
if (modal) {
|
|
6864
|
+
if (modal._wandCloseTimer) { clearTimeout(modal._wandCloseTimer); modal._wandCloseTimer = null; }
|
|
6865
|
+
modal.classList.remove("closing");
|
|
6842
6866
|
modal.classList.remove("hidden");
|
|
6843
6867
|
lastFocusedElement = document.activeElement;
|
|
6844
6868
|
state.sessionTool = getPreferredTool();
|
|
@@ -6860,8 +6884,7 @@
|
|
|
6860
6884
|
state.modalOpen = false;
|
|
6861
6885
|
var modal = document.getElementById("session-modal");
|
|
6862
6886
|
if (modal) {
|
|
6863
|
-
|
|
6864
|
-
// Remove focus trap
|
|
6887
|
+
// Remove focus trap before kicking off the exit animation
|
|
6865
6888
|
if (focusTrapHandler) {
|
|
6866
6889
|
document.removeEventListener("keydown", focusTrapHandler);
|
|
6867
6890
|
focusTrapHandler = null;
|
|
@@ -6870,10 +6893,38 @@
|
|
|
6870
6893
|
if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
|
|
6871
6894
|
lastFocusedElement.focus();
|
|
6872
6895
|
}
|
|
6896
|
+
animateModalClose(modal);
|
|
6873
6897
|
}
|
|
6874
6898
|
hidePathSuggestions();
|
|
6875
6899
|
}
|
|
6876
6900
|
|
|
6901
|
+
// Run the liquid-glass exit animation on a modal-backdrop, then mark it hidden.
|
|
6902
|
+
// Falls back to instant hide when reduced-motion is requested or a tab is in the background.
|
|
6903
|
+
function animateModalClose(modal) {
|
|
6904
|
+
if (!modal) return;
|
|
6905
|
+
if (modal.classList.contains("hidden")) return;
|
|
6906
|
+
var prefersReducedMotion = false;
|
|
6907
|
+
try {
|
|
6908
|
+
prefersReducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
6909
|
+
} catch (_e) {}
|
|
6910
|
+
if (prefersReducedMotion || document.hidden) {
|
|
6911
|
+
modal.classList.remove("closing");
|
|
6912
|
+
modal.classList.add("hidden");
|
|
6913
|
+
return;
|
|
6914
|
+
}
|
|
6915
|
+
// Cancel any outstanding pending hide on the same node
|
|
6916
|
+
if (modal._wandCloseTimer) {
|
|
6917
|
+
clearTimeout(modal._wandCloseTimer);
|
|
6918
|
+
modal._wandCloseTimer = null;
|
|
6919
|
+
}
|
|
6920
|
+
modal.classList.add("closing");
|
|
6921
|
+
modal._wandCloseTimer = setTimeout(function() {
|
|
6922
|
+
modal.classList.remove("closing");
|
|
6923
|
+
modal.classList.add("hidden");
|
|
6924
|
+
modal._wandCloseTimer = null;
|
|
6925
|
+
}, 170);
|
|
6926
|
+
}
|
|
6927
|
+
|
|
6877
6928
|
function setupFocusTrap(modal) {
|
|
6878
6929
|
if (focusTrapHandler) {
|
|
6879
6930
|
document.removeEventListener("keydown", focusTrapHandler);
|
|
@@ -7070,6 +7121,8 @@
|
|
|
7070
7121
|
closeSessionModal();
|
|
7071
7122
|
var modal = document.getElementById("settings-modal");
|
|
7072
7123
|
if (modal) {
|
|
7124
|
+
if (modal._wandCloseTimer) { clearTimeout(modal._wandCloseTimer); modal._wandCloseTimer = null; }
|
|
7125
|
+
modal.classList.remove("closing");
|
|
7073
7126
|
modal.classList.remove("hidden");
|
|
7074
7127
|
lastFocusedElement = document.activeElement;
|
|
7075
7128
|
var passEl = document.getElementById("new-password");
|
|
@@ -7093,6 +7146,8 @@
|
|
|
7093
7146
|
if (volEl) {
|
|
7094
7147
|
volEl.value = state.notifVolume;
|
|
7095
7148
|
if (volValEl) volValEl.textContent = state.notifVolume + "%";
|
|
7149
|
+
// Sync the iOS-style fill via the input listener (calls _syncRangeFill)
|
|
7150
|
+
try { volEl.dispatchEvent(new Event("input", { bubbles: true })); } catch (_e) {}
|
|
7096
7151
|
}
|
|
7097
7152
|
var volField = document.getElementById("notif-volume-field");
|
|
7098
7153
|
if (volField) volField.style.display = state.notifSound ? "" : "none";
|
|
@@ -7114,6 +7169,8 @@
|
|
|
7114
7169
|
state.notifVolume = nativeVol;
|
|
7115
7170
|
if (volEl) volEl.value = nativeVol;
|
|
7116
7171
|
if (volValEl) volValEl.textContent = nativeVol + "%";
|
|
7172
|
+
// Sync the iOS-style fill so the orange track matches
|
|
7173
|
+
if (volEl) { try { volEl.dispatchEvent(new Event("input", { bubbles: true })); } catch (_e) {} }
|
|
7117
7174
|
try { localStorage.setItem("wand-notif-volume", String(nativeVol)); } catch (_e) {}
|
|
7118
7175
|
} catch (_e) {}
|
|
7119
7176
|
}
|
|
@@ -7129,8 +7186,7 @@
|
|
|
7129
7186
|
function closeSettingsModal() {
|
|
7130
7187
|
var modal = document.getElementById("settings-modal");
|
|
7131
7188
|
if (modal) {
|
|
7132
|
-
|
|
7133
|
-
// Remove focus trap
|
|
7189
|
+
// Remove focus trap before kicking off the exit animation
|
|
7134
7190
|
if (focusTrapHandler) {
|
|
7135
7191
|
document.removeEventListener("keydown", focusTrapHandler);
|
|
7136
7192
|
focusTrapHandler = null;
|
|
@@ -7139,6 +7195,7 @@
|
|
|
7139
7195
|
if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
|
|
7140
7196
|
lastFocusedElement.focus();
|
|
7141
7197
|
}
|
|
7198
|
+
animateModalClose(modal);
|
|
7142
7199
|
}
|
|
7143
7200
|
}
|
|
7144
7201
|
|
|
@@ -7479,6 +7536,9 @@
|
|
|
7479
7536
|
var langEl = document.getElementById("cfg-language");
|
|
7480
7537
|
if (langEl) langEl.value = cfg.language || "";
|
|
7481
7538
|
|
|
7539
|
+
var srEl = document.getElementById("cfg-structured-runner");
|
|
7540
|
+
if (srEl) srEl.value = cfg.structuredRunner || "sdk";
|
|
7541
|
+
|
|
7482
7542
|
// Default model
|
|
7483
7543
|
state.configDefaultModel = cfg.defaultModel || "";
|
|
7484
7544
|
updateSettingsDefaultModelSelect();
|
|
@@ -7537,6 +7597,7 @@
|
|
|
7537
7597
|
shell: (document.getElementById("cfg-shell") || {}).value,
|
|
7538
7598
|
language: (document.getElementById("cfg-language") || {}).value || "",
|
|
7539
7599
|
defaultModel: (document.getElementById("cfg-default-model") || {}).value || "",
|
|
7600
|
+
structuredRunner: (document.getElementById("cfg-structured-runner") || {}).value || "sdk",
|
|
7540
7601
|
};
|
|
7541
7602
|
|
|
7542
7603
|
var previousDefaultModel = (state.config && state.config.defaultModel) || "";
|
|
@@ -7555,7 +7616,9 @@
|
|
|
7555
7616
|
msgEl.textContent = data.error;
|
|
7556
7617
|
msgEl.style.color = "var(--error)";
|
|
7557
7618
|
} else {
|
|
7558
|
-
msgEl.textContent =
|
|
7619
|
+
msgEl.textContent = data.restartRequired
|
|
7620
|
+
? "配置已保存,部分部署字段(host/port/https/shell)需要重启服务才生效。"
|
|
7621
|
+
: "配置已保存。";
|
|
7559
7622
|
msgEl.style.color = "var(--success)";
|
|
7560
7623
|
}
|
|
7561
7624
|
msgEl.classList.remove("hidden");
|
|
@@ -9288,6 +9351,9 @@
|
|
|
9288
9351
|
return Promise.resolve();
|
|
9289
9352
|
}
|
|
9290
9353
|
|
|
9354
|
+
// 防止同一会话并发提交(快速双击 / 重复触发)
|
|
9355
|
+
var _structuredSubmittingSessions = {};
|
|
9356
|
+
|
|
9291
9357
|
function postStructuredInput(input, inputBox, session) {
|
|
9292
9358
|
console.log("[WAND] postStructuredInput selectedId:", state.selectedId, "input:", input && input.substring(0, 50), "session:", session && { id: session.id, sessionKind: session.sessionKind, runner: session.runner, status: session.status, inFlight: session.structuredState && session.structuredState.inFlight });
|
|
9293
9359
|
if (!state.selectedId || !input) return Promise.resolve();
|
|
@@ -9295,6 +9361,11 @@
|
|
|
9295
9361
|
showToast("会话不存在,请重新选择或新建会话。", "error");
|
|
9296
9362
|
return Promise.resolve();
|
|
9297
9363
|
}
|
|
9364
|
+
// 同一会话的上一次提交尚未落地,直接忽略防止重复发送
|
|
9365
|
+
if (_structuredSubmittingSessions[session.id]) {
|
|
9366
|
+
console.log("[wand] postStructuredInput: duplicate submit ignored for session", session.id);
|
|
9367
|
+
return Promise.resolve();
|
|
9368
|
+
}
|
|
9298
9369
|
|
|
9299
9370
|
var isInterrupting = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
9300
9371
|
// Immediately render user message with thinking indicator
|
|
@@ -9327,23 +9398,36 @@
|
|
|
9327
9398
|
// the HTTP response arrives.
|
|
9328
9399
|
var epochBeforePost = state.queueEpoch;
|
|
9329
9400
|
|
|
9401
|
+
// 给每次发送生成唯一 idempotency key。Android WebView 进程被冻结再恢复
|
|
9402
|
+
// 的边界场景下,底层网络栈偶尔会把上次未收到响应的 POST 重发一次(前端
|
|
9403
|
+
// JS 拦不住),导致同一条消息被 backend 处理两遍。带上 key 让 backend
|
|
9404
|
+
// 在窗口内识别重发并丢弃。
|
|
9405
|
+
var idempotencyKey = (typeof crypto !== "undefined" && crypto.randomUUID)
|
|
9406
|
+
? crypto.randomUUID()
|
|
9407
|
+
: (Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10));
|
|
9408
|
+
|
|
9330
9409
|
// 用 session.id(参数绑定,in-flight 期间不变)而不是 state.selectedId
|
|
9331
9410
|
// 拼 URL,避免用户切到别的会话后 fetch 落到错误 sessionId。
|
|
9411
|
+
_structuredSubmittingSessions[session.id] = true;
|
|
9332
9412
|
return fetch("/api/structured-sessions/" + session.id + "/messages", {
|
|
9333
9413
|
method: "POST",
|
|
9334
9414
|
headers: { "Content-Type": "application/json" },
|
|
9335
9415
|
credentials: "same-origin",
|
|
9336
|
-
body: JSON.stringify({ input: input, interrupt: isInterrupting || undefined })
|
|
9416
|
+
body: JSON.stringify({ input: input, interrupt: isInterrupting || undefined, idempotencyKey: idempotencyKey })
|
|
9337
9417
|
})
|
|
9338
9418
|
.then(function(res) {
|
|
9339
9419
|
if (!res.ok) {
|
|
9340
9420
|
return res.json().catch(function() { return { error: "请求失败" }; }).then(function(payload) {
|
|
9341
|
-
|
|
9421
|
+
var err = new Error((payload && payload.error) || "无法发送结构化消息。");
|
|
9422
|
+
err.errorCode = payload && payload.errorCode;
|
|
9423
|
+
err.httpStatus = res.status;
|
|
9424
|
+
throw err;
|
|
9342
9425
|
});
|
|
9343
9426
|
}
|
|
9344
9427
|
return res.json();
|
|
9345
9428
|
})
|
|
9346
9429
|
.then(function(snapshot) {
|
|
9430
|
+
_structuredSubmittingSessions[session.id] = false;
|
|
9347
9431
|
if (snapshot && snapshot.error) {
|
|
9348
9432
|
throw new Error(snapshot.error);
|
|
9349
9433
|
}
|
|
@@ -9364,10 +9448,33 @@
|
|
|
9364
9448
|
}
|
|
9365
9449
|
})
|
|
9366
9450
|
.catch(function(error) {
|
|
9451
|
+
_structuredSubmittingSessions[session.id] = false;
|
|
9452
|
+
|
|
9453
|
+
// duplicate_idempotency_key:服务端识别出 WebView 底层重发的副本,
|
|
9454
|
+
// 直接拦截不处理。这里**不**回滚乐观更新——第一次的请求实际上已经
|
|
9455
|
+
// 被服务端接收并处理(或正在处理),ws 推送会带回真实状态;如果在
|
|
9456
|
+
// 这里把 user turn rollback 掉,第一次的 user 消息会从 UI 上消失。
|
|
9457
|
+
if (error && error.errorCode === "duplicate_idempotency_key") {
|
|
9458
|
+
showToast(error.message || "检测到重复发送,已拦截。", "warning");
|
|
9459
|
+
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
9460
|
+
return;
|
|
9461
|
+
}
|
|
9462
|
+
|
|
9463
|
+
// 回滚乐观更新:恢复发送前的 messages(去掉刚加的 userTurn)和 inFlight 状态
|
|
9464
|
+
var rollbackMsgs = userMsgs.slice(0, -1);
|
|
9367
9465
|
updateSessionSnapshot({
|
|
9368
9466
|
id: session.id,
|
|
9467
|
+
status: session.status,
|
|
9468
|
+
messages: rollbackMsgs,
|
|
9369
9469
|
structuredState: Object.assign({}, session.structuredState || {}, { inFlight: false }),
|
|
9370
9470
|
});
|
|
9471
|
+
if (session.id === state.selectedId) {
|
|
9472
|
+
state.currentMessages = buildMessagesForRender(
|
|
9473
|
+
Object.assign({}, session, { messages: rollbackMsgs, structuredState: Object.assign({}, session.structuredState || {}, { inFlight: false }) }),
|
|
9474
|
+
rollbackMsgs
|
|
9475
|
+
);
|
|
9476
|
+
renderChat(true);
|
|
9477
|
+
}
|
|
9371
9478
|
var message = (error && error.message) || "";
|
|
9372
9479
|
var isTransientAbort =
|
|
9373
9480
|
message === "Failed to fetch" ||
|
|
@@ -9559,12 +9666,8 @@
|
|
|
9559
9666
|
}, Promise.resolve());
|
|
9560
9667
|
}
|
|
9561
9668
|
|
|
9562
|
-
// pendingMessages
|
|
9563
|
-
//
|
|
9564
|
-
// 问题:用户连续按方向键时几秒就把队列填满;shift 把最早按下的丢
|
|
9565
|
-
// 掉,剩下的反而是后期按的——重连后回放的"输入序列"和用户实际
|
|
9566
|
-
// 按下的顺序矛盾。给每条消息打时间戳,flush 时直接丢弃过期项,
|
|
9567
|
-
// 让"离线超过 N 秒后重连"恢复成一个干净状态而不是错位重放。
|
|
9669
|
+
// pendingMessages 缓存 ws 离线时的输入,重连后回放。每条带时间戳,
|
|
9670
|
+
// flush 时丢弃过期项——离线 >TTL 后回放老按键序列只会让 PTY 错位。
|
|
9568
9671
|
var PENDING_INPUT_TTL_MS = 5000;
|
|
9569
9672
|
var PENDING_INPUT_MAX = 100;
|
|
9570
9673
|
function enqueuePendingInput(input) {
|
|
@@ -9634,13 +9737,6 @@
|
|
|
9634
9737
|
return Promise.resolve();
|
|
9635
9738
|
}
|
|
9636
9739
|
|
|
9637
|
-
console.log("[wand] postInput: sending", {
|
|
9638
|
-
sessionId: state.selectedId,
|
|
9639
|
-
inputLength: input.length,
|
|
9640
|
-
view: effectiveView,
|
|
9641
|
-
wsConnected: state.wsConnected
|
|
9642
|
-
});
|
|
9643
|
-
|
|
9644
9740
|
return fetch("/api/sessions/" + requestSessionId + "/input", {
|
|
9645
9741
|
method: "POST",
|
|
9646
9742
|
headers: { "Content-Type": "application/json" },
|
|
@@ -9908,25 +10004,22 @@
|
|
|
9908
10004
|
: "Enter 发送 · Shift+Enter 换行";
|
|
9909
10005
|
}
|
|
9910
10006
|
}
|
|
9911
|
-
|
|
9912
|
-
//
|
|
9913
|
-
// 输入框/发送按钮就保持可用——发送时由 ensureSessionReadyForInput 透明完成恢复。
|
|
10007
|
+
// 历史会话只要可自动恢复(Claude provider + 有 claudeSessionId),输入框/发送按钮
|
|
10008
|
+
// 就保持可用——发送时由 ensureSessionReadyForInput 透明完成恢复。
|
|
9914
10009
|
var canResumeOnSend = !structured && !isRunning && canAutoResumeSession(selectedSession);
|
|
9915
10010
|
if (composer) {
|
|
9916
10011
|
composer.placeholder = getComposerPlaceholder(selectedSession, state.terminalInteractive);
|
|
9917
|
-
composer.disabled = structured
|
|
10012
|
+
composer.disabled = !structured && !!selectedSession && !isRunning && !canResumeOnSend;
|
|
9918
10013
|
composer.setAttribute("aria-disabled", composer.disabled ? "true" : "false");
|
|
9919
|
-
//
|
|
9920
|
-
//
|
|
9921
|
-
//
|
|
9922
|
-
// 发到了 PTY、输入框里也留下了字"的双状态)。disabled 会让
|
|
9923
|
-
// textarea 失去焦点能力影响一些场景,readOnly 更轻、保留焦点。
|
|
10014
|
+
// 终端交互模式下按键由 document capture phase 透传到 PTY;用
|
|
10015
|
+
// readOnly 而非 disabled 防止 IME 组合输入等边界场景下字符同时
|
|
10016
|
+
// 落到 textarea,又保留 focus 能力。
|
|
9924
10017
|
composer.readOnly = !!state.terminalInteractive;
|
|
9925
10018
|
composer.classList.toggle("is-terminal-passthrough", !!state.terminalInteractive);
|
|
9926
10019
|
}
|
|
9927
10020
|
var sendBtn = document.getElementById("send-input-button");
|
|
9928
10021
|
if (sendBtn) {
|
|
9929
|
-
sendBtn.disabled = structured
|
|
10022
|
+
sendBtn.disabled = !structured && !!selectedSession && !isRunning && !canResumeOnSend;
|
|
9930
10023
|
sendBtn.setAttribute("title", structured
|
|
9931
10024
|
? "发送"
|
|
9932
10025
|
: (isCodex ? (isRunning ? "发送给 Codex" : "Codex 会话已结束") : (!selectedSession || isRunning || canResumeOnSend ? "发送" : "会话已结束")));
|
|
@@ -9987,15 +10080,9 @@
|
|
|
9987
10080
|
scheduleShortcutResync();
|
|
9988
10081
|
}
|
|
9989
10082
|
|
|
9990
|
-
//
|
|
9991
|
-
//
|
|
9992
|
-
//
|
|
9993
|
-
// (比如 Codex 的菜单切换),导致 DOM 行残留 / 错位 —— 表现就是用户
|
|
9994
|
-
// 反馈"按方向键之后画面错位,必须按一下右上角缩放才恢复"。
|
|
9995
|
-
// 这里在每次快捷键点击之后安排一次延迟的 softResyncTerminal 兜底,
|
|
9996
|
-
// 等价于自动按一次缩放按钮:reset 状态机 + 重放 buffer,把残留的
|
|
9997
|
-
// 错位洗掉。延迟 ~500ms 是为了让服务端先把这次按键的回执完整推过来,
|
|
9998
|
-
// 避免 resync 时只回放到 chunk 一半。
|
|
10083
|
+
// 快捷键点击后做一次延迟 resync 兜底:maybeScheduleResyncForChunk 偶尔会漏
|
|
10084
|
+
// 抓 Codex 菜单切换之类的原地重绘,导致 DOM 行残留。500ms 是为了等服务端把
|
|
10085
|
+
// 本次按键的回执完整推过来,避免 resync 只回放到 chunk 一半。
|
|
9999
10086
|
function scheduleShortcutResync() {
|
|
10000
10087
|
if (!state.terminal) return;
|
|
10001
10088
|
scheduleSoftResyncTerminal(500);
|
|
@@ -10594,12 +10681,8 @@
|
|
|
10594
10681
|
}
|
|
10595
10682
|
|
|
10596
10683
|
function updateInputPanelViewportSpacing() {
|
|
10597
|
-
//
|
|
10598
|
-
//
|
|
10599
|
-
// panel 自身撑高、内部底部多出空白,textarea(panel 顶部)反而
|
|
10600
|
-
// 被往上推、离键盘更远。新方案改为让 body 高度跟随 visualViewport
|
|
10601
|
-
// 收缩(见 syncAppViewportHeight),input-panel 自然贴键盘上沿。
|
|
10602
|
-
// 这里清掉旧 keyboard-offset,避免新旧双重补偿。
|
|
10684
|
+
// 键盘空间通过 syncAppViewportHeight 让 body 跟随 visualViewport 收缩处理;
|
|
10685
|
+
// 这里清掉历史遗留的 --keyboard-offset 避免双重补偿。
|
|
10603
10686
|
var inputPanel = document.querySelector('.input-panel');
|
|
10604
10687
|
if (!inputPanel) return;
|
|
10605
10688
|
inputPanel.style.removeProperty('--keyboard-offset');
|
|
@@ -11991,13 +12074,10 @@
|
|
|
11991
12074
|
function handleWebSocketMessage(msg) {
|
|
11992
12075
|
switch (msg.type) {
|
|
11993
12076
|
case 'output':
|
|
11994
|
-
//
|
|
11995
|
-
//
|
|
11996
|
-
//
|
|
11997
|
-
//
|
|
11998
|
-
// 主动做一次 softResyncTerminal —— 等价于自动按一次右上角缩放按钮,
|
|
11999
|
-
// 把 Claude/Codex 流式渲染中残留的错位光标定位序列洗掉。
|
|
12000
|
-
// 用 120ms 微延迟 + 单 timer 防抖,避免连续 false→true→false 触发多次重放。
|
|
12077
|
+
// For structured sessions, output may be "" during streaming — check messages too.
|
|
12078
|
+
// thinking → idle 边界自愈:bridge 把 isResponding 透传过来,true→false 时
|
|
12079
|
+
// 主动 softResyncTerminal,洗掉流式渲染残留的错位光标定位序列。
|
|
12080
|
+
// 120ms 微延迟 + 单 timer 防抖,避免连续 false→true→false 多次重放。
|
|
12001
12081
|
if (msg.data && msg.sessionId
|
|
12002
12082
|
&& Object.prototype.hasOwnProperty.call(msg.data, 'isResponding')) {
|
|
12003
12083
|
if (!state._lastIsResponding) state._lastIsResponding = {};
|
|
@@ -12192,6 +12272,8 @@
|
|
|
12192
12272
|
|
|
12193
12273
|
if (isStructuredEnded && msg.sessionId === state.selectedId) {
|
|
12194
12274
|
flushStructuredInputQueue();
|
|
12275
|
+
// 结构化会话结束时也清 localStorage,防止下次加载恢复僵尸队列
|
|
12276
|
+
clearStructuredQueuePersistence(msg.sessionId);
|
|
12195
12277
|
} else if (!isStructuredEnded) {
|
|
12196
12278
|
state.structuredInputQueue = [];
|
|
12197
12279
|
clearStructuredQueuePersistence(state.selectedId);
|
|
@@ -12235,18 +12317,12 @@
|
|
|
12235
12317
|
renderChat(true);
|
|
12236
12318
|
updateTaskDisplay();
|
|
12237
12319
|
updateApprovalStats();
|
|
12238
|
-
//
|
|
12239
|
-
//
|
|
12240
|
-
//
|
|
12241
|
-
// 全量重写、全等就直接 return false——前者会把 alt-screen
|
|
12242
|
-
// 中的 Claude TUI 切走,后者会把"应该按真实 cols 重写"的
|
|
12243
|
-
// 机会跳过。改用 replace 强制 reset+按当前 cols 重写一次,
|
|
12244
|
-
// 这是订阅时唯一可信的全量基线。
|
|
12320
|
+
// 订阅返回的是服务端 ring buffer 最新窗口,与客户端 terminalOutput
|
|
12321
|
+
// 可能不连续。强制 replace(reset + 按当前 cols 重写)是订阅时唯一
|
|
12322
|
+
// 可信的全量基线,避免 append 的 prefix 检查走错分支。
|
|
12245
12323
|
updateTerminalOutput(msg.data.output || "", msg.sessionId, "replace");
|
|
12246
|
-
//
|
|
12247
|
-
//
|
|
12248
|
-
// ResizeObserver 的回调是异步的,得用 fit-with-retry 兜
|
|
12249
|
-
// 一次,确保最终一定按真实宽度重排。
|
|
12324
|
+
// wterm 启动 cols=120,replace 写入可能落在错的列宽上;ResizeObserver
|
|
12325
|
+
// 回调异步,用 fit-with-retry 兜一次确保按真实宽度重排。
|
|
12250
12326
|
ensureTerminalFitWithRetry("init");
|
|
12251
12327
|
}
|
|
12252
12328
|
break;
|
|
@@ -12631,8 +12707,7 @@
|
|
|
12631
12707
|
var selectedForDelay = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
12632
12708
|
var isActiveStream = selectedForDelay && selectedForDelay.status === "running"
|
|
12633
12709
|
&& selectedForDelay.sessionKind !== "structured";
|
|
12634
|
-
// 活跃流时拉到
|
|
12635
|
-
// 旧实现里这两档在 setTimeout 调用时被覆盖成固定 30ms,分档逻辑形同虚设。
|
|
12710
|
+
// 活跃流时拉到 LIVE 减少高频重渲;空闲时用 IDLE 快速响应。
|
|
12636
12711
|
var delay = isActiveStream ? CHAT_RENDER_LIVE_MS : CHAT_RENDER_IDLE_MS;
|
|
12637
12712
|
chatRenderTimer = setTimeout(function() {
|
|
12638
12713
|
chatRenderTimer = null;
|
|
@@ -15005,6 +15080,49 @@
|
|
|
15005
15080
|
return source;
|
|
15006
15081
|
}
|
|
15007
15082
|
|
|
15083
|
+
function isWordChar(code) {
|
|
15084
|
+
return (code >= 48 && code <= 57) ||
|
|
15085
|
+
(code >= 65 && code <= 90) ||
|
|
15086
|
+
(code >= 97 && code <= 122) ||
|
|
15087
|
+
code === 95;
|
|
15088
|
+
}
|
|
15089
|
+
|
|
15090
|
+
function replaceUnderscoreEmphasis(source, openTag, closeTag) {
|
|
15091
|
+
var cursor = 0;
|
|
15092
|
+
while (cursor < source.length) {
|
|
15093
|
+
var start = source.indexOf("_", cursor);
|
|
15094
|
+
if (start === -1) break;
|
|
15095
|
+
var leftCode = start > 0 ? source.charCodeAt(start - 1) : 0;
|
|
15096
|
+
if (isWordChar(leftCode)) {
|
|
15097
|
+
cursor = start + 1;
|
|
15098
|
+
continue;
|
|
15099
|
+
}
|
|
15100
|
+
var searchFrom = start + 1;
|
|
15101
|
+
var end = -1;
|
|
15102
|
+
while (searchFrom < source.length) {
|
|
15103
|
+
var candidate = source.indexOf("_", searchFrom);
|
|
15104
|
+
if (candidate === -1) break;
|
|
15105
|
+
var rightIdx = candidate + 1;
|
|
15106
|
+
var rightCode = rightIdx < source.length ? source.charCodeAt(rightIdx) : 0;
|
|
15107
|
+
if (!isWordChar(rightCode)) {
|
|
15108
|
+
end = candidate;
|
|
15109
|
+
break;
|
|
15110
|
+
}
|
|
15111
|
+
searchFrom = candidate + 1;
|
|
15112
|
+
}
|
|
15113
|
+
if (end === -1) break;
|
|
15114
|
+
var inner = source.slice(start + 1, end);
|
|
15115
|
+
if (!inner) {
|
|
15116
|
+
cursor = end + 1;
|
|
15117
|
+
continue;
|
|
15118
|
+
}
|
|
15119
|
+
var replacement = openTag + inner + closeTag;
|
|
15120
|
+
source = source.slice(0, start) + replacement + source.slice(end + 1);
|
|
15121
|
+
cursor = start + replacement.length;
|
|
15122
|
+
}
|
|
15123
|
+
return source;
|
|
15124
|
+
}
|
|
15125
|
+
|
|
15008
15126
|
function replaceLinePrefix(source, marker, openTag, closeTag) {
|
|
15009
15127
|
return source.split(newline).map(function(line) {
|
|
15010
15128
|
if (line.indexOf(marker) !== 0) return line;
|
|
@@ -15066,12 +15184,13 @@
|
|
|
15066
15184
|
}
|
|
15067
15185
|
|
|
15068
15186
|
var highlighted = highlightCode(code.trim(), lang);
|
|
15187
|
+
var protectedHighlighted = highlighted.replace(/_/g, '_').replace(/\*/g, '*');
|
|
15069
15188
|
var replacement = '<div class="code-block">' +
|
|
15070
15189
|
'<div class="code-block-header">' +
|
|
15071
15190
|
'<span class="code-lang">' + (lang || "code") + '</span>' +
|
|
15072
15191
|
'<button class="code-copy">Copy</button>' +
|
|
15073
15192
|
'</div>' +
|
|
15074
|
-
'<pre><code>' +
|
|
15193
|
+
'<pre><code>' + protectedHighlighted + '</code></pre>' +
|
|
15075
15194
|
'</div>';
|
|
15076
15195
|
result = result.slice(0, start) + replacement + result.slice(endTag + 3);
|
|
15077
15196
|
pos = start + replacement.length;
|
|
@@ -15088,14 +15207,15 @@
|
|
|
15088
15207
|
continue;
|
|
15089
15208
|
}
|
|
15090
15209
|
var inlineCode = result.slice(inlineStart + 1, inlineEnd);
|
|
15091
|
-
var
|
|
15210
|
+
var protectedInlineCode = inlineCode.replace(/_/g, '_').replace(/\*/g, '*');
|
|
15211
|
+
var inlineReplacement = '<code class="code-inline">' + protectedInlineCode + '</code>';
|
|
15092
15212
|
result = result.slice(0, inlineStart) + inlineReplacement + result.slice(inlineEnd + 1);
|
|
15093
15213
|
pos = inlineStart + inlineReplacement.length;
|
|
15094
15214
|
}
|
|
15095
15215
|
|
|
15096
15216
|
result = replacePair(result, "**", '<strong>', '</strong>');
|
|
15097
15217
|
result = replacePair(result, "*", '<em>', '</em>');
|
|
15098
|
-
result =
|
|
15218
|
+
result = replaceUnderscoreEmphasis(result, '<em>', '</em>');
|
|
15099
15219
|
result = replaceLinePrefix(result, "### ", '<h3>', '</h3>');
|
|
15100
15220
|
result = replaceLinePrefix(result, "## ", '<h2>', '</h2>');
|
|
15101
15221
|
result = replaceLinePrefix(result, "# ", '<h1>', '</h1>');
|