@co0ontty/wand 1.5.2 → 1.5.5
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 +67 -14
- package/dist/server.js +0 -49
- package/dist/structured-session-manager.js +17 -0
- package/dist/web-ui/content/scripts.js +260 -131
- package/dist/web-ui/content/styles.css +133 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
# wand
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
通过浏览器远程访问和管理本地 CLI 工具的 Web 控制台。专为 Claude Code 设计,支持终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览等功能。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 安装
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- 会话持久化与恢复
|
|
9
|
-
- Claude Code 集成
|
|
10
|
-
- 文件浏览器
|
|
11
|
-
- HTTPS 安全连接(可选,配置 `https: true` 启用)
|
|
7
|
+
### 一键安装
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
自动检测并安装 Node.js(需要 v22+),然后安装 wand:
|
|
14
10
|
|
|
15
11
|
```bash
|
|
16
12
|
bash <(curl -Ls https://raw.githubusercontent.com/co0ontty/wand/master/install.sh)
|
|
17
13
|
```
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
### 手动安装
|
|
20
16
|
|
|
21
17
|
```bash
|
|
22
18
|
npm install -g @co0ontty/wand
|
|
@@ -24,16 +20,73 @@ wand init
|
|
|
24
20
|
wand web
|
|
25
21
|
```
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
安装完成后打开浏览器访问终端中提示的地址即可。
|
|
24
|
+
|
|
25
|
+
## 配置
|
|
26
|
+
|
|
27
|
+
配置文件位于 `~/.wand/config.json`,首次 `wand init` 时自动生成。
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
wand config:path # 查看配置文件路径
|
|
31
|
+
wand config:show # 查看当前配置
|
|
32
|
+
wand config:set host 0.0.0.0 # 修改配置项
|
|
33
|
+
wand config:set port 9443
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
常用配置项:
|
|
37
|
+
|
|
38
|
+
| 字段 | 默认值 | 说明 |
|
|
39
|
+
|------|--------|------|
|
|
40
|
+
| `host` | `127.0.0.1` | 监听地址,`0.0.0.0` 允许远程访问 |
|
|
41
|
+
| `port` | `8443` | 监听端口 |
|
|
42
|
+
| `https` | `false` | 启用 HTTPS(自签证书自动生成) |
|
|
43
|
+
| `password` | (随机生成) | 登录密码 |
|
|
44
|
+
| `replyLanguage` | `""` | Claude 回复语言偏好 |
|
|
45
|
+
|
|
46
|
+
## 功能
|
|
47
|
+
|
|
48
|
+
- **双视图模式** — 终端原始输出和结构化对话视图可随时切换
|
|
49
|
+
- **会话管理** — 创建、归档、恢复会话;支持从 Claude 原生历史记录恢复
|
|
50
|
+
- **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略
|
|
51
|
+
- **文件浏览器** — 内置路径浏览、搜索和收藏功能
|
|
52
|
+
- **多种运行模式** — full-access / default / auto-edit 等 Claude 运行模式
|
|
53
|
+
- **PWA 支持** — 可添加到主屏幕作为独立应用使用
|
|
54
|
+
- **HTTPS** — 可选自签证书,适合远程或移动端访问
|
|
28
55
|
|
|
29
56
|
## 开发
|
|
30
57
|
|
|
31
58
|
```bash
|
|
32
|
-
npm install
|
|
33
|
-
npm run
|
|
34
|
-
npm run
|
|
59
|
+
npm install # 安装依赖
|
|
60
|
+
npm run dev # 从源码直接启动开发服务器
|
|
61
|
+
npm run check # TypeScript 类型检查
|
|
62
|
+
npm run build # 编译 + 复制静态资源到 dist/
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
隔离测试环境(不影响生产实例):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm run dev -- -c /tmp/wand-test/config.json
|
|
35
69
|
```
|
|
36
70
|
|
|
71
|
+
## 项目结构
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
src/
|
|
75
|
+
cli.ts # CLI 入口,解析命令和参数
|
|
76
|
+
server.ts # Express 服务器、REST API、WebSocket
|
|
77
|
+
server-session-routes.ts # 会话/恢复/历史相关路由
|
|
78
|
+
process-manager.ts # PTY 会话编排、输入输出路由、权限处理
|
|
79
|
+
claude-pty-bridge.ts # PTY 输出解析为结构化对话数据
|
|
80
|
+
storage.ts # SQLite 持久化
|
|
81
|
+
config.ts # 配置加载与合并
|
|
82
|
+
session-lifecycle.ts # 会话状态机(idle/thinking/waiting/archived)
|
|
83
|
+
session-logger.ts # 文件日志 ~/.wand/sessions/
|
|
84
|
+
resume-policy.ts # Claude 历史绑定与恢复策略
|
|
85
|
+
web-ui/ # 服务端渲染的前端 HTML/CSS/JS
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
数据存储在 `~/.wand/` 下:`config.json`(配置)、`wand.db`(SQLite)、`sessions/`(日志)。
|
|
89
|
+
|
|
37
90
|
## License
|
|
38
91
|
|
|
39
|
-
MIT
|
|
92
|
+
MIT
|
package/dist/server.js
CHANGED
|
@@ -604,55 +604,6 @@ export async function startServer(config, configPath) {
|
|
|
604
604
|
{ path: "/", name: "根目录", icon: "📁" },
|
|
605
605
|
]);
|
|
606
606
|
});
|
|
607
|
-
app.get("/api/favorite-paths", (_req, res) => {
|
|
608
|
-
const stored = storage.getConfigValue("favorite_paths");
|
|
609
|
-
const favorites = parseStoredPathList(stored);
|
|
610
|
-
res.json(favorites.filter((f) => !isBlockedFolderPath(normalizeFolderPath(f.path))));
|
|
611
|
-
});
|
|
612
|
-
app.post("/api/favorite-paths", (req, res) => {
|
|
613
|
-
const { path: favPath, name, icon } = req.body;
|
|
614
|
-
if (!favPath) {
|
|
615
|
-
res.status(400).json({ error: "路径不能为空。" });
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
const resolvedFavoritePath = normalizeFolderPath(favPath);
|
|
619
|
-
if (isBlockedFolderPath(resolvedFavoritePath)) {
|
|
620
|
-
res.status(403).json({ error: "访问被拒绝:无法收藏系统敏感目录。" });
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
const stored = storage.getConfigValue("favorite_paths");
|
|
624
|
-
const favorites = parseStoredPathList(stored);
|
|
625
|
-
if (favorites.some((f) => normalizeFolderPath(f.path) === resolvedFavoritePath)) {
|
|
626
|
-
res.status(400).json({ error: "该路径已在收藏列表中。" });
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
const newFavorite = {
|
|
630
|
-
path: resolvedFavoritePath,
|
|
631
|
-
name: name || path.basename(resolvedFavoritePath),
|
|
632
|
-
icon: icon || "⭐",
|
|
633
|
-
addedAt: new Date().toISOString(),
|
|
634
|
-
};
|
|
635
|
-
favorites.push(newFavorite);
|
|
636
|
-
storage.setConfigValue("favorite_paths", JSON.stringify(favorites));
|
|
637
|
-
res.status(201).json(newFavorite);
|
|
638
|
-
});
|
|
639
|
-
app.delete("/api/favorite-paths", (req, res) => {
|
|
640
|
-
const { path: delPath } = req.body;
|
|
641
|
-
if (!delPath) {
|
|
642
|
-
res.status(400).json({ error: "路径不能为空。" });
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
const stored = storage.getConfigValue("favorite_paths");
|
|
646
|
-
const favorites = parseStoredPathList(stored);
|
|
647
|
-
const index = favorites.findIndex((f) => f.path === delPath);
|
|
648
|
-
if (index === -1) {
|
|
649
|
-
res.status(404).json({ error: "未找到该收藏路径。" });
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
favorites.splice(index, 1);
|
|
653
|
-
storage.setConfigValue("favorite_paths", JSON.stringify(favorites));
|
|
654
|
-
res.json({ ok: true });
|
|
655
|
-
});
|
|
656
607
|
app.get("/api/recent-paths", (_req, res) => {
|
|
657
608
|
const stored = storage.getConfigValue("recent_paths");
|
|
658
609
|
const recent = parseStoredPathList(stored);
|
|
@@ -627,6 +627,23 @@ export class StructuredSessionManager {
|
|
|
627
627
|
sessionId,
|
|
628
628
|
data: { status: finished.status, exitCode: 0, messages: finished.messages, sessionKind: "structured", structuredState: finished.structuredState },
|
|
629
629
|
});
|
|
630
|
+
// Auto-continue after plan mode exit: when Claude calls ExitPlanMode,
|
|
631
|
+
// the `-p` process exits because stdin is "ignore" and it cannot get
|
|
632
|
+
// user confirmation. Detect this and automatically resume execution
|
|
633
|
+
// so the plan is actually carried out.
|
|
634
|
+
const lastToolUse = [...turnState.blocks].reverse().find((b) => b.type === "tool_use");
|
|
635
|
+
if (lastToolUse && lastToolUse.name === "ExitPlanMode" && turnState.sessionId) {
|
|
636
|
+
console.log("[WAND] ExitPlanMode detected – auto-continuing plan execution for session:", sessionId);
|
|
637
|
+
resolve();
|
|
638
|
+
// Schedule the continuation outside the current promise chain so it
|
|
639
|
+
// does not block the close handler.
|
|
640
|
+
setImmediate(() => {
|
|
641
|
+
this.sendMessage(sessionId, "Plan approved. Proceed with the implementation.").catch((err) => {
|
|
642
|
+
console.error("[WAND] Auto-continue after ExitPlanMode failed:", err);
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
630
647
|
resolve();
|
|
631
648
|
});
|
|
632
649
|
});
|
|
@@ -164,10 +164,13 @@
|
|
|
164
164
|
var _statusBarStartTime = 0;
|
|
165
165
|
|
|
166
166
|
function renderStructuredStatusBar(chatMessages, session) {
|
|
167
|
-
//
|
|
168
|
-
var
|
|
167
|
+
// Status bar now lives above the input-composer, inside .input-panel
|
|
168
|
+
var inputPanel = document.querySelector(".input-panel");
|
|
169
|
+
var existing = document.querySelector(".structured-status-bar");
|
|
170
|
+
var composer = document.querySelector(".input-composer");
|
|
169
171
|
if (!session || !isStructuredSession(session)) {
|
|
170
172
|
if (existing) existing.remove();
|
|
173
|
+
if (composer) composer.classList.remove("in-flight");
|
|
171
174
|
clearInterval(_statusBarTimerId);
|
|
172
175
|
_statusBarTimerId = null;
|
|
173
176
|
return;
|
|
@@ -181,21 +184,26 @@
|
|
|
181
184
|
_statusBarStartTime = Date.now();
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
|
|
187
|
+
// Add glow to input composer
|
|
188
|
+
if (composer) composer.classList.add("in-flight");
|
|
189
|
+
|
|
190
|
+
if (!existing && inputPanel && composer) {
|
|
185
191
|
var bar = document.createElement("div");
|
|
186
192
|
bar.className = "structured-status-bar";
|
|
187
193
|
bar.innerHTML =
|
|
194
|
+
'<span class="status-bar-dot"></span>' +
|
|
188
195
|
'<span class="status-bar-label">回复中</span>' +
|
|
189
|
-
'<div class="status-bar-track"><div class="status-bar-fill"></div></div>' +
|
|
190
196
|
'<span class="status-bar-timer">0.0s</span>';
|
|
191
|
-
//
|
|
192
|
-
|
|
197
|
+
// Insert right before the input-composer element
|
|
198
|
+
inputPanel.insertBefore(bar, composer);
|
|
193
199
|
existing = bar;
|
|
194
|
-
} else if (existing.classList.contains("completed")) {
|
|
200
|
+
} else if (existing && existing.classList.contains("completed")) {
|
|
195
201
|
// Was completed, now in-flight again — reset
|
|
196
202
|
existing.classList.remove("completed");
|
|
197
203
|
existing.style.animation = "none";
|
|
198
204
|
existing.querySelector(".status-bar-label").textContent = "回复中";
|
|
205
|
+
var dot = existing.querySelector(".status-bar-dot");
|
|
206
|
+
if (dot) dot.style.display = "";
|
|
199
207
|
_statusBarStartTime = Date.now();
|
|
200
208
|
}
|
|
201
209
|
|
|
@@ -214,12 +222,17 @@
|
|
|
214
222
|
clearInterval(_statusBarTimerId);
|
|
215
223
|
_statusBarTimerId = null;
|
|
216
224
|
|
|
225
|
+
// Remove glow from input composer
|
|
226
|
+
if (composer) composer.classList.remove("in-flight");
|
|
227
|
+
|
|
217
228
|
if (existing && !existing.classList.contains("completed")) {
|
|
218
229
|
// Just finished — transition to completed state
|
|
219
230
|
var elapsed = _statusBarStartTime ? ((Date.now() - _statusBarStartTime) / 1000).toFixed(1) : "0.0";
|
|
220
231
|
existing.classList.add("completed");
|
|
221
232
|
existing.querySelector(".status-bar-label").textContent = "完成";
|
|
222
233
|
existing.querySelector(".status-bar-timer").textContent = elapsed + "s";
|
|
234
|
+
var dot = existing.querySelector(".status-bar-dot");
|
|
235
|
+
if (dot) dot.style.display = "none";
|
|
223
236
|
_statusBarStartTime = 0;
|
|
224
237
|
// Remove after animation ends
|
|
225
238
|
setTimeout(function() {
|
|
@@ -471,6 +484,8 @@
|
|
|
471
484
|
if (!state.selectedId) return "";
|
|
472
485
|
var isTerminal = state.currentView === "terminal";
|
|
473
486
|
if (!isTerminal) return "";
|
|
487
|
+
var sel = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
488
|
+
if (sel && isStructuredSession(sel)) return "";
|
|
474
489
|
var keys = renderShortcutKeys();
|
|
475
490
|
var arrow = state.shortcutsExpanded ? '›' : '‹';
|
|
476
491
|
return '<div class="inline-shortcuts-wrap' + (state.shortcutsExpanded ? ' expanded' : '') + '">' +
|
|
@@ -484,6 +499,8 @@
|
|
|
484
499
|
if (!state.selectedId) return "";
|
|
485
500
|
var isTerminal = state.currentView === "terminal";
|
|
486
501
|
if (!isTerminal) return "";
|
|
502
|
+
var sel = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
503
|
+
if (sel && isStructuredSession(sel)) return "";
|
|
487
504
|
return '<div class="inline-shortcuts-expanded-row' + (state.shortcutsExpanded ? ' visible' : '') + '">' + renderShortcutKeys() + '</div>';
|
|
488
505
|
}
|
|
489
506
|
|
|
@@ -700,12 +717,11 @@
|
|
|
700
717
|
'<span id="session-mode-display" class="session-mode-display">' + (selectedSession ? getModeLabel(selectedSession.mode) : '默认') + '</span>' +
|
|
701
718
|
(selectedSession && selectedSession.autoApprovePermissions ? '<span class="session-info-separator">|</span><span id="auto-approve-toggle" class="auto-approve-indicator active" title="自动批准已启用 — 点击关闭">🛡 自动批准</span>' : '<span class="session-info-separator">|</span><span id="auto-approve-toggle" class="auto-approve-indicator" title="自动批准已关闭 — 点击开启">🛡 手动</span>') +
|
|
702
719
|
'<span class="session-info-separator">|</span>' +
|
|
703
|
-
'<span id="session-kind-display" class="session-kind-display">' + (selectedSession ? (
|
|
720
|
+
'<span id="session-kind-display" class="session-kind-display">' + (selectedSession ? getSessionKindLabel(selectedSession) : '终端') + '</span>' +
|
|
704
721
|
'<span class="session-info-separator">|</span>' +
|
|
705
722
|
'<span id="session-status-display" class="session-status-display">' + (selectedSession ? getSessionStatusLabel(selectedSession) : '-') + '</span>' +
|
|
706
723
|
(selectedSession && 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>' : '') +
|
|
707
|
-
'<span class="session-info-separator">|</span>' +
|
|
708
|
-
'<span id="session-exit-display" class="session-exit-display">exit=' + (selectedSession && selectedSession.exitCode !== undefined ? selectedSession.exitCode : 'n/a') + '</span>' +
|
|
724
|
+
(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>' : '') +
|
|
709
725
|
'</div>' +
|
|
710
726
|
'</div>' +
|
|
711
727
|
'<p id="action-error" class="error-message hidden"></p>' +
|
|
@@ -1030,14 +1046,13 @@
|
|
|
1030
1046
|
|
|
1031
1047
|
function selectAllVisibleItems() {
|
|
1032
1048
|
var nextSessionIds = {};
|
|
1033
|
-
state.sessions.
|
|
1049
|
+
state.sessions.filter(function(session) {
|
|
1050
|
+
return !session.resumedToSessionId;
|
|
1051
|
+
}).forEach(function(session) {
|
|
1034
1052
|
nextSessionIds[session.id] = true;
|
|
1035
1053
|
});
|
|
1036
|
-
var cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
1037
1054
|
var nextHistoryIds = {};
|
|
1038
|
-
getVisibleClaudeHistorySessions().
|
|
1039
|
-
return !session.timestamp || new Date(session.timestamp).getTime() <= cutoff;
|
|
1040
|
-
}).forEach(function(session) {
|
|
1055
|
+
getVisibleClaudeHistorySessions().forEach(function(session) {
|
|
1041
1056
|
nextHistoryIds[session.claudeSessionId] = true;
|
|
1042
1057
|
});
|
|
1043
1058
|
state.selectedSessionIds = nextSessionIds;
|
|
@@ -1744,7 +1759,15 @@
|
|
|
1744
1759
|
if (!session) return "";
|
|
1745
1760
|
if (session.archived) return "已归档";
|
|
1746
1761
|
if (session.permissionBlocked) return "等待授权";
|
|
1747
|
-
|
|
1762
|
+
var statusMap = {
|
|
1763
|
+
"stopped": "已停止",
|
|
1764
|
+
"running": "运行中",
|
|
1765
|
+
"idle": "空闲",
|
|
1766
|
+
"thinking": "思考中",
|
|
1767
|
+
"waiting-input": "等待输入",
|
|
1768
|
+
"initializing": "启动中"
|
|
1769
|
+
};
|
|
1770
|
+
return statusMap[session.status] || session.status;
|
|
1748
1771
|
}
|
|
1749
1772
|
|
|
1750
1773
|
function getSessionStatusClass(session) {
|
|
@@ -2313,19 +2336,6 @@
|
|
|
2313
2336
|
var selectedIndex = -1;
|
|
2314
2337
|
var folderItems = [];
|
|
2315
2338
|
|
|
2316
|
-
function saveWorkingDir(path) {
|
|
2317
|
-
state.workingDir = path;
|
|
2318
|
-
try {
|
|
2319
|
-
localStorage.setItem("wand-working-dir", path);
|
|
2320
|
-
} catch (e) {
|
|
2321
|
-
// Ignore localStorage errors
|
|
2322
|
-
}
|
|
2323
|
-
// Also add to recent paths (defined later, will be called after function is available)
|
|
2324
|
-
if (typeof addRecentPath === "function") {
|
|
2325
|
-
addRecentPath(path);
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
2339
|
// Helper functions for path validation feedback
|
|
2330
2340
|
function showValidationError(message) {
|
|
2331
2341
|
if (folderPickerInput) {
|
|
@@ -2349,69 +2359,44 @@
|
|
|
2349
2359
|
}
|
|
2350
2360
|
}
|
|
2351
2361
|
|
|
2352
|
-
// Helper functions for recent paths
|
|
2353
|
-
|
|
2354
|
-
try {
|
|
2355
|
-
var saved = localStorage.getItem("wand-recent-paths");
|
|
2356
|
-
return saved ? JSON.parse(saved) : [];
|
|
2357
|
-
} catch (e) {
|
|
2358
|
-
return [];
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
|
-
function addRecentPath(path) {
|
|
2363
|
-
var recent = getRecentPaths();
|
|
2364
|
-
// Remove if already exists
|
|
2365
|
-
recent = recent.filter(function(p) { return p !== path; });
|
|
2366
|
-
// Add to front
|
|
2367
|
-
recent.unshift(path);
|
|
2368
|
-
// Keep only last 5
|
|
2369
|
-
recent = recent.slice(0, 5);
|
|
2370
|
-
try {
|
|
2371
|
-
localStorage.setItem("wand-recent-paths", JSON.stringify(recent));
|
|
2372
|
-
} catch (e) {
|
|
2373
|
-
// Ignore localStorage errors
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
function renderRecentPaths() {
|
|
2378
|
-
var recent = getRecentPaths();
|
|
2379
|
-
if (recent.length === 0) return "";
|
|
2362
|
+
// Helper functions for recent paths (single source: backend API)
|
|
2363
|
+
// NOTE: fetchRecentPaths and addRecentPath are defined at outer scope
|
|
2380
2364
|
|
|
2365
|
+
function renderRecentPathsHtml(items) {
|
|
2366
|
+
if (!items.length) return "";
|
|
2381
2367
|
var html = '<div class="folder-recent-section">' +
|
|
2382
2368
|
'<div class="folder-recent-title">最近使用</div>';
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
html += '<div class="folder-recent-item" data-path="' + escapeHtml(
|
|
2386
|
-
'<span class="folder-recent-item-
|
|
2387
|
-
'<span class="folder-recent-item-path">' + escapeHtml(path) + '</span>' +
|
|
2369
|
+
items.forEach(function(item) {
|
|
2370
|
+
var p = item.path || item;
|
|
2371
|
+
html += '<div class="folder-recent-item" data-path="' + escapeHtml(p) + '">' +
|
|
2372
|
+
'<span class="folder-recent-item-path">' + escapeHtml(p) + '</span>' +
|
|
2388
2373
|
'</div>';
|
|
2389
2374
|
});
|
|
2390
|
-
|
|
2391
2375
|
html += '</div>';
|
|
2392
2376
|
return html;
|
|
2393
2377
|
}
|
|
2394
2378
|
|
|
2395
2379
|
function showRecentPathsDropdown() {
|
|
2396
2380
|
if (!folderPickerDropdown) return;
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2381
|
+
fetchRecentPaths(function(items) {
|
|
2382
|
+
var recentHtml = renderRecentPathsHtml(items);
|
|
2383
|
+
if (recentHtml) {
|
|
2384
|
+
folderPickerDropdown.innerHTML = recentHtml;
|
|
2385
|
+
folderPickerDropdown.classList.remove("hidden");
|
|
2386
|
+
folderPickerDropdown.querySelectorAll(".folder-recent-item").forEach(function(item) {
|
|
2387
|
+
item.addEventListener("click", function() {
|
|
2388
|
+
var path = this.dataset.path;
|
|
2389
|
+
if (folderPickerInput) {
|
|
2390
|
+
folderPickerInput.value = path;
|
|
2391
|
+
saveWorkingDir(path);
|
|
2392
|
+
loadFolderSuggestions(path);
|
|
2393
|
+
}
|
|
2394
|
+
});
|
|
2410
2395
|
});
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
}
|
|
2396
|
+
} else {
|
|
2397
|
+
hideFolderDropdown();
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2415
2400
|
}
|
|
2416
2401
|
|
|
2417
2402
|
// Working directory indicator click handler for active sessions
|
|
@@ -2472,7 +2457,6 @@
|
|
|
2472
2457
|
var path = entry.fullPath;
|
|
2473
2458
|
folderPickerInput.value = path;
|
|
2474
2459
|
saveWorkingDir(path);
|
|
2475
|
-
addRecentPath(path);
|
|
2476
2460
|
loadFolderSuggestions(path);
|
|
2477
2461
|
break;
|
|
2478
2462
|
}
|
|
@@ -2703,6 +2687,32 @@
|
|
|
2703
2687
|
setupVisualViewportHandlers();
|
|
2704
2688
|
}
|
|
2705
2689
|
|
|
2690
|
+
function saveWorkingDir(path) {
|
|
2691
|
+
state.workingDir = path;
|
|
2692
|
+
try {
|
|
2693
|
+
localStorage.setItem("wand-working-dir", path);
|
|
2694
|
+
} catch (e) {
|
|
2695
|
+
// Ignore localStorage errors
|
|
2696
|
+
}
|
|
2697
|
+
addRecentPath(path);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
function fetchRecentPaths(callback) {
|
|
2701
|
+
fetch("/api/recent-paths", { credentials: "same-origin" })
|
|
2702
|
+
.then(function(res) { return res.json(); })
|
|
2703
|
+
.then(function(items) { callback(items || []); })
|
|
2704
|
+
.catch(function() { callback([]); });
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
function addRecentPath(path) {
|
|
2708
|
+
return fetch("/api/recent-paths", {
|
|
2709
|
+
method: "POST",
|
|
2710
|
+
headers: { "Content-Type": "application/json" },
|
|
2711
|
+
credentials: "same-origin",
|
|
2712
|
+
body: JSON.stringify({ path: path })
|
|
2713
|
+
}).catch(function() {});
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2706
2716
|
function activateSessionItem(sessionId) {
|
|
2707
2717
|
var session = state.sessions.find(function(s) { return s.id === sessionId; });
|
|
2708
2718
|
if (session && session.status !== "running" && !isStructuredSession(session)) {
|
|
@@ -3503,13 +3513,13 @@
|
|
|
3503
3513
|
}
|
|
3504
3514
|
|
|
3505
3515
|
function getSessionKindLabel(session) {
|
|
3506
|
-
return isStructuredSession(session) ? "
|
|
3516
|
+
return isStructuredSession(session) ? "结构化" : "终端";
|
|
3507
3517
|
}
|
|
3508
3518
|
|
|
3509
3519
|
function getSessionKindDescription(session) {
|
|
3510
3520
|
return isStructuredSession(session)
|
|
3511
|
-
? "
|
|
3512
|
-
: "
|
|
3521
|
+
? "结构化 · 块级记录"
|
|
3522
|
+
: "终端 · PTY 会话";
|
|
3513
3523
|
}
|
|
3514
3524
|
|
|
3515
3525
|
function isRecoverableToolError(toolResult, nextResult) {
|
|
@@ -3843,8 +3853,9 @@
|
|
|
3843
3853
|
var exitEl = document.getElementById("session-exit-display");
|
|
3844
3854
|
var cwdText = selectedSession && selectedSession.cwd ? selectedSession.cwd : "未设置目录";
|
|
3845
3855
|
var modeText = selectedSession ? getModeLabel(selectedSession.mode) : "默认";
|
|
3846
|
-
var kindText = selectedSession ? getSessionKindLabel(selectedSession) : "
|
|
3847
|
-
var
|
|
3856
|
+
var kindText = selectedSession ? getSessionKindLabel(selectedSession) : "终端";
|
|
3857
|
+
var isStructured = selectedSession && isStructuredSession(selectedSession);
|
|
3858
|
+
var exitText = isStructured ? "" : "退出码=" + (selectedSession && selectedSession.exitCode !== undefined ? selectedSession.exitCode : "n/a");
|
|
3848
3859
|
if (cwdEl && cwdEl.textContent !== cwdText) cwdEl.textContent = cwdText;
|
|
3849
3860
|
if (modeEl && modeEl.textContent !== modeText) modeEl.textContent = modeText;
|
|
3850
3861
|
if (kindEl && kindEl.textContent !== kindText) kindEl.textContent = kindText;
|
|
@@ -4560,6 +4571,7 @@
|
|
|
4560
4571
|
syncComposerModeSelect();
|
|
4561
4572
|
return createStructuredSession(undefined, cwd, mode)
|
|
4562
4573
|
.then(function(data) {
|
|
4574
|
+
saveWorkingDir(cwd);
|
|
4563
4575
|
closeSessionModal();
|
|
4564
4576
|
closeSessionsDrawer();
|
|
4565
4577
|
return data;
|
|
@@ -4597,6 +4609,7 @@
|
|
|
4597
4609
|
state.selectedId = data.id;
|
|
4598
4610
|
console.log("[WAND] runPtyCommandFromModal created session:", data.id, "sessionKind:", data.sessionKind, "runner:", data.runner);
|
|
4599
4611
|
persistSelectedId();
|
|
4612
|
+
saveWorkingDir(cwd);
|
|
4600
4613
|
state.drafts[data.id] = "";
|
|
4601
4614
|
resetChatRenderCache();
|
|
4602
4615
|
closeSessionModal();
|
|
@@ -4655,18 +4668,14 @@
|
|
|
4655
4668
|
function loadBlankChatCwdDropdown(dropdown) {
|
|
4656
4669
|
var defaultCwd = getConfigCwd();
|
|
4657
4670
|
dropdown.innerHTML = '<div class="blank-chat-cwd-loading">加载中...</div>';
|
|
4658
|
-
|
|
4659
|
-
.then(function(res) { return res.json(); })
|
|
4660
|
-
.then(function(items) {
|
|
4671
|
+
fetchRecentPaths(function(items) {
|
|
4661
4672
|
var html = "";
|
|
4662
|
-
// Default directory always first
|
|
4663
4673
|
var currentDir = state.workingDir || defaultCwd;
|
|
4664
4674
|
html += '<div class="blank-chat-cwd-item' + (currentDir === defaultCwd ? " active" : "") + '" data-path="' + escapeHtml(defaultCwd) + '">' +
|
|
4665
4675
|
'<span class="blank-chat-cwd-item-label">默认</span>' +
|
|
4666
4676
|
'<span class="blank-chat-cwd-item-path">' + escapeHtml(defaultCwd) + '</span>' +
|
|
4667
4677
|
'</div>';
|
|
4668
|
-
|
|
4669
|
-
if (items && items.length) {
|
|
4678
|
+
if (items.length) {
|
|
4670
4679
|
var seen = {};
|
|
4671
4680
|
seen[defaultCwd] = true;
|
|
4672
4681
|
items.forEach(function(item) {
|
|
@@ -4689,26 +4698,18 @@
|
|
|
4689
4698
|
dropdown.classList.add("hidden");
|
|
4690
4699
|
var arrow = document.getElementById("blank-chat-cwd-arrow");
|
|
4691
4700
|
if (arrow) arrow.textContent = "▼";
|
|
4692
|
-
// Update folder picker input if exists
|
|
4693
4701
|
var fpInput = document.getElementById("folder-picker-input");
|
|
4694
4702
|
if (fpInput) fpInput.value = path;
|
|
4695
4703
|
});
|
|
4696
4704
|
});
|
|
4697
|
-
|
|
4698
|
-
.catch(function() {
|
|
4699
|
-
dropdown.innerHTML = '<div class="blank-chat-cwd-item" data-path="' + escapeHtml(defaultCwd) + '">' +
|
|
4700
|
-
'<span class="blank-chat-cwd-item-path">' + escapeHtml(defaultCwd) + '</span>' +
|
|
4701
|
-
'</div>';
|
|
4702
|
-
});
|
|
4705
|
+
});
|
|
4703
4706
|
}
|
|
4704
4707
|
|
|
4705
4708
|
function loadRecentPathBubbles() {
|
|
4706
4709
|
var container = document.getElementById("recent-paths-bubbles");
|
|
4707
4710
|
if (!container) return;
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
.then(function(items) {
|
|
4711
|
-
if (!items || !items.length) {
|
|
4711
|
+
fetchRecentPaths(function(items) {
|
|
4712
|
+
if (!items.length) {
|
|
4712
4713
|
container.innerHTML = "";
|
|
4713
4714
|
return;
|
|
4714
4715
|
}
|
|
@@ -4726,10 +4727,7 @@
|
|
|
4726
4727
|
}
|
|
4727
4728
|
});
|
|
4728
4729
|
});
|
|
4729
|
-
|
|
4730
|
-
.catch(function() {
|
|
4731
|
-
if (container) container.innerHTML = "";
|
|
4732
|
-
});
|
|
4730
|
+
});
|
|
4733
4731
|
}
|
|
4734
4732
|
|
|
4735
4733
|
function schedulePathSuggestions() {
|
|
@@ -5652,10 +5650,12 @@
|
|
|
5652
5650
|
}
|
|
5653
5651
|
});
|
|
5654
5652
|
// Inline keyboard visibility follows current view
|
|
5655
|
-
var inlineKeyboard = document.
|
|
5653
|
+
var inlineKeyboard = document.querySelector(".inline-shortcuts-wrap");
|
|
5656
5654
|
if (inlineKeyboard) inlineKeyboard.classList.toggle("hidden", structured || state.currentView !== "terminal");
|
|
5655
|
+
var expandedRow = document.querySelector(".inline-shortcuts-expanded-row");
|
|
5656
|
+
if (expandedRow) expandedRow.classList.toggle("hidden", structured || state.currentView !== "terminal");
|
|
5657
5657
|
var inputHint = document.querySelector(".input-hint");
|
|
5658
|
-
if (inputHint) inputHint.classList.toggle("hidden", structured ?
|
|
5658
|
+
if (inputHint) inputHint.classList.toggle("hidden", structured ? true : state.currentView === "terminal");
|
|
5659
5659
|
var container = document.getElementById("output");
|
|
5660
5660
|
if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
|
|
5661
5661
|
}
|
|
@@ -8040,12 +8040,12 @@
|
|
|
8040
8040
|
function fullRenderChat() {
|
|
8041
8041
|
// Extract system info from PTY output
|
|
8042
8042
|
var systemInfo = extractPtySystemInfo(selectedSession.output, messages);
|
|
8043
|
-
|
|
8043
|
+
|
|
8044
8044
|
// Build HTML with system info cards interleaved
|
|
8045
8045
|
var html = '';
|
|
8046
8046
|
var reversedMessages = messages.slice().reverse();
|
|
8047
8047
|
var msgCount = messages.length;
|
|
8048
|
-
|
|
8048
|
+
|
|
8049
8049
|
for (var i = 0; i < reversedMessages.length; i++) {
|
|
8050
8050
|
var msg = reversedMessages[i];
|
|
8051
8051
|
var originalIndex = msgCount - 1 - i; // Original index in messages array
|
|
@@ -8070,7 +8070,7 @@
|
|
|
8070
8070
|
}
|
|
8071
8071
|
|
|
8072
8072
|
// Render message
|
|
8073
|
-
html += renderChatMessage(msg);
|
|
8073
|
+
html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null);
|
|
8074
8074
|
}
|
|
8075
8075
|
|
|
8076
8076
|
chatMessages.innerHTML = html;
|
|
@@ -8107,6 +8107,47 @@
|
|
|
8107
8107
|
});
|
|
8108
8108
|
}
|
|
8109
8109
|
|
|
8110
|
+
// Pre-compute per-round cumulative usage.
|
|
8111
|
+
// A "round" starts at a user message and includes all subsequent assistant turns
|
|
8112
|
+
// until the next user message. Only the last assistant in each round shows the total.
|
|
8113
|
+
var roundUsageByIndex = {};
|
|
8114
|
+
(function() {
|
|
8115
|
+
var acc = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, totalCostUsd: 0 };
|
|
8116
|
+
var lastAssistantIdx = -1;
|
|
8117
|
+
for (var mi = 0; mi < messages.length; mi++) {
|
|
8118
|
+
var m = messages[mi];
|
|
8119
|
+
if (m.role === "user") {
|
|
8120
|
+
if (lastAssistantIdx >= 0 && (acc.inputTokens > 0 || acc.outputTokens > 0 || acc.totalCostUsd > 0)) {
|
|
8121
|
+
roundUsageByIndex[lastAssistantIdx] = {
|
|
8122
|
+
inputTokens: acc.inputTokens,
|
|
8123
|
+
outputTokens: acc.outputTokens,
|
|
8124
|
+
cacheReadInputTokens: acc.cacheReadInputTokens,
|
|
8125
|
+
totalCostUsd: acc.totalCostUsd
|
|
8126
|
+
};
|
|
8127
|
+
}
|
|
8128
|
+
acc = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, totalCostUsd: 0 };
|
|
8129
|
+
lastAssistantIdx = -1;
|
|
8130
|
+
} else if (m.role === "assistant" && m.usage) {
|
|
8131
|
+
var u = m.usage;
|
|
8132
|
+
acc.inputTokens += (u.inputTokens || 0);
|
|
8133
|
+
acc.outputTokens += (u.outputTokens || 0);
|
|
8134
|
+
acc.cacheReadInputTokens += (u.cacheReadInputTokens || 0);
|
|
8135
|
+
acc.totalCostUsd += (u.totalCostUsd || 0);
|
|
8136
|
+
lastAssistantIdx = mi;
|
|
8137
|
+
} else if (m.role === "assistant") {
|
|
8138
|
+
lastAssistantIdx = mi;
|
|
8139
|
+
}
|
|
8140
|
+
}
|
|
8141
|
+
if (lastAssistantIdx >= 0 && (acc.inputTokens > 0 || acc.outputTokens > 0 || acc.totalCostUsd > 0)) {
|
|
8142
|
+
roundUsageByIndex[lastAssistantIdx] = {
|
|
8143
|
+
inputTokens: acc.inputTokens,
|
|
8144
|
+
outputTokens: acc.outputTokens,
|
|
8145
|
+
cacheReadInputTokens: acc.cacheReadInputTokens,
|
|
8146
|
+
totalCostUsd: acc.totalCostUsd
|
|
8147
|
+
};
|
|
8148
|
+
}
|
|
8149
|
+
})();
|
|
8150
|
+
|
|
8110
8151
|
if (needsFullRender) {
|
|
8111
8152
|
fullRenderChat();
|
|
8112
8153
|
} else if (msgCount > existingCount) {
|
|
@@ -8118,7 +8159,8 @@
|
|
|
8118
8159
|
var insertedEls = [];
|
|
8119
8160
|
for (var i = 0; i < newMessages.length; i++) {
|
|
8120
8161
|
var div = document.createElement("div");
|
|
8121
|
-
|
|
8162
|
+
var nmOrigIdx = existingCount + (newMessages.length - 1 - i);
|
|
8163
|
+
div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null);
|
|
8122
8164
|
var el = div.firstElementChild;
|
|
8123
8165
|
if (el) {
|
|
8124
8166
|
el.classList.add("animate-in");
|
|
@@ -8143,7 +8185,8 @@
|
|
|
8143
8185
|
for (var mi = 0; mi < reversedMessages.length && mi < existingEls.length; mi++) {
|
|
8144
8186
|
var currentEl = existingEls[mi];
|
|
8145
8187
|
var tmpWrap = document.createElement("div");
|
|
8146
|
-
|
|
8188
|
+
var srOrigIdx = reversedMessages.length - 1 - mi;
|
|
8189
|
+
tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null);
|
|
8147
8190
|
var replacementEl = tmpWrap.firstElementChild;
|
|
8148
8191
|
if (!replacementEl) continue;
|
|
8149
8192
|
if (currentEl.innerHTML !== replacementEl.innerHTML || currentEl.className !== replacementEl.className) {
|
|
@@ -8859,7 +8902,7 @@
|
|
|
8859
8902
|
return messages;
|
|
8860
8903
|
}
|
|
8861
8904
|
|
|
8862
|
-
function renderChatMessage(msg) {
|
|
8905
|
+
function renderChatMessage(msg, roundUsage) {
|
|
8863
8906
|
// Thinking card (deep thought) — from PTY parsing
|
|
8864
8907
|
if (msg.role === "thinking") {
|
|
8865
8908
|
return '<div class="chat-message thinking">' +
|
|
@@ -8883,7 +8926,7 @@
|
|
|
8883
8926
|
|
|
8884
8927
|
// Structured content blocks (from JSON chat mode)
|
|
8885
8928
|
if (Array.isArray(msg.content)) {
|
|
8886
|
-
return renderStructuredMessage(msg);
|
|
8929
|
+
return renderStructuredMessage(msg, roundUsage);
|
|
8887
8930
|
}
|
|
8888
8931
|
|
|
8889
8932
|
// Legacy string content (from PTY parsing)
|
|
@@ -8938,7 +8981,99 @@
|
|
|
8938
8981
|
return '<div class="structured-tool-hint">已自动恢复一次 ' + escapeHtml(getToolDisplayName(toolName)) + ' 参数问题</div>';
|
|
8939
8982
|
}
|
|
8940
8983
|
|
|
8941
|
-
|
|
8984
|
+
// ── 连续同类工具调用分组 ──
|
|
8985
|
+
var GROUPABLE_TOOLS = { Read: 1, Glob: 1, Grep: 1, WebFetch: 1, WebSearch: 1, TodoRead: 1 };
|
|
8986
|
+
|
|
8987
|
+
function groupConsecutiveTools(content) {
|
|
8988
|
+
var groups = [];
|
|
8989
|
+
var i = 0;
|
|
8990
|
+
while (i < content.length) {
|
|
8991
|
+
var block = content[i];
|
|
8992
|
+
if (block.type === "tool_result") { i++; continue; }
|
|
8993
|
+
if (block.type === "tool_use" && GROUPABLE_TOOLS[block.name]) {
|
|
8994
|
+
var run = [{ block: block, index: i }];
|
|
8995
|
+
var j = i + 1;
|
|
8996
|
+
while (j < content.length) {
|
|
8997
|
+
if (content[j].type === "tool_result") { j++; continue; }
|
|
8998
|
+
if (content[j].type === "tool_use" && GROUPABLE_TOOLS[content[j].name]) {
|
|
8999
|
+
run.push({ block: content[j], index: j });
|
|
9000
|
+
j++;
|
|
9001
|
+
} else { break; }
|
|
9002
|
+
}
|
|
9003
|
+
if (run.length >= 2) {
|
|
9004
|
+
groups.push({ type: "group", items: run, endIndex: j });
|
|
9005
|
+
} else {
|
|
9006
|
+
groups.push({ type: "single", block: block, index: i });
|
|
9007
|
+
}
|
|
9008
|
+
i = j;
|
|
9009
|
+
} else {
|
|
9010
|
+
groups.push({ type: "single", block: block, index: i });
|
|
9011
|
+
i++;
|
|
9012
|
+
}
|
|
9013
|
+
}
|
|
9014
|
+
return groups;
|
|
9015
|
+
}
|
|
9016
|
+
|
|
9017
|
+
var TOOL_GROUP_LABELS = { Read: "读取", Glob: "搜索", Grep: "搜索", WebFetch: "抓取", WebSearch: "搜索", TodoRead: "待办" };
|
|
9018
|
+
|
|
9019
|
+
function renderToolGroup(items, role, toolResults) {
|
|
9020
|
+
// Count by tool name
|
|
9021
|
+
var counts = {};
|
|
9022
|
+
for (var k = 0; k < items.length; k++) {
|
|
9023
|
+
var n = items[k].block.name;
|
|
9024
|
+
counts[n] = (counts[n] || 0) + 1;
|
|
9025
|
+
}
|
|
9026
|
+
// Check if all done or still pending
|
|
9027
|
+
var allDone = true;
|
|
9028
|
+
var anyError = false;
|
|
9029
|
+
for (var k = 0; k < items.length; k++) {
|
|
9030
|
+
var b = items[k].block;
|
|
9031
|
+
var tr = pickToolResultForDisplay(toolResults, b.id);
|
|
9032
|
+
if (!tr) { allDone = false; }
|
|
9033
|
+
else if (tr.is_error) { anyError = true; }
|
|
9034
|
+
}
|
|
9035
|
+
var statusIcon = !allDone ? "…" : (anyError ? "✗" : "✓");
|
|
9036
|
+
var statusClass = !allDone ? "pending" : (anyError ? "error" : "done");
|
|
9037
|
+
// Summary text
|
|
9038
|
+
var parts = [];
|
|
9039
|
+
for (var name in counts) {
|
|
9040
|
+
parts.push(counts[name] + " " + (TOOL_GROUP_LABELS[name] || name));
|
|
9041
|
+
}
|
|
9042
|
+
var summaryText = parts.join(" · ");
|
|
9043
|
+
|
|
9044
|
+
// Render each item's inline-tool card
|
|
9045
|
+
var innerHtml = "";
|
|
9046
|
+
for (var k = 0; k < items.length; k++) {
|
|
9047
|
+
try {
|
|
9048
|
+
innerHtml += renderContentBlock(items[k].block, role, toolResults, items[k].index);
|
|
9049
|
+
} catch (e) {
|
|
9050
|
+
innerHtml += '<div class="render-error">工具渲染失败</div>';
|
|
9051
|
+
}
|
|
9052
|
+
}
|
|
9053
|
+
|
|
9054
|
+
return '<div class="tool-group" data-expanded="false" data-status="' + statusClass + '">' +
|
|
9055
|
+
'<div class="tool-group-summary" onclick="__toolGroupToggle(this.parentNode)">' +
|
|
9056
|
+
'<span class="tool-group-status">' + statusIcon + '</span>' +
|
|
9057
|
+
'<span class="tool-group-text">' + escapeHtml(summaryText) + '</span>' +
|
|
9058
|
+
'<span class="tool-group-count">' + items.length + ' 个调用</span>' +
|
|
9059
|
+
'<svg class="tool-group-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>' +
|
|
9060
|
+
'</div>' +
|
|
9061
|
+
'<div class="tool-group-body">' + innerHtml + '</div>' +
|
|
9062
|
+
'</div>';
|
|
9063
|
+
}
|
|
9064
|
+
|
|
9065
|
+
// global toggle
|
|
9066
|
+
window.__toolGroupToggle = function(el) {
|
|
9067
|
+
if (!el) return;
|
|
9068
|
+
var expanded = el.getAttribute("data-expanded") === "true";
|
|
9069
|
+
el.setAttribute("data-expanded", expanded ? "false" : "true");
|
|
9070
|
+
var body = el.querySelector(".tool-group-body");
|
|
9071
|
+
if (body) body.style.display = expanded ? "none" : "block";
|
|
9072
|
+
var chevron = el.querySelector(".tool-group-chevron");
|
|
9073
|
+
if (chevron) chevron.style.transform = expanded ? "" : "rotate(180deg)";
|
|
9074
|
+
};
|
|
9075
|
+
|
|
9076
|
+
function renderStructuredMessage(msg, roundUsage) {
|
|
8942
9077
|
var role = msg.role;
|
|
8943
9078
|
var avatar = role === "assistant" ? '<div class="chat-message-avatar">赛博虎妞</div>' : "";
|
|
8944
9079
|
|
|
@@ -8956,10 +9091,15 @@
|
|
|
8956
9091
|
var blocksHtml = "";
|
|
8957
9092
|
|
|
8958
9093
|
try {
|
|
8959
|
-
|
|
8960
|
-
|
|
9094
|
+
var groups = groupConsecutiveTools(msg.content);
|
|
9095
|
+
for (var g = 0; g < groups.length; g++) {
|
|
9096
|
+
var grp = groups[g];
|
|
8961
9097
|
try {
|
|
8962
|
-
|
|
9098
|
+
if (grp.type === "group") {
|
|
9099
|
+
blocksHtml += renderToolGroup(grp.items, role, toolResults);
|
|
9100
|
+
} else {
|
|
9101
|
+
blocksHtml += renderContentBlock(grp.block, role, toolResults, grp.index);
|
|
9102
|
+
}
|
|
8963
9103
|
} catch (e) {
|
|
8964
9104
|
blocksHtml += '<div class="render-error">消息块渲染失败</div>';
|
|
8965
9105
|
}
|
|
@@ -8972,17 +9112,6 @@
|
|
|
8972
9112
|
}
|
|
8973
9113
|
|
|
8974
9114
|
var usageHtml = "";
|
|
8975
|
-
if (role === "assistant" && msg.usage) {
|
|
8976
|
-
var u = msg.usage;
|
|
8977
|
-
var parts = [];
|
|
8978
|
-
if (u.inputTokens !== undefined) parts.push("输入 " + u.inputTokens);
|
|
8979
|
-
if (u.outputTokens !== undefined) parts.push("输出 " + u.outputTokens);
|
|
8980
|
-
if (u.cacheReadInputTokens !== undefined && u.cacheReadInputTokens > 0) parts.push("缓存 " + u.cacheReadInputTokens);
|
|
8981
|
-
if (u.totalCostUsd !== undefined) parts.push("$" + u.totalCostUsd.toFixed(4));
|
|
8982
|
-
if (parts.length > 0) {
|
|
8983
|
-
usageHtml = '<div class="message-usage">' + parts.join(" · ") + '</div>';
|
|
8984
|
-
}
|
|
8985
|
-
}
|
|
8986
9115
|
|
|
8987
9116
|
return '<div class="chat-message ' + role + '">' +
|
|
8988
9117
|
avatar +
|
|
@@ -2876,6 +2876,65 @@
|
|
|
2876
2876
|
border-radius: 2px;
|
|
2877
2877
|
}
|
|
2878
2878
|
|
|
2879
|
+
/* ── Tool Group (连续同类调用折叠) ── */
|
|
2880
|
+
.tool-group {
|
|
2881
|
+
margin: 2px 0;
|
|
2882
|
+
border-radius: 6px;
|
|
2883
|
+
border: 1px solid var(--border-subtle, rgba(127,127,127,0.1));
|
|
2884
|
+
overflow: hidden;
|
|
2885
|
+
}
|
|
2886
|
+
.tool-group-summary {
|
|
2887
|
+
display: flex;
|
|
2888
|
+
align-items: center;
|
|
2889
|
+
gap: 6px;
|
|
2890
|
+
padding: 5px 10px;
|
|
2891
|
+
cursor: pointer;
|
|
2892
|
+
font-size: 0.75rem;
|
|
2893
|
+
color: var(--text-secondary);
|
|
2894
|
+
user-select: none;
|
|
2895
|
+
transition: background var(--transition-fast);
|
|
2896
|
+
}
|
|
2897
|
+
.tool-group-summary:hover {
|
|
2898
|
+
background: var(--bg-hover, rgba(127,127,127,0.06));
|
|
2899
|
+
}
|
|
2900
|
+
.tool-group-status {
|
|
2901
|
+
font-size: 0.6875rem;
|
|
2902
|
+
flex-shrink: 0;
|
|
2903
|
+
width: 14px;
|
|
2904
|
+
text-align: center;
|
|
2905
|
+
}
|
|
2906
|
+
.tool-group[data-status="done"] .tool-group-status { color: var(--success, #22c55e); }
|
|
2907
|
+
.tool-group[data-status="error"] .tool-group-status { color: var(--error, #ef4444); }
|
|
2908
|
+
.tool-group[data-status="pending"] .tool-group-status { color: var(--text-muted); }
|
|
2909
|
+
.tool-group-text {
|
|
2910
|
+
flex: 1;
|
|
2911
|
+
min-width: 0;
|
|
2912
|
+
overflow: hidden;
|
|
2913
|
+
text-overflow: ellipsis;
|
|
2914
|
+
white-space: nowrap;
|
|
2915
|
+
}
|
|
2916
|
+
.tool-group-count {
|
|
2917
|
+
flex-shrink: 0;
|
|
2918
|
+
font-size: 0.625rem;
|
|
2919
|
+
color: var(--text-muted);
|
|
2920
|
+
}
|
|
2921
|
+
.tool-group-chevron {
|
|
2922
|
+
flex-shrink: 0;
|
|
2923
|
+
transition: transform 0.2s ease;
|
|
2924
|
+
color: var(--text-muted);
|
|
2925
|
+
}
|
|
2926
|
+
.tool-group[data-expanded="true"] .tool-group-chevron {
|
|
2927
|
+
transform: rotate(180deg);
|
|
2928
|
+
}
|
|
2929
|
+
.tool-group-body {
|
|
2930
|
+
display: none;
|
|
2931
|
+
padding: 2px 6px 4px;
|
|
2932
|
+
border-top: 1px solid var(--border-subtle, rgba(127,127,127,0.08));
|
|
2933
|
+
}
|
|
2934
|
+
.tool-group[data-expanded="true"] .tool-group-body {
|
|
2935
|
+
display: block;
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2879
2938
|
/* ── Inline Tool Display (Read, Glob, Grep, WebFetch, WebSearch, TodoRead) ── */
|
|
2880
2939
|
.inline-tool {
|
|
2881
2940
|
display: flex;
|
|
@@ -7522,64 +7581,100 @@
|
|
|
7522
7581
|
}
|
|
7523
7582
|
|
|
7524
7583
|
/* ── 结构化会话状态条 ── */
|
|
7584
|
+
/* ── 输入框顶部波浪脉冲(回复中) ── */
|
|
7585
|
+
.input-composer.in-flight {
|
|
7586
|
+
border-color: transparent;
|
|
7587
|
+
}
|
|
7588
|
+
.input-composer.in-flight::before {
|
|
7589
|
+
content: "";
|
|
7590
|
+
position: absolute;
|
|
7591
|
+
top: -2px;
|
|
7592
|
+
left: -60%;
|
|
7593
|
+
width: 220%;
|
|
7594
|
+
height: 3px;
|
|
7595
|
+
border-radius: 14px 14px 0 0;
|
|
7596
|
+
background:
|
|
7597
|
+
radial-gradient(ellipse 280px 4px,
|
|
7598
|
+
rgba(var(--accent-rgb, 197, 101, 61), 0.45) 0%,
|
|
7599
|
+
rgba(var(--accent-rgb, 197, 101, 61), 0.12) 35%,
|
|
7600
|
+
transparent 60%)
|
|
7601
|
+
no-repeat;
|
|
7602
|
+
background-size: 280px 4px;
|
|
7603
|
+
animation: composerWaveSlide 7s cubic-bezier(0.35, 0, 0.65, 1) infinite;
|
|
7604
|
+
z-index: 2;
|
|
7605
|
+
pointer-events: none;
|
|
7606
|
+
}
|
|
7607
|
+
.input-composer.in-flight::after {
|
|
7608
|
+
content: "";
|
|
7609
|
+
position: absolute;
|
|
7610
|
+
top: -2px;
|
|
7611
|
+
left: -60%;
|
|
7612
|
+
width: 220%;
|
|
7613
|
+
height: 3px;
|
|
7614
|
+
border-radius: 14px 14px 0 0;
|
|
7615
|
+
background:
|
|
7616
|
+
radial-gradient(ellipse 220px 3px,
|
|
7617
|
+
rgba(var(--accent-rgb, 197, 101, 61), 0.25) 0%,
|
|
7618
|
+
rgba(var(--accent-rgb, 197, 101, 61), 0.06) 35%,
|
|
7619
|
+
transparent 60%)
|
|
7620
|
+
no-repeat;
|
|
7621
|
+
background-size: 220px 3px;
|
|
7622
|
+
animation: composerWaveSlide 9s cubic-bezier(0.35, 0, 0.65, 1) infinite reverse;
|
|
7623
|
+
z-index: 2;
|
|
7624
|
+
pointer-events: none;
|
|
7625
|
+
}
|
|
7626
|
+
@keyframes composerWaveSlide {
|
|
7627
|
+
0% { background-position: 0% center; }
|
|
7628
|
+
100% { background-position: 100% center; }
|
|
7629
|
+
}
|
|
7630
|
+
|
|
7631
|
+
/* ── 结构化会话状态条(输入框右上角) ── */
|
|
7525
7632
|
.structured-status-bar {
|
|
7526
7633
|
display: flex;
|
|
7527
7634
|
align-items: center;
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
background:
|
|
7533
|
-
border:
|
|
7534
|
-
font-size: 0.
|
|
7535
|
-
color: var(--text-
|
|
7635
|
+
justify-content: flex-end;
|
|
7636
|
+
gap: 5px;
|
|
7637
|
+
margin: 0 4px 2px 0;
|
|
7638
|
+
padding: 0;
|
|
7639
|
+
background: transparent;
|
|
7640
|
+
border: none;
|
|
7641
|
+
font-size: 0.6875rem;
|
|
7642
|
+
color: var(--text-muted);
|
|
7536
7643
|
transition: all 0.3s ease;
|
|
7537
7644
|
overflow: hidden;
|
|
7538
7645
|
}
|
|
7539
7646
|
|
|
7540
|
-
.structured-status-bar .status-bar-
|
|
7647
|
+
.structured-status-bar .status-bar-dot {
|
|
7648
|
+
width: 5px;
|
|
7649
|
+
height: 5px;
|
|
7650
|
+
border-radius: 50%;
|
|
7651
|
+
background: rgba(var(--accent-rgb, 197, 101, 61), 0.8);
|
|
7652
|
+
animation: statusDotPulse 1.2s ease-in-out infinite;
|
|
7541
7653
|
flex-shrink: 0;
|
|
7542
|
-
font-weight: 600;
|
|
7543
|
-
color: var(--accent-soft);
|
|
7544
|
-
}
|
|
7545
|
-
|
|
7546
|
-
.structured-status-bar .status-bar-track {
|
|
7547
|
-
flex: 1;
|
|
7548
|
-
height: 3px;
|
|
7549
|
-
border-radius: 2px;
|
|
7550
|
-
background: rgba(var(--accent-rgb, 99, 102, 241), 0.1);
|
|
7551
|
-
overflow: hidden;
|
|
7552
|
-
position: relative;
|
|
7553
7654
|
}
|
|
7554
7655
|
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
left: 0;
|
|
7559
|
-
width: 40%;
|
|
7560
|
-
height: 100%;
|
|
7561
|
-
border-radius: 2px;
|
|
7562
|
-
background: linear-gradient(90deg, transparent, var(--accent-soft), transparent);
|
|
7563
|
-
animation: marqueeScroll 1.5s ease-in-out infinite;
|
|
7656
|
+
@keyframes statusDotPulse {
|
|
7657
|
+
0%, 100% { opacity: 0.4; transform: scale(0.9); }
|
|
7658
|
+
50% { opacity: 1; transform: scale(1.1); }
|
|
7564
7659
|
}
|
|
7565
7660
|
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7661
|
+
.structured-status-bar .status-bar-label {
|
|
7662
|
+
flex-shrink: 0;
|
|
7663
|
+
font-weight: 500;
|
|
7664
|
+
color: var(--text-muted);
|
|
7665
|
+
font-size: 0.625rem;
|
|
7569
7666
|
}
|
|
7570
7667
|
|
|
7571
7668
|
.structured-status-bar .status-bar-timer {
|
|
7572
7669
|
flex-shrink: 0;
|
|
7573
7670
|
font-variant-numeric: tabular-nums;
|
|
7574
7671
|
font-family: var(--font-mono);
|
|
7575
|
-
font-size: 0.
|
|
7672
|
+
font-size: 0.625rem;
|
|
7576
7673
|
color: var(--text-muted);
|
|
7577
7674
|
}
|
|
7578
7675
|
|
|
7579
7676
|
/* 完成态 */
|
|
7580
7677
|
.structured-status-bar.completed {
|
|
7581
|
-
background: rgba(79, 122, 88, 0.06);
|
|
7582
|
-
border-color: rgba(79, 122, 88, 0.15);
|
|
7583
7678
|
animation: statusBarFadeOut 2s ease-out 1s forwards;
|
|
7584
7679
|
}
|
|
7585
7680
|
|
|
@@ -7587,22 +7682,10 @@
|
|
|
7587
7682
|
color: var(--success);
|
|
7588
7683
|
}
|
|
7589
7684
|
|
|
7590
|
-
.structured-status-bar.completed .status-bar-track {
|
|
7591
|
-
background: rgba(79, 122, 88, 0.1);
|
|
7592
|
-
}
|
|
7593
|
-
|
|
7594
|
-
.structured-status-bar.completed .status-bar-fill {
|
|
7595
|
-
width: 100%;
|
|
7596
|
-
background: var(--success);
|
|
7597
|
-
opacity: 0.5;
|
|
7598
|
-
animation: none;
|
|
7599
|
-
left: 0;
|
|
7600
|
-
}
|
|
7601
|
-
|
|
7602
7685
|
@keyframes statusBarFadeOut {
|
|
7603
|
-
0% { opacity: 1; max-height:
|
|
7604
|
-
70% { opacity: 0; max-height:
|
|
7605
|
-
100% { opacity: 0; max-height: 0; margin: 0;
|
|
7686
|
+
0% { opacity: 1; max-height: 20px; }
|
|
7687
|
+
70% { opacity: 0; max-height: 20px; }
|
|
7688
|
+
100% { opacity: 0; max-height: 0; margin: 0; }
|
|
7606
7689
|
}
|
|
7607
7690
|
|
|
7608
7691
|
/* 结束标记 */
|