@co0ontty/wand 1.21.5 → 1.21.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +44 -18
- package/dist/config.d.ts +34 -0
- package/dist/config.js +163 -1
- package/dist/server.js +47 -50
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +29 -0
- package/dist/structured-session-manager.js +69 -16
- package/dist/web-ui/content/scripts.js +126 -51
- package/dist/web-ui/content/styles.css +1471 -254
- package/package.json +1 -3
|
@@ -1065,6 +1065,30 @@ export class StructuredSessionManager {
|
|
|
1065
1065
|
model: undefined,
|
|
1066
1066
|
usage: undefined,
|
|
1067
1067
|
};
|
|
1068
|
+
// claude -p --output-format stream-json 在同一条消息流式生成期间会重复
|
|
1069
|
+
// emit 同一个 message.id 的 "assistant" 事件,每次 content 略多一些;子
|
|
1070
|
+
// agent 流(Task 工具)则会插入若干 parent_tool_use_id 不同的 message.id。
|
|
1071
|
+
// 朴素的 push(...content) 会让早期片段被反复合并复制,最终被 compact 出
|
|
1072
|
+
// 怪异结果,导致 UI 上 tool_use / 子 agent 输出"显示一下就消失"。
|
|
1073
|
+
// 这里按 (message.id) 去重,相同 id 视作同一消息的更新覆盖;tool_result
|
|
1074
|
+
// 用单调递增的合成 key 顺序追加。每次事件后用插入顺序重建 turnState.blocks。
|
|
1075
|
+
const blocksByKey = new Map();
|
|
1076
|
+
const keyOrder = [];
|
|
1077
|
+
let toolResultSeq = 0;
|
|
1078
|
+
const upsertBlocks = (key, blocks) => {
|
|
1079
|
+
if (!blocksByKey.has(key))
|
|
1080
|
+
keyOrder.push(key);
|
|
1081
|
+
blocksByKey.set(key, blocks);
|
|
1082
|
+
};
|
|
1083
|
+
const rebuildTurnBlocks = () => {
|
|
1084
|
+
const flat = [];
|
|
1085
|
+
for (const key of keyOrder) {
|
|
1086
|
+
const entry = blocksByKey.get(key);
|
|
1087
|
+
if (entry && entry.length > 0)
|
|
1088
|
+
flat.push(...entry);
|
|
1089
|
+
}
|
|
1090
|
+
turnState.blocks = flat;
|
|
1091
|
+
};
|
|
1068
1092
|
// Line buffer for NDJSON: chunks from stdout may split mid-line.
|
|
1069
1093
|
let lineBuf = "";
|
|
1070
1094
|
// Debounce output events to avoid flooding the WebSocket.
|
|
@@ -1141,8 +1165,15 @@ export class StructuredSessionManager {
|
|
|
1141
1165
|
this.logger?.appendStreamEvent(sessionId, parsed);
|
|
1142
1166
|
if (parsed && parsed.type === "assistant" && parsed.message) {
|
|
1143
1167
|
const extracted = this.extractAssistantMessage(parsed.message);
|
|
1168
|
+
// 用 message.id 作为 key:claude -p 流式重发同一条消息时整段覆盖
|
|
1169
|
+
// (而不是与早期片段累加),子 agent 的不同消息 id 各占一格、保留
|
|
1170
|
+
// 父子完整顺序。没有 id 时退化为合成 key 走追加模式。
|
|
1171
|
+
const msgId = typeof parsed.message.id === "string" && parsed.message.id
|
|
1172
|
+
? `assistant:${parsed.message.id}`
|
|
1173
|
+
: `assistant:anon:${keyOrder.length}`;
|
|
1144
1174
|
if (extracted.content.length > 0) {
|
|
1145
|
-
|
|
1175
|
+
upsertBlocks(msgId, extracted.content);
|
|
1176
|
+
rebuildTurnBlocks();
|
|
1146
1177
|
}
|
|
1147
1178
|
// NOTE: usage from streaming "assistant" events contains partial/incremental
|
|
1148
1179
|
// token counts (e.g. output_tokens=1 during streaming) and is NOT accurate.
|
|
@@ -1166,9 +1197,11 @@ export class StructuredSessionManager {
|
|
|
1166
1197
|
return;
|
|
1167
1198
|
}
|
|
1168
1199
|
if (parsed && parsed.type === "user" && parsed.message && Array.isArray(parsed.message.content)) {
|
|
1200
|
+
// tool_result 没有自身 id,按到达顺序用合成 key 追加(永远不被覆盖)。
|
|
1201
|
+
const collected = [];
|
|
1169
1202
|
for (const block of parsed.message.content) {
|
|
1170
1203
|
if (block && block.type === "tool_result") {
|
|
1171
|
-
|
|
1204
|
+
collected.push({
|
|
1172
1205
|
type: "tool_result",
|
|
1173
1206
|
tool_use_id: typeof block.tool_use_id === "string" ? block.tool_use_id : "",
|
|
1174
1207
|
content: this.normalizeToolResultContent(block.content),
|
|
@@ -1176,6 +1209,10 @@ export class StructuredSessionManager {
|
|
|
1176
1209
|
});
|
|
1177
1210
|
}
|
|
1178
1211
|
}
|
|
1212
|
+
if (collected.length > 0) {
|
|
1213
|
+
upsertBlocks(`tool_result:${toolResultSeq++}`, collected);
|
|
1214
|
+
rebuildTurnBlocks();
|
|
1215
|
+
}
|
|
1179
1216
|
syncSnapshot();
|
|
1180
1217
|
scheduleEmit();
|
|
1181
1218
|
return;
|
|
@@ -1476,8 +1513,16 @@ export class StructuredSessionManager {
|
|
|
1476
1513
|
model: undefined,
|
|
1477
1514
|
usage: undefined,
|
|
1478
1515
|
};
|
|
1479
|
-
// Tracks in-progress streaming blocks keyed by content_block index from stream_event
|
|
1516
|
+
// Tracks in-progress streaming blocks keyed by content_block index from stream_event.
|
|
1517
|
+
// The map is cleared whenever a complete `assistant` message arrives — its blocks
|
|
1518
|
+
// are then promoted into `finalizedBlocks` below.
|
|
1480
1519
|
const streamingBlockByIndex = new Map();
|
|
1520
|
+
// Blocks from messages that have already completed within this turn — including
|
|
1521
|
+
// the parent assistant's prior messages, every subagent assistant message, and
|
|
1522
|
+
// every tool_result. Subagent (Task tool) flows produce many assistant messages
|
|
1523
|
+
// back-to-back; without this list, each new streaming message would visually
|
|
1524
|
+
// erase everything that came before it in the same turn.
|
|
1525
|
+
const finalizedBlocks = [];
|
|
1481
1526
|
let emitTimer = null;
|
|
1482
1527
|
const flushEmit = () => {
|
|
1483
1528
|
if (emitTimer) {
|
|
@@ -1493,16 +1538,18 @@ export class StructuredSessionManager {
|
|
|
1493
1538
|
if (!emitTimer)
|
|
1494
1539
|
emitTimer = setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS);
|
|
1495
1540
|
};
|
|
1496
|
-
// Rebuild ContentBlock[] from the in-progress streaming
|
|
1541
|
+
// Rebuild ContentBlock[] from finalized history + the in-progress streaming map.
|
|
1542
|
+
// Returning only the streaming blocks would drop every prior parent/subagent
|
|
1543
|
+
// message in this turn (the original disappearing-output bug).
|
|
1497
1544
|
const rebuildStreamingBlocks = () => {
|
|
1498
1545
|
const sorted = [...streamingBlockByIndex.entries()].sort((a, b) => a[0] - b[0]);
|
|
1499
|
-
const
|
|
1546
|
+
const streaming = [];
|
|
1500
1547
|
for (const [, sb] of sorted) {
|
|
1501
1548
|
if (sb.type === "text") {
|
|
1502
|
-
|
|
1549
|
+
streaming.push({ type: "text", text: sb.text });
|
|
1503
1550
|
}
|
|
1504
1551
|
else if (sb.type === "thinking") {
|
|
1505
|
-
|
|
1552
|
+
streaming.push({ type: "thinking", thinking: sb.thinking });
|
|
1506
1553
|
}
|
|
1507
1554
|
else if (sb.type === "tool_use" && sb.id && sb.name) {
|
|
1508
1555
|
let input = {};
|
|
@@ -1512,10 +1559,10 @@ export class StructuredSessionManager {
|
|
|
1512
1559
|
}
|
|
1513
1560
|
catch { /* partial json */ }
|
|
1514
1561
|
}
|
|
1515
|
-
|
|
1562
|
+
streaming.push({ type: "tool_use", id: sb.id, name: sb.name, input });
|
|
1516
1563
|
}
|
|
1517
1564
|
}
|
|
1518
|
-
return
|
|
1565
|
+
return [...finalizedBlocks, ...streaming];
|
|
1519
1566
|
};
|
|
1520
1567
|
const syncSnapshot = () => {
|
|
1521
1568
|
const current = this.sessions.get(sessionId);
|
|
@@ -1611,14 +1658,15 @@ export class StructuredSessionManager {
|
|
|
1611
1658
|
}
|
|
1612
1659
|
continue;
|
|
1613
1660
|
}
|
|
1614
|
-
// Complete assistant turn —
|
|
1661
|
+
// Complete assistant turn — promote streaming content into the finalized
|
|
1662
|
+
// history so subsequent messages (subagents, follow-up parent messages)
|
|
1663
|
+
// append to it instead of erasing it.
|
|
1615
1664
|
if (msg.type === "assistant") {
|
|
1616
1665
|
const assistantMsg = msg;
|
|
1617
1666
|
const extracted = this.extractAssistantMessage(assistantMsg.message);
|
|
1618
|
-
|
|
1619
|
-
const toolResults = turnState.blocks.filter(b => b.type === "tool_result");
|
|
1620
|
-
turnState.blocks = [...extracted.content, ...toolResults];
|
|
1667
|
+
finalizedBlocks.push(...extracted.content);
|
|
1621
1668
|
streamingBlockByIndex.clear();
|
|
1669
|
+
turnState.blocks = rebuildStreamingBlocks();
|
|
1622
1670
|
if (assistantMsg.session_id)
|
|
1623
1671
|
turnState.sessionId = assistantMsg.session_id;
|
|
1624
1672
|
syncSnapshot();
|
|
@@ -1634,14 +1682,15 @@ export class StructuredSessionManager {
|
|
|
1634
1682
|
}
|
|
1635
1683
|
continue;
|
|
1636
1684
|
}
|
|
1637
|
-
// Tool results fed back from the claude subprocess
|
|
1685
|
+
// Tool results fed back from the claude subprocess (parent's view of a
|
|
1686
|
+
// tool call, or a subagent's tool_result during Task execution).
|
|
1638
1687
|
if (msg.type === "user") {
|
|
1639
1688
|
const userMsg = msg;
|
|
1640
1689
|
const content = Array.isArray(userMsg.message?.content) ? userMsg.message.content : [];
|
|
1641
1690
|
for (const block of content) {
|
|
1642
1691
|
const b = block;
|
|
1643
1692
|
if (b?.type === "tool_result") {
|
|
1644
|
-
|
|
1693
|
+
finalizedBlocks.push({
|
|
1645
1694
|
type: "tool_result",
|
|
1646
1695
|
tool_use_id: typeof b.tool_use_id === "string" ? b.tool_use_id : "",
|
|
1647
1696
|
content: this.normalizeToolResultContent(b.content),
|
|
@@ -1649,6 +1698,7 @@ export class StructuredSessionManager {
|
|
|
1649
1698
|
});
|
|
1650
1699
|
}
|
|
1651
1700
|
}
|
|
1701
|
+
turnState.blocks = rebuildStreamingBlocks();
|
|
1652
1702
|
syncSnapshot();
|
|
1653
1703
|
scheduleEmit();
|
|
1654
1704
|
continue;
|
|
@@ -1821,7 +1871,10 @@ export class StructuredSessionManager {
|
|
|
1821
1871
|
if (previous
|
|
1822
1872
|
&& previous.type === "text"
|
|
1823
1873
|
&& block.type === "text") {
|
|
1824
|
-
previous.text
|
|
1874
|
+
// 用新对象替换 compacted 末尾,**不要**就地改 previous.text —— previous
|
|
1875
|
+
// 通常和调用方持有的 turnState.blocks 共享引用,原地 mutate 会让下次
|
|
1876
|
+
// syncSnapshot 把已合并的内容再合并一次,呈指数级复制。
|
|
1877
|
+
compacted[compacted.length - 1] = { type: "text", text: `${previous.text}${block.text}` };
|
|
1825
1878
|
continue;
|
|
1826
1879
|
}
|
|
1827
1880
|
compacted.push(block);
|
|
@@ -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">' +
|
|
@@ -2129,10 +2137,10 @@
|
|
|
2129
2137
|
'<div class="field">' +
|
|
2130
2138
|
'<label class="field-label" for="cfg-structured-runner">结构化会话 Runner</label>' +
|
|
2131
2139
|
'<select id="cfg-structured-runner" class="field-input">' +
|
|
2132
|
-
'<option value="
|
|
2133
|
-
'<option value="
|
|
2140
|
+
'<option value="sdk">SDK(@anthropic-ai/claude-agent-sdk,默认)</option>' +
|
|
2141
|
+
'<option value="cli">CLI(spawn claude -p)</option>' +
|
|
2134
2142
|
'</select>' +
|
|
2135
|
-
'<p class="field-hint" style="margin-top:4px;">SDK 模式使用官方 Agent SDK 替代 CLI subprocess
|
|
2143
|
+
'<p class="field-hint" style="margin-top:4px;">SDK 模式使用官方 Agent SDK 替代 CLI subprocess,接口更整洁,功能等价。保存后对新建会话立即生效。</p>' +
|
|
2136
2144
|
'</div>' +
|
|
2137
2145
|
'<div class="field">' +
|
|
2138
2146
|
'<label class="field-label" for="cfg-default-model">默认模型</label>' +
|
|
@@ -2350,7 +2358,10 @@
|
|
|
2350
2358
|
if (!state.sessionsManageMode) {
|
|
2351
2359
|
return '<div class="session-manage-bar">' +
|
|
2352
2360
|
'<span class="sidebar-intro">最近的会话记录</span>' +
|
|
2353
|
-
'<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>' +
|
|
2354
2365
|
'</div>';
|
|
2355
2366
|
}
|
|
2356
2367
|
|
|
@@ -2364,13 +2375,19 @@
|
|
|
2364
2375
|
var selectAllAction = allSelected ? "clear-selection" : "select-all-visible";
|
|
2365
2376
|
var selectAllDisabled = selectable === 0 ? ' disabled' : '';
|
|
2366
2377
|
|
|
2378
|
+
// Linear-style toolbar:
|
|
2379
|
+
// [N selected] ───────── [全选] [清空] [delete (danger)] [完成 (primary)]
|
|
2367
2380
|
return '<div class="session-manage-bar active">' +
|
|
2368
|
-
'<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>' +
|
|
2369
2385
|
'<div class="session-manage-actions">' +
|
|
2370
|
-
'<button class="
|
|
2371
|
-
'<button class="
|
|
2372
|
-
'<
|
|
2373
|
-
'<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>' +
|
|
2374
2391
|
'</div>' +
|
|
2375
2392
|
'</div>';
|
|
2376
2393
|
}
|
|
@@ -2416,7 +2433,7 @@
|
|
|
2416
2433
|
? ' <span class="history-count">' + visibleHistory.length + '</span>'
|
|
2417
2434
|
: '';
|
|
2418
2435
|
var clearAllButton = state.claudeHistoryExpanded && state.claudeHistoryLoaded && visibleHistory.length > 0
|
|
2419
|
-
? '<button class="
|
|
2436
|
+
? '<button class="btn btn-danger btn-xs session-history-clear" data-action="clear-all-history" type="button">清空</button>'
|
|
2420
2437
|
: '';
|
|
2421
2438
|
var header = '<div class="session-group-title claude-history-toggle" id="claude-history-toggle">' +
|
|
2422
2439
|
'<span class="chevron">' + chevron + '</span> Claude 历史' + countBadge +
|
|
@@ -2626,7 +2643,7 @@
|
|
|
2626
2643
|
'<div class="session-group-title claude-history-directory-title">' +
|
|
2627
2644
|
'<span class="chevron">' + chevron + '</span>' +
|
|
2628
2645
|
'<span class="claude-history-directory-label">' + escapeHtml(cwdShort) + ' (' + count + ')</span>' +
|
|
2629
|
-
'<button class="
|
|
2646
|
+
'<button class="btn btn-danger btn-xs claude-history-directory-clear-btn" data-action="delete-history-directory" data-cwd="' +
|
|
2630
2647
|
escapeHtml(cwd) + '" type="button" aria-label="清空此目录的历史会话" title="清空此目录的历史会话">清空此目录</button>' +
|
|
2631
2648
|
'</div>' +
|
|
2632
2649
|
'</div>';
|
|
@@ -3461,9 +3478,17 @@
|
|
|
3461
3478
|
}
|
|
3462
3479
|
|
|
3463
3480
|
function renderWorktreeToggle(enabled) {
|
|
3464
|
-
return '<label class="session-inline-toggle" for="session-worktree-toggle">' +
|
|
3465
|
-
'<
|
|
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>' +
|
|
3466
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>' +
|
|
3467
3492
|
'</label>';
|
|
3468
3493
|
}
|
|
3469
3494
|
|
|
@@ -3492,7 +3517,7 @@
|
|
|
3492
3517
|
'<h2 class="modal-title">新对话</h2>' +
|
|
3493
3518
|
'<p class="modal-subtitle">启动 Claude 或 Codex 会话,选择 provider、会话类型、模式和工作目录。</p>' +
|
|
3494
3519
|
'</div>' +
|
|
3495
|
-
'<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>' +
|
|
3496
3521
|
'</div>' +
|
|
3497
3522
|
'<div class="modal-body">' +
|
|
3498
3523
|
'<div class="field">' +
|
|
@@ -4039,9 +4064,21 @@
|
|
|
4039
4064
|
// Volume slider
|
|
4040
4065
|
var notifVolumeEl = document.getElementById("cfg-notif-volume");
|
|
4041
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
|
+
};
|
|
4042
4078
|
if (notifVolumeEl) {
|
|
4043
4079
|
notifVolumeEl.value = state.notifVolume;
|
|
4044
4080
|
if (notifVolumeVal) notifVolumeVal.textContent = state.notifVolume + "%";
|
|
4081
|
+
_syncRangeFill(notifVolumeEl);
|
|
4045
4082
|
// Hide if sound is off
|
|
4046
4083
|
var volField = document.getElementById("notif-volume-field");
|
|
4047
4084
|
if (volField) volField.style.display = state.notifSound ? "" : "none";
|
|
@@ -4049,6 +4086,7 @@
|
|
|
4049
4086
|
notifVolumeEl.addEventListener("input", function() {
|
|
4050
4087
|
state.notifVolume = parseInt(notifVolumeEl.value, 10);
|
|
4051
4088
|
if (notifVolumeVal) notifVolumeVal.textContent = state.notifVolume + "%";
|
|
4089
|
+
_syncRangeFill(notifVolumeEl);
|
|
4052
4090
|
try { localStorage.setItem("wand-notif-volume", String(state.notifVolume)); } catch (e) {}
|
|
4053
4091
|
// Also sync to native bridge if available
|
|
4054
4092
|
if (_hasNativeBridge && typeof WandNative.setNotificationVolume === "function") {
|
|
@@ -6823,6 +6861,8 @@
|
|
|
6823
6861
|
updateDrawerState();
|
|
6824
6862
|
var modal = document.getElementById("session-modal");
|
|
6825
6863
|
if (modal) {
|
|
6864
|
+
if (modal._wandCloseTimer) { clearTimeout(modal._wandCloseTimer); modal._wandCloseTimer = null; }
|
|
6865
|
+
modal.classList.remove("closing");
|
|
6826
6866
|
modal.classList.remove("hidden");
|
|
6827
6867
|
lastFocusedElement = document.activeElement;
|
|
6828
6868
|
state.sessionTool = getPreferredTool();
|
|
@@ -6844,8 +6884,7 @@
|
|
|
6844
6884
|
state.modalOpen = false;
|
|
6845
6885
|
var modal = document.getElementById("session-modal");
|
|
6846
6886
|
if (modal) {
|
|
6847
|
-
|
|
6848
|
-
// Remove focus trap
|
|
6887
|
+
// Remove focus trap before kicking off the exit animation
|
|
6849
6888
|
if (focusTrapHandler) {
|
|
6850
6889
|
document.removeEventListener("keydown", focusTrapHandler);
|
|
6851
6890
|
focusTrapHandler = null;
|
|
@@ -6854,10 +6893,38 @@
|
|
|
6854
6893
|
if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
|
|
6855
6894
|
lastFocusedElement.focus();
|
|
6856
6895
|
}
|
|
6896
|
+
animateModalClose(modal);
|
|
6857
6897
|
}
|
|
6858
6898
|
hidePathSuggestions();
|
|
6859
6899
|
}
|
|
6860
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
|
+
|
|
6861
6928
|
function setupFocusTrap(modal) {
|
|
6862
6929
|
if (focusTrapHandler) {
|
|
6863
6930
|
document.removeEventListener("keydown", focusTrapHandler);
|
|
@@ -7054,6 +7121,8 @@
|
|
|
7054
7121
|
closeSessionModal();
|
|
7055
7122
|
var modal = document.getElementById("settings-modal");
|
|
7056
7123
|
if (modal) {
|
|
7124
|
+
if (modal._wandCloseTimer) { clearTimeout(modal._wandCloseTimer); modal._wandCloseTimer = null; }
|
|
7125
|
+
modal.classList.remove("closing");
|
|
7057
7126
|
modal.classList.remove("hidden");
|
|
7058
7127
|
lastFocusedElement = document.activeElement;
|
|
7059
7128
|
var passEl = document.getElementById("new-password");
|
|
@@ -7077,6 +7146,8 @@
|
|
|
7077
7146
|
if (volEl) {
|
|
7078
7147
|
volEl.value = state.notifVolume;
|
|
7079
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) {}
|
|
7080
7151
|
}
|
|
7081
7152
|
var volField = document.getElementById("notif-volume-field");
|
|
7082
7153
|
if (volField) volField.style.display = state.notifSound ? "" : "none";
|
|
@@ -7098,6 +7169,8 @@
|
|
|
7098
7169
|
state.notifVolume = nativeVol;
|
|
7099
7170
|
if (volEl) volEl.value = nativeVol;
|
|
7100
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) {} }
|
|
7101
7174
|
try { localStorage.setItem("wand-notif-volume", String(nativeVol)); } catch (_e) {}
|
|
7102
7175
|
} catch (_e) {}
|
|
7103
7176
|
}
|
|
@@ -7113,8 +7186,7 @@
|
|
|
7113
7186
|
function closeSettingsModal() {
|
|
7114
7187
|
var modal = document.getElementById("settings-modal");
|
|
7115
7188
|
if (modal) {
|
|
7116
|
-
|
|
7117
|
-
// Remove focus trap
|
|
7189
|
+
// Remove focus trap before kicking off the exit animation
|
|
7118
7190
|
if (focusTrapHandler) {
|
|
7119
7191
|
document.removeEventListener("keydown", focusTrapHandler);
|
|
7120
7192
|
focusTrapHandler = null;
|
|
@@ -7123,6 +7195,7 @@
|
|
|
7123
7195
|
if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
|
|
7124
7196
|
lastFocusedElement.focus();
|
|
7125
7197
|
}
|
|
7198
|
+
animateModalClose(modal);
|
|
7126
7199
|
}
|
|
7127
7200
|
}
|
|
7128
7201
|
|
|
@@ -7543,7 +7616,9 @@
|
|
|
7543
7616
|
msgEl.textContent = data.error;
|
|
7544
7617
|
msgEl.style.color = "var(--error)";
|
|
7545
7618
|
} else {
|
|
7546
|
-
msgEl.textContent =
|
|
7619
|
+
msgEl.textContent = data.restartRequired
|
|
7620
|
+
? "配置已保存,部分部署字段(host/port/https/shell)需要重启服务才生效。"
|
|
7621
|
+
: "配置已保存。";
|
|
7547
7622
|
msgEl.style.color = "var(--success)";
|
|
7548
7623
|
}
|
|
7549
7624
|
msgEl.classList.remove("hidden");
|