@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 CHANGED
@@ -1,22 +1,18 @@
1
1
  # wand
2
2
 
3
- 通过浏览器访问本地终端,支持 Claude 等命令行工具。
3
+ 通过浏览器远程访问和管理本地 CLI 工具的 Web 控制台。专为 Claude Code 设计,支持终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览等功能。
4
4
 
5
- ## 功能
5
+ ## 安装
6
6
 
7
- - Web 终端 / Chat 模式双视图
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
- 配置文件:`~/.wand/config.json`
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 build
34
- npm run dev
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
- // Remove stale bar if session changed or not structured
168
- var existing = chatMessages.querySelector(".structured-status-bar");
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
- if (!existing) {
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
- // column-reverse: first child = visual bottom
192
- chatMessages.insertBefore(bar, chatMessages.firstChild);
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 ? (isStructuredSession(selectedSession) ? 'Structured' : 'PTY') : 'PTY') + '</span>' +
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.forEach(function(session) {
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().filter(function(session) {
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
- return session.status;
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
- function getRecentPaths() {
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
- recent.forEach(function(path) {
2385
- html += '<div class="folder-recent-item" data-path="' + escapeHtml(path) + '">' +
2386
- '<span class="folder-recent-item-icon">📁</span>' +
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
- var recentHtml = renderRecentPaths();
2398
- if (recentHtml) {
2399
- folderPickerDropdown.innerHTML = recentHtml;
2400
- folderPickerDropdown.classList.remove("hidden");
2401
- // Add click handlers for recent paths
2402
- folderPickerDropdown.querySelectorAll(".folder-recent-item").forEach(function(item) {
2403
- item.addEventListener("click", function() {
2404
- var path = this.dataset.path;
2405
- if (folderPickerInput) {
2406
- folderPickerInput.value = path;
2407
- saveWorkingDir(path);
2408
- loadFolderSuggestions(path);
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
- } else {
2413
- hideFolderDropdown();
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) ? "Structured" : "PTY";
3516
+ return isStructuredSession(session) ? "结构化" : "终端";
3507
3517
  }
3508
3518
 
3509
3519
  function getSessionKindDescription(session) {
3510
3520
  return isStructuredSession(session)
3511
- ? "Structured · block transcript"
3512
- : "PTY · terminal session";
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) : "PTY";
3847
- var exitText = "exit=" + (selectedSession && selectedSession.exitCode !== undefined ? selectedSession.exitCode : "n/a");
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
- fetch("/api/recent-paths", { credentials: "same-origin" })
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
- // Recent paths (exclude default to avoid duplicate)
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
- fetch("/api/recent-paths", { credentials: "same-origin" })
4709
- .then(function(res) { return res.json(); })
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.getElementById("inline-keyboard");
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 ? false : state.currentView === "terminal");
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
- div.innerHTML = renderChatMessage(newMessages[i]);
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
- tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi]);
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
- function renderStructuredMessage(msg) {
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
- for (var i = 0; i < msg.content.length; i++) {
8960
- var block = msg.content[i];
9094
+ var groups = groupConsecutiveTools(msg.content);
9095
+ for (var g = 0; g < groups.length; g++) {
9096
+ var grp = groups[g];
8961
9097
  try {
8962
- blocksHtml += renderContentBlock(block, role, toolResults, i);
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
- gap: 10px;
7529
- margin: 8px 0 4px;
7530
- padding: 8px 12px;
7531
- border-radius: var(--radius-sm);
7532
- background: rgba(var(--accent-rgb, 99, 102, 241), 0.06);
7533
- border: 1px solid rgba(var(--accent-rgb, 99, 102, 241), 0.12);
7534
- font-size: 0.75rem;
7535
- color: var(--text-secondary);
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-label {
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
- .structured-status-bar .status-bar-fill {
7556
- position: absolute;
7557
- top: 0;
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
- @keyframes marqueeScroll {
7567
- 0% { left: -40%; }
7568
- 100% { left: 100%; }
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.6875rem;
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: 50px; margin: 8px 0 4px; padding: 8px 12px; }
7604
- 70% { opacity: 0; max-height: 50px; margin: 8px 0 4px; padding: 8px 12px; }
7605
- 100% { opacity: 0; max-height: 0; margin: 0; padding: 0 12px; border-width: 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
  /* 结束标记 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.5.2",
3
+ "version": "1.5.5",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {