@co0ontty/wand 1.5.1 → 1.5.4
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 +116 -99
- package/dist/web-ui/content/styles.css +20 -20
- 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
|
});
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
cwdValue: "",
|
|
101
101
|
modeValue: "managed",
|
|
102
102
|
chatMode: "managed",
|
|
103
|
-
sessionCreateKind: "
|
|
103
|
+
sessionCreateKind: "structured",
|
|
104
104
|
sessionTool: "claude",
|
|
105
105
|
preferredCommand: "claude",
|
|
106
106
|
structuredRunner: "claude-cli-print",
|
|
@@ -1030,14 +1030,13 @@
|
|
|
1030
1030
|
|
|
1031
1031
|
function selectAllVisibleItems() {
|
|
1032
1032
|
var nextSessionIds = {};
|
|
1033
|
-
state.sessions.
|
|
1033
|
+
state.sessions.filter(function(session) {
|
|
1034
|
+
return !session.resumedToSessionId;
|
|
1035
|
+
}).forEach(function(session) {
|
|
1034
1036
|
nextSessionIds[session.id] = true;
|
|
1035
1037
|
});
|
|
1036
|
-
var cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
1037
1038
|
var nextHistoryIds = {};
|
|
1038
|
-
getVisibleClaudeHistorySessions().
|
|
1039
|
-
return !session.timestamp || new Date(session.timestamp).getTime() <= cutoff;
|
|
1040
|
-
}).forEach(function(session) {
|
|
1039
|
+
getVisibleClaudeHistorySessions().forEach(function(session) {
|
|
1041
1040
|
nextHistoryIds[session.claudeSessionId] = true;
|
|
1042
1041
|
});
|
|
1043
1042
|
state.selectedSessionIds = nextSessionIds;
|
|
@@ -1815,7 +1814,7 @@
|
|
|
1815
1814
|
{ id: "full-access", label: "全权限", desc: "自动确认权限" },
|
|
1816
1815
|
{ id: "auto-edit", label: "自动编辑", desc: "自动确认修改" },
|
|
1817
1816
|
{ id: "default", label: "标准", desc: "逐步确认操作" },
|
|
1818
|
-
{ id: "native", label: "原生", desc: "
|
|
1817
|
+
{ id: "native", label: "原生", desc: "原生结构化输出" }
|
|
1819
1818
|
];
|
|
1820
1819
|
return modes.map(function(m) {
|
|
1821
1820
|
var active = m.id === selectedMode ? " active" : "";
|
|
@@ -1828,8 +1827,8 @@
|
|
|
1828
1827
|
|
|
1829
1828
|
function renderSessionKindOptions(selectedKind) {
|
|
1830
1829
|
var kinds = [
|
|
1831
|
-
{ id: "
|
|
1832
|
-
{ id: "
|
|
1830
|
+
{ id: "structured", label: "结构化", desc: "智能对话模式" },
|
|
1831
|
+
{ id: "pty", label: "PTY", desc: "交互式终端会话" }
|
|
1833
1832
|
];
|
|
1834
1833
|
return kinds.map(function(kind) {
|
|
1835
1834
|
var active = kind.id === selectedKind ? " active" : "";
|
|
@@ -1842,15 +1841,15 @@
|
|
|
1842
1841
|
|
|
1843
1842
|
function getSessionKindHint(kind) {
|
|
1844
1843
|
if (kind === "structured") {
|
|
1845
|
-
return "
|
|
1844
|
+
return "结构化聊天界面,支持多轮对话、流式输出和工具调用展示。";
|
|
1846
1845
|
}
|
|
1847
|
-
return "
|
|
1846
|
+
return "原始 PTY 终端会话,支持持续交互、终端视图和权限流。";
|
|
1848
1847
|
}
|
|
1849
1848
|
|
|
1850
1849
|
function renderSessionModal() {
|
|
1851
1850
|
var modalTool = getPreferredTool();
|
|
1852
1851
|
var modalMode = getSafeModeForTool(modalTool, state.modeValue || state.chatMode || "default");
|
|
1853
|
-
var sessionKind = state.sessionCreateKind || "
|
|
1852
|
+
var sessionKind = state.sessionCreateKind || "structured";
|
|
1854
1853
|
return '<section id="session-modal" class="modal-backdrop hidden">' +
|
|
1855
1854
|
'<div class="modal session-modal">' +
|
|
1856
1855
|
'<div class="modal-header">' +
|
|
@@ -2349,69 +2348,58 @@
|
|
|
2349
2348
|
}
|
|
2350
2349
|
}
|
|
2351
2350
|
|
|
2352
|
-
// Helper functions for recent paths
|
|
2353
|
-
function
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
return [];
|
|
2359
|
-
}
|
|
2351
|
+
// Helper functions for recent paths (single source: backend API)
|
|
2352
|
+
function fetchRecentPaths(callback) {
|
|
2353
|
+
fetch("/api/recent-paths", { credentials: "same-origin" })
|
|
2354
|
+
.then(function(res) { return res.json(); })
|
|
2355
|
+
.then(function(items) { callback(items || []); })
|
|
2356
|
+
.catch(function() { callback([]); });
|
|
2360
2357
|
}
|
|
2361
2358
|
|
|
2362
2359
|
function addRecentPath(path) {
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
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
|
-
}
|
|
2360
|
+
return fetch("/api/recent-paths", {
|
|
2361
|
+
method: "POST",
|
|
2362
|
+
headers: { "Content-Type": "application/json" },
|
|
2363
|
+
credentials: "same-origin",
|
|
2364
|
+
body: JSON.stringify({ path: path })
|
|
2365
|
+
}).catch(function() {});
|
|
2375
2366
|
}
|
|
2376
2367
|
|
|
2377
|
-
function
|
|
2378
|
-
|
|
2379
|
-
if (recent.length === 0) return "";
|
|
2380
|
-
|
|
2368
|
+
function renderRecentPathsHtml(items) {
|
|
2369
|
+
if (!items.length) return "";
|
|
2381
2370
|
var html = '<div class="folder-recent-section">' +
|
|
2382
2371
|
'<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>' +
|
|
2372
|
+
items.forEach(function(item) {
|
|
2373
|
+
var p = item.path || item;
|
|
2374
|
+
html += '<div class="folder-recent-item" data-path="' + escapeHtml(p) + '">' +
|
|
2375
|
+
'<span class="folder-recent-item-path">' + escapeHtml(p) + '</span>' +
|
|
2388
2376
|
'</div>';
|
|
2389
2377
|
});
|
|
2390
|
-
|
|
2391
2378
|
html += '</div>';
|
|
2392
2379
|
return html;
|
|
2393
2380
|
}
|
|
2394
2381
|
|
|
2395
2382
|
function showRecentPathsDropdown() {
|
|
2396
2383
|
if (!folderPickerDropdown) return;
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2384
|
+
fetchRecentPaths(function(items) {
|
|
2385
|
+
var recentHtml = renderRecentPathsHtml(items);
|
|
2386
|
+
if (recentHtml) {
|
|
2387
|
+
folderPickerDropdown.innerHTML = recentHtml;
|
|
2388
|
+
folderPickerDropdown.classList.remove("hidden");
|
|
2389
|
+
folderPickerDropdown.querySelectorAll(".folder-recent-item").forEach(function(item) {
|
|
2390
|
+
item.addEventListener("click", function() {
|
|
2391
|
+
var path = this.dataset.path;
|
|
2392
|
+
if (folderPickerInput) {
|
|
2393
|
+
folderPickerInput.value = path;
|
|
2394
|
+
saveWorkingDir(path);
|
|
2395
|
+
loadFolderSuggestions(path);
|
|
2396
|
+
}
|
|
2397
|
+
});
|
|
2410
2398
|
});
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
}
|
|
2399
|
+
} else {
|
|
2400
|
+
hideFolderDropdown();
|
|
2401
|
+
}
|
|
2402
|
+
});
|
|
2415
2403
|
}
|
|
2416
2404
|
|
|
2417
2405
|
// Working directory indicator click handler for active sessions
|
|
@@ -2472,7 +2460,6 @@
|
|
|
2472
2460
|
var path = entry.fullPath;
|
|
2473
2461
|
folderPickerInput.value = path;
|
|
2474
2462
|
saveWorkingDir(path);
|
|
2475
|
-
addRecentPath(path);
|
|
2476
2463
|
loadFolderSuggestions(path);
|
|
2477
2464
|
break;
|
|
2478
2465
|
}
|
|
@@ -3462,7 +3449,7 @@
|
|
|
3462
3449
|
return "保留交互式会话,同时更偏向直接编辑代码。";
|
|
3463
3450
|
}
|
|
3464
3451
|
if (mode === "native") {
|
|
3465
|
-
return "
|
|
3452
|
+
return "调用 Claude 原生 API 输出,适合快速问答或一次性生成。";
|
|
3466
3453
|
}
|
|
3467
3454
|
if (mode === "managed") {
|
|
3468
3455
|
return "AI 自动完成所有工作,无需中途确认,适合有明确目标的任务。";
|
|
@@ -3612,7 +3599,7 @@
|
|
|
3612
3599
|
var modeHint = document.getElementById("mode-description");
|
|
3613
3600
|
var kindHint = document.getElementById("session-kind-description");
|
|
3614
3601
|
var tool = "claude";
|
|
3615
|
-
var sessionKind = state.sessionCreateKind || "
|
|
3602
|
+
var sessionKind = state.sessionCreateKind || "structured";
|
|
3616
3603
|
|
|
3617
3604
|
state.sessionTool = tool;
|
|
3618
3605
|
state.modeValue = getSafeModeForTool(tool, state.modeValue || state.chatMode || "default");
|
|
@@ -4007,7 +3994,7 @@
|
|
|
4007
3994
|
modal.classList.remove("hidden");
|
|
4008
3995
|
lastFocusedElement = document.activeElement;
|
|
4009
3996
|
state.sessionTool = getPreferredTool();
|
|
4010
|
-
state.sessionCreateKind = "
|
|
3997
|
+
state.sessionCreateKind = "structured";
|
|
4011
3998
|
state.modeValue = getSafeModeForTool(state.sessionTool, state.modeValue || state.chatMode);
|
|
4012
3999
|
syncSessionModalUI();
|
|
4013
4000
|
loadRecentPathBubbles();
|
|
@@ -4535,7 +4522,7 @@
|
|
|
4535
4522
|
var cwdEl = document.getElementById("cwd");
|
|
4536
4523
|
var errorEl = document.getElementById("modal-error");
|
|
4537
4524
|
var command = getPreferredTool();
|
|
4538
|
-
var sessionKind = state.sessionCreateKind || "
|
|
4525
|
+
var sessionKind = state.sessionCreateKind || "structured";
|
|
4539
4526
|
|
|
4540
4527
|
hideError(errorEl);
|
|
4541
4528
|
|
|
@@ -4560,6 +4547,7 @@
|
|
|
4560
4547
|
syncComposerModeSelect();
|
|
4561
4548
|
return createStructuredSession(undefined, cwd, mode)
|
|
4562
4549
|
.then(function(data) {
|
|
4550
|
+
saveWorkingDir(cwd);
|
|
4563
4551
|
closeSessionModal();
|
|
4564
4552
|
closeSessionsDrawer();
|
|
4565
4553
|
return data;
|
|
@@ -4597,6 +4585,7 @@
|
|
|
4597
4585
|
state.selectedId = data.id;
|
|
4598
4586
|
console.log("[WAND] runPtyCommandFromModal created session:", data.id, "sessionKind:", data.sessionKind, "runner:", data.runner);
|
|
4599
4587
|
persistSelectedId();
|
|
4588
|
+
saveWorkingDir(cwd);
|
|
4600
4589
|
state.drafts[data.id] = "";
|
|
4601
4590
|
resetChatRenderCache();
|
|
4602
4591
|
closeSessionModal();
|
|
@@ -4655,18 +4644,14 @@
|
|
|
4655
4644
|
function loadBlankChatCwdDropdown(dropdown) {
|
|
4656
4645
|
var defaultCwd = getConfigCwd();
|
|
4657
4646
|
dropdown.innerHTML = '<div class="blank-chat-cwd-loading">加载中...</div>';
|
|
4658
|
-
|
|
4659
|
-
.then(function(res) { return res.json(); })
|
|
4660
|
-
.then(function(items) {
|
|
4647
|
+
fetchRecentPaths(function(items) {
|
|
4661
4648
|
var html = "";
|
|
4662
|
-
// Default directory always first
|
|
4663
4649
|
var currentDir = state.workingDir || defaultCwd;
|
|
4664
4650
|
html += '<div class="blank-chat-cwd-item' + (currentDir === defaultCwd ? " active" : "") + '" data-path="' + escapeHtml(defaultCwd) + '">' +
|
|
4665
4651
|
'<span class="blank-chat-cwd-item-label">默认</span>' +
|
|
4666
4652
|
'<span class="blank-chat-cwd-item-path">' + escapeHtml(defaultCwd) + '</span>' +
|
|
4667
4653
|
'</div>';
|
|
4668
|
-
|
|
4669
|
-
if (items && items.length) {
|
|
4654
|
+
if (items.length) {
|
|
4670
4655
|
var seen = {};
|
|
4671
4656
|
seen[defaultCwd] = true;
|
|
4672
4657
|
items.forEach(function(item) {
|
|
@@ -4689,26 +4674,18 @@
|
|
|
4689
4674
|
dropdown.classList.add("hidden");
|
|
4690
4675
|
var arrow = document.getElementById("blank-chat-cwd-arrow");
|
|
4691
4676
|
if (arrow) arrow.textContent = "▼";
|
|
4692
|
-
// Update folder picker input if exists
|
|
4693
4677
|
var fpInput = document.getElementById("folder-picker-input");
|
|
4694
4678
|
if (fpInput) fpInput.value = path;
|
|
4695
4679
|
});
|
|
4696
4680
|
});
|
|
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
|
-
});
|
|
4681
|
+
});
|
|
4703
4682
|
}
|
|
4704
4683
|
|
|
4705
4684
|
function loadRecentPathBubbles() {
|
|
4706
4685
|
var container = document.getElementById("recent-paths-bubbles");
|
|
4707
4686
|
if (!container) return;
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
.then(function(items) {
|
|
4711
|
-
if (!items || !items.length) {
|
|
4687
|
+
fetchRecentPaths(function(items) {
|
|
4688
|
+
if (!items.length) {
|
|
4712
4689
|
container.innerHTML = "";
|
|
4713
4690
|
return;
|
|
4714
4691
|
}
|
|
@@ -4726,10 +4703,7 @@
|
|
|
4726
4703
|
}
|
|
4727
4704
|
});
|
|
4728
4705
|
});
|
|
4729
|
-
|
|
4730
|
-
.catch(function() {
|
|
4731
|
-
if (container) container.innerHTML = "";
|
|
4732
|
-
});
|
|
4706
|
+
});
|
|
4733
4707
|
}
|
|
4734
4708
|
|
|
4735
4709
|
function schedulePathSuggestions() {
|
|
@@ -8040,12 +8014,12 @@
|
|
|
8040
8014
|
function fullRenderChat() {
|
|
8041
8015
|
// Extract system info from PTY output
|
|
8042
8016
|
var systemInfo = extractPtySystemInfo(selectedSession.output, messages);
|
|
8043
|
-
|
|
8017
|
+
|
|
8044
8018
|
// Build HTML with system info cards interleaved
|
|
8045
8019
|
var html = '';
|
|
8046
8020
|
var reversedMessages = messages.slice().reverse();
|
|
8047
8021
|
var msgCount = messages.length;
|
|
8048
|
-
|
|
8022
|
+
|
|
8049
8023
|
for (var i = 0; i < reversedMessages.length; i++) {
|
|
8050
8024
|
var msg = reversedMessages[i];
|
|
8051
8025
|
var originalIndex = msgCount - 1 - i; // Original index in messages array
|
|
@@ -8070,7 +8044,7 @@
|
|
|
8070
8044
|
}
|
|
8071
8045
|
|
|
8072
8046
|
// Render message
|
|
8073
|
-
html += renderChatMessage(msg);
|
|
8047
|
+
html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null);
|
|
8074
8048
|
}
|
|
8075
8049
|
|
|
8076
8050
|
chatMessages.innerHTML = html;
|
|
@@ -8107,6 +8081,47 @@
|
|
|
8107
8081
|
});
|
|
8108
8082
|
}
|
|
8109
8083
|
|
|
8084
|
+
// Pre-compute per-round cumulative usage.
|
|
8085
|
+
// A "round" starts at a user message and includes all subsequent assistant turns
|
|
8086
|
+
// until the next user message. Only the last assistant in each round shows the total.
|
|
8087
|
+
var roundUsageByIndex = {};
|
|
8088
|
+
(function() {
|
|
8089
|
+
var acc = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, totalCostUsd: 0 };
|
|
8090
|
+
var lastAssistantIdx = -1;
|
|
8091
|
+
for (var mi = 0; mi < messages.length; mi++) {
|
|
8092
|
+
var m = messages[mi];
|
|
8093
|
+
if (m.role === "user") {
|
|
8094
|
+
if (lastAssistantIdx >= 0 && (acc.inputTokens > 0 || acc.outputTokens > 0 || acc.totalCostUsd > 0)) {
|
|
8095
|
+
roundUsageByIndex[lastAssistantIdx] = {
|
|
8096
|
+
inputTokens: acc.inputTokens,
|
|
8097
|
+
outputTokens: acc.outputTokens,
|
|
8098
|
+
cacheReadInputTokens: acc.cacheReadInputTokens,
|
|
8099
|
+
totalCostUsd: acc.totalCostUsd
|
|
8100
|
+
};
|
|
8101
|
+
}
|
|
8102
|
+
acc = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, totalCostUsd: 0 };
|
|
8103
|
+
lastAssistantIdx = -1;
|
|
8104
|
+
} else if (m.role === "assistant" && m.usage) {
|
|
8105
|
+
var u = m.usage;
|
|
8106
|
+
acc.inputTokens += (u.inputTokens || 0);
|
|
8107
|
+
acc.outputTokens += (u.outputTokens || 0);
|
|
8108
|
+
acc.cacheReadInputTokens += (u.cacheReadInputTokens || 0);
|
|
8109
|
+
acc.totalCostUsd += (u.totalCostUsd || 0);
|
|
8110
|
+
lastAssistantIdx = mi;
|
|
8111
|
+
} else if (m.role === "assistant") {
|
|
8112
|
+
lastAssistantIdx = mi;
|
|
8113
|
+
}
|
|
8114
|
+
}
|
|
8115
|
+
if (lastAssistantIdx >= 0 && (acc.inputTokens > 0 || acc.outputTokens > 0 || acc.totalCostUsd > 0)) {
|
|
8116
|
+
roundUsageByIndex[lastAssistantIdx] = {
|
|
8117
|
+
inputTokens: acc.inputTokens,
|
|
8118
|
+
outputTokens: acc.outputTokens,
|
|
8119
|
+
cacheReadInputTokens: acc.cacheReadInputTokens,
|
|
8120
|
+
totalCostUsd: acc.totalCostUsd
|
|
8121
|
+
};
|
|
8122
|
+
}
|
|
8123
|
+
})();
|
|
8124
|
+
|
|
8110
8125
|
if (needsFullRender) {
|
|
8111
8126
|
fullRenderChat();
|
|
8112
8127
|
} else if (msgCount > existingCount) {
|
|
@@ -8118,7 +8133,8 @@
|
|
|
8118
8133
|
var insertedEls = [];
|
|
8119
8134
|
for (var i = 0; i < newMessages.length; i++) {
|
|
8120
8135
|
var div = document.createElement("div");
|
|
8121
|
-
|
|
8136
|
+
var nmOrigIdx = existingCount + (newMessages.length - 1 - i);
|
|
8137
|
+
div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null);
|
|
8122
8138
|
var el = div.firstElementChild;
|
|
8123
8139
|
if (el) {
|
|
8124
8140
|
el.classList.add("animate-in");
|
|
@@ -8143,7 +8159,8 @@
|
|
|
8143
8159
|
for (var mi = 0; mi < reversedMessages.length && mi < existingEls.length; mi++) {
|
|
8144
8160
|
var currentEl = existingEls[mi];
|
|
8145
8161
|
var tmpWrap = document.createElement("div");
|
|
8146
|
-
|
|
8162
|
+
var srOrigIdx = reversedMessages.length - 1 - mi;
|
|
8163
|
+
tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null);
|
|
8147
8164
|
var replacementEl = tmpWrap.firstElementChild;
|
|
8148
8165
|
if (!replacementEl) continue;
|
|
8149
8166
|
if (currentEl.innerHTML !== replacementEl.innerHTML || currentEl.className !== replacementEl.className) {
|
|
@@ -8859,7 +8876,7 @@
|
|
|
8859
8876
|
return messages;
|
|
8860
8877
|
}
|
|
8861
8878
|
|
|
8862
|
-
function renderChatMessage(msg) {
|
|
8879
|
+
function renderChatMessage(msg, roundUsage) {
|
|
8863
8880
|
// Thinking card (deep thought) — from PTY parsing
|
|
8864
8881
|
if (msg.role === "thinking") {
|
|
8865
8882
|
return '<div class="chat-message thinking">' +
|
|
@@ -8883,7 +8900,7 @@
|
|
|
8883
8900
|
|
|
8884
8901
|
// Structured content blocks (from JSON chat mode)
|
|
8885
8902
|
if (Array.isArray(msg.content)) {
|
|
8886
|
-
return renderStructuredMessage(msg);
|
|
8903
|
+
return renderStructuredMessage(msg, roundUsage);
|
|
8887
8904
|
}
|
|
8888
8905
|
|
|
8889
8906
|
// Legacy string content (from PTY parsing)
|
|
@@ -8938,7 +8955,7 @@
|
|
|
8938
8955
|
return '<div class="structured-tool-hint">已自动恢复一次 ' + escapeHtml(getToolDisplayName(toolName)) + ' 参数问题</div>';
|
|
8939
8956
|
}
|
|
8940
8957
|
|
|
8941
|
-
function renderStructuredMessage(msg) {
|
|
8958
|
+
function renderStructuredMessage(msg, roundUsage) {
|
|
8942
8959
|
var role = msg.role;
|
|
8943
8960
|
var avatar = role === "assistant" ? '<div class="chat-message-avatar">赛博虎妞</div>' : "";
|
|
8944
8961
|
|
|
@@ -8972,13 +8989,13 @@
|
|
|
8972
8989
|
}
|
|
8973
8990
|
|
|
8974
8991
|
var usageHtml = "";
|
|
8975
|
-
if (role === "assistant" &&
|
|
8976
|
-
var u =
|
|
8992
|
+
if (role === "assistant" && roundUsage) {
|
|
8993
|
+
var u = roundUsage;
|
|
8977
8994
|
var parts = [];
|
|
8978
|
-
if (u.inputTokens
|
|
8979
|
-
if (u.outputTokens
|
|
8980
|
-
if (u.cacheReadInputTokens
|
|
8981
|
-
if (u.totalCostUsd
|
|
8995
|
+
if (u.inputTokens > 0) parts.push("输入 " + u.inputTokens);
|
|
8996
|
+
if (u.outputTokens > 0) parts.push("输出 " + u.outputTokens);
|
|
8997
|
+
if (u.cacheReadInputTokens > 0) parts.push("缓存 " + u.cacheReadInputTokens);
|
|
8998
|
+
if (u.totalCostUsd > 0) parts.push("$" + u.totalCostUsd.toFixed(4));
|
|
8982
8999
|
if (parts.length > 0) {
|
|
8983
9000
|
usageHtml = '<div class="message-usage">' + parts.join(" · ") + '</div>';
|
|
8984
9001
|
}
|
|
@@ -7525,29 +7525,29 @@
|
|
|
7525
7525
|
.structured-status-bar {
|
|
7526
7526
|
display: flex;
|
|
7527
7527
|
align-items: center;
|
|
7528
|
-
gap:
|
|
7529
|
-
margin:
|
|
7530
|
-
padding:
|
|
7528
|
+
gap: 8px;
|
|
7529
|
+
margin: 0;
|
|
7530
|
+
padding: 4px 10px;
|
|
7531
7531
|
border-radius: var(--radius-sm);
|
|
7532
|
-
background:
|
|
7533
|
-
border:
|
|
7534
|
-
font-size: 0.
|
|
7535
|
-
color: var(--text-
|
|
7532
|
+
background: transparent;
|
|
7533
|
+
border: none;
|
|
7534
|
+
font-size: 0.6875rem;
|
|
7535
|
+
color: var(--text-muted);
|
|
7536
7536
|
transition: all 0.3s ease;
|
|
7537
7537
|
overflow: hidden;
|
|
7538
7538
|
}
|
|
7539
7539
|
|
|
7540
7540
|
.structured-status-bar .status-bar-label {
|
|
7541
7541
|
flex-shrink: 0;
|
|
7542
|
-
font-weight:
|
|
7543
|
-
color: var(--
|
|
7542
|
+
font-weight: 500;
|
|
7543
|
+
color: var(--text-muted);
|
|
7544
7544
|
}
|
|
7545
7545
|
|
|
7546
7546
|
.structured-status-bar .status-bar-track {
|
|
7547
7547
|
flex: 1;
|
|
7548
|
-
height:
|
|
7549
|
-
border-radius:
|
|
7550
|
-
background: rgba(var(--accent-rgb, 99, 102, 241), 0.
|
|
7548
|
+
height: 2px;
|
|
7549
|
+
border-radius: 1px;
|
|
7550
|
+
background: rgba(var(--accent-rgb, 99, 102, 241), 0.08);
|
|
7551
7551
|
overflow: hidden;
|
|
7552
7552
|
position: relative;
|
|
7553
7553
|
}
|
|
@@ -7556,9 +7556,9 @@
|
|
|
7556
7556
|
position: absolute;
|
|
7557
7557
|
top: 0;
|
|
7558
7558
|
left: 0;
|
|
7559
|
-
width:
|
|
7559
|
+
width: 30%;
|
|
7560
7560
|
height: 100%;
|
|
7561
|
-
border-radius:
|
|
7561
|
+
border-radius: 1px;
|
|
7562
7562
|
background: linear-gradient(90deg, transparent, var(--accent-soft), transparent);
|
|
7563
7563
|
animation: marqueeScroll 1.5s ease-in-out infinite;
|
|
7564
7564
|
}
|
|
@@ -7572,14 +7572,14 @@
|
|
|
7572
7572
|
flex-shrink: 0;
|
|
7573
7573
|
font-variant-numeric: tabular-nums;
|
|
7574
7574
|
font-family: var(--font-mono);
|
|
7575
|
-
font-size: 0.
|
|
7575
|
+
font-size: 0.625rem;
|
|
7576
7576
|
color: var(--text-muted);
|
|
7577
7577
|
}
|
|
7578
7578
|
|
|
7579
7579
|
/* 完成态 */
|
|
7580
7580
|
.structured-status-bar.completed {
|
|
7581
|
-
background:
|
|
7582
|
-
border-color:
|
|
7581
|
+
background: transparent;
|
|
7582
|
+
border-color: transparent;
|
|
7583
7583
|
animation: statusBarFadeOut 2s ease-out 1s forwards;
|
|
7584
7584
|
}
|
|
7585
7585
|
|
|
@@ -7600,9 +7600,9 @@
|
|
|
7600
7600
|
}
|
|
7601
7601
|
|
|
7602
7602
|
@keyframes statusBarFadeOut {
|
|
7603
|
-
0% { opacity: 1; max-height:
|
|
7604
|
-
70% { opacity: 0; max-height:
|
|
7605
|
-
100% { opacity: 0; max-height: 0; margin: 0; padding: 0
|
|
7603
|
+
0% { opacity: 1; max-height: 30px; margin: 0; padding: 4px 10px; }
|
|
7604
|
+
70% { opacity: 0; max-height: 30px; margin: 0; padding: 4px 10px; }
|
|
7605
|
+
100% { opacity: 0; max-height: 0; margin: 0; padding: 0 10px; border-width: 0; }
|
|
7606
7606
|
}
|
|
7607
7607
|
|
|
7608
7608
|
/* 结束标记 */
|