@co0ontty/wand 1.5.4 → 1.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,7 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { WandStorage } from "./storage.js";
3
- import { ExecutionMode, SessionSnapshot, WandConfig } from "./types.js";
4
- export interface ProcessEvent {
5
- type: "output" | "status" | "started" | "ended" | "usage" | "task" | "notification";
6
- sessionId: string;
7
- data?: unknown;
8
- }
3
+ import { ExecutionMode, ProcessEventHandler, SessionSnapshot, WandConfig } from "./types.js";
4
+ export type { ProcessEvent, ProcessEventHandler } from "./types.js";
9
5
  /** Human-readable task information for the UI */
10
6
  export interface TaskInfo {
11
7
  title: string;
@@ -17,7 +13,6 @@ export declare class SessionInputError extends Error {
17
13
  readonly sessionStatus?: SessionSnapshot["status"] | undefined;
18
14
  constructor(message: string, code: "SESSION_NOT_FOUND" | "SESSION_NOT_RUNNING" | "SESSION_NO_PTY", sessionId: string, sessionStatus?: SessionSnapshot["status"] | undefined);
19
15
  }
20
- export type ProcessEventHandler = (event: ProcessEvent) => void;
21
16
  /** A Claude Code session discovered by scanning ~/.claude/projects/ directories. */
22
17
  export interface ClaudeHistorySession {
23
18
  claudeSessionId: string;
@@ -1,6 +1,5 @@
1
1
  import { WandStorage } from "./storage.js";
2
- import { ExecutionMode, SessionRunner, SessionSnapshot, WandConfig } from "./types.js";
3
- import { ProcessEvent } from "./ws-broadcast.js";
2
+ import { ExecutionMode, ProcessEvent, SessionRunner, SessionSnapshot, WandConfig } from "./types.js";
4
3
  interface CreateStructuredSessionOptions {
5
4
  cwd: string;
6
5
  mode: ExecutionMode;
package/dist/types.d.ts CHANGED
@@ -8,6 +8,13 @@ export type EscalationScope = "write_file" | "run_command" | "network" | "outsid
8
8
  export type EscalationRunner = "json" | "pty";
9
9
  export type EscalationResolution = "approve_once" | "approve_turn" | "deny" | "fallback_manual";
10
10
  export type EscalationSource = "tool_permission_request" | "sandbox_hard_block" | "workspace_policy_limit" | "cli_capability_limit" | "unknown";
11
+ /** WebSocket / ProcessManager event envelope used throughout the app. */
12
+ export interface ProcessEvent {
13
+ type: "output" | "status" | "started" | "ended" | "usage" | "task" | "notification";
14
+ sessionId: string;
15
+ data?: unknown;
16
+ }
17
+ export type ProcessEventHandler = (event: ProcessEvent) => void;
11
18
  export interface EscalationRequest {
12
19
  requestId: string;
13
20
  scope: EscalationScope;
@@ -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>' +
@@ -788,31 +804,50 @@
788
804
 
789
805
  // General config tab
790
806
  '<div class="settings-panel" id="settings-tab-general">' +
791
- '<div class="field">' +
792
- '<label class="field-label" for="cfg-host">监听地址 (host)</label>' +
793
- '<input id="cfg-host" type="text" class="field-input" placeholder="127.0.0.1" />' +
794
- '</div>' +
795
- '<div class="field">' +
796
- '<label class="field-label" for="cfg-port">端口 (port)</label>' +
797
- '<input id="cfg-port" type="number" class="field-input" placeholder="8443" min="1" max="65535" />' +
807
+ '<div class="field-row">' +
808
+ '<div class="field">' +
809
+ '<label class="field-label" for="cfg-host">监听地址 (host)</label>' +
810
+ '<input id="cfg-host" type="text" class="field-input" placeholder="127.0.0.1" />' +
811
+ '</div>' +
812
+ '<div class="field">' +
813
+ '<label class="field-label" for="cfg-port">端口 (port)</label>' +
814
+ '<input id="cfg-port" type="number" class="field-input" placeholder="8443" min="1" max="65535" />' +
815
+ '</div>' +
798
816
  '</div>' +
799
817
  '<div class="field field-inline">' +
800
- '<label class="field-label" for="cfg-https">启用 HTTPS</label>' +
801
818
  '<input id="cfg-https" type="checkbox" class="field-checkbox" />' +
819
+ '<label class="field-label" for="cfg-https">启用 HTTPS</label>' +
802
820
  '</div>' +
803
- '<div class="field">' +
804
- '<label class="field-label" for="cfg-mode">默认执行模式</label>' +
805
- '<select id="cfg-mode" class="field-input">' +
806
- '<option value="default">default</option>' +
807
- '<option value="assist">assist</option>' +
808
- '<option value="agent">agent</option>' +
809
- '<option value="agent-max">agent-max</option>' +
810
- '<option value="auto-edit">auto-edit</option>' +
811
- '<option value="full-access">full-access</option>' +
812
- '<option value="native">native</option>' +
813
- '<option value="managed">managed</option>' +
814
- '</select>' +
821
+ '<div class="field-row">' +
822
+ '<div class="field">' +
823
+ '<label class="field-label" for="cfg-mode">默认执行模式</label>' +
824
+ '<select id="cfg-mode" class="field-input">' +
825
+ '<option value="default">default</option>' +
826
+ '<option value="assist">assist</option>' +
827
+ '<option value="agent">agent</option>' +
828
+ '<option value="agent-max">agent-max</option>' +
829
+ '<option value="auto-edit">auto-edit</option>' +
830
+ '<option value="full-access">full-access</option>' +
831
+ '<option value="native">native</option>' +
832
+ '<option value="managed">managed</option>' +
833
+ '</select>' +
834
+ '</div>' +
835
+ '<div class="field">' +
836
+ '<label class="field-label" for="cfg-language">回复语言</label>' +
837
+ '<select id="cfg-language" class="field-input">' +
838
+ '<option value="">自动(不指定)</option>' +
839
+ '<option value="中文">中文</option>' +
840
+ '<option value="English">English</option>' +
841
+ '<option value="日本語">日本語</option>' +
842
+ '<option value="한국어">한국어</option>' +
843
+ '<option value="Español">Español</option>' +
844
+ '<option value="Français">Français</option>' +
845
+ '<option value="Deutsch">Deutsch</option>' +
846
+ '<option value="Русский">Русский</option>' +
847
+ '</select>' +
848
+ '</div>' +
815
849
  '</div>' +
850
+ '<p class="field-hint" style="margin-top:-4px;">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
816
851
  '<div class="field">' +
817
852
  '<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
818
853
  '<input id="cfg-cwd" type="text" class="field-input" placeholder="/home/user" />' +
@@ -821,52 +856,40 @@
821
856
  '<label class="field-label" for="cfg-shell">Shell</label>' +
822
857
  '<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
823
858
  '</div>' +
824
- '<div class="field">' +
825
- '<label class="field-label" for="cfg-language">回复语言</label>' +
826
- '<select id="cfg-language" class="field-input">' +
827
- '<option value="">自动(不指定)</option>' +
828
- '<option value="中文">中文</option>' +
829
- '<option value="English">English</option>' +
830
- '<option value="日本語">日本語</option>' +
831
- '<option value="한국어">한국어</option>' +
832
- '<option value="Español">Español</option>' +
833
- '<option value="Français">Français</option>' +
834
- '<option value="Deutsch">Deutsch</option>' +
835
- '<option value="Русский">Русский</option>' +
836
- '</select>' +
837
- '<p class="hint" style="margin-top:4px;margin-bottom:0;">设置后,Claude 将尽量使用指定语言回复。</p>' +
838
- '</div>' +
839
859
  '<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
840
860
  '<p id="config-message" class="hint hidden"></p>' +
841
861
  '</div>' +
842
862
 
843
863
  // Security tab
844
864
  '<div class="settings-panel" id="settings-tab-security">' +
845
- '<h3 class="settings-section-title">修改密码</h3>' +
846
- '<div class="field">' +
847
- '<label class="field-label" for="new-password">新密码</label>' +
848
- '<input id="new-password" type="password" class="field-input" placeholder="输入新密码(至少 6 个字符)" autocomplete="new-password" />' +
849
- '</div>' +
850
- '<div class="field">' +
851
- '<label class="field-label" for="confirm-password">确认密码</label>' +
852
- '<input id="confirm-password" type="password" class="field-input" placeholder="再次输入新密码" autocomplete="new-password" />' +
853
- '</div>' +
854
- '<button id="save-password-button" class="btn btn-primary btn-block">保存密码</button>' +
855
- '<p id="settings-error" class="error-message hidden"></p>' +
856
- '<p id="settings-success" class="hint hidden" style="color: var(--success);"></p>' +
857
- '<hr class="settings-divider" />' +
858
- '<h3 class="settings-section-title">SSL 证书</h3>' +
859
- '<p class="settings-hint" id="cert-status">加载中...</p>' +
860
- '<div class="field">' +
861
- '<label class="field-label" for="cert-key-file">私钥文件 (.key)</label>' +
862
- '<input id="cert-key-file" type="file" class="field-input field-file" accept=".key,.pem" />' +
865
+ '<div class="settings-card">' +
866
+ '<h3 class="settings-section-title">\ud83d\udd12 修改密码</h3>' +
867
+ '<div class="field">' +
868
+ '<label class="field-label" for="new-password">新密码</label>' +
869
+ '<input id="new-password" type="password" class="field-input" placeholder="输入新密码(至少 6 个字符)" autocomplete="new-password" />' +
870
+ '</div>' +
871
+ '<div class="field">' +
872
+ '<label class="field-label" for="confirm-password">确认密码</label>' +
873
+ '<input id="confirm-password" type="password" class="field-input" placeholder="再次输入新密码" autocomplete="new-password" />' +
874
+ '</div>' +
875
+ '<button id="save-password-button" class="btn btn-primary btn-block">保存密码</button>' +
876
+ '<p id="settings-error" class="error-message hidden"></p>' +
877
+ '<p id="settings-success" class="hint hidden" style="color: var(--success);"></p>' +
863
878
  '</div>' +
864
- '<div class="field">' +
865
- '<label class="field-label" for="cert-cert-file">证书文件 (.crt/.pem)</label>' +
866
- '<input id="cert-cert-file" type="file" class="field-input field-file" accept=".crt,.pem,.cert" />' +
879
+ '<div class="settings-card">' +
880
+ '<h3 class="settings-section-title">\ud83d\udd10 SSL 证书</h3>' +
881
+ '<p class="settings-hint" id="cert-status">加载中...</p>' +
882
+ '<div class="field">' +
883
+ '<label class="field-label" for="cert-key-file">私钥文件 (.key)</label>' +
884
+ '<input id="cert-key-file" type="file" class="field-input field-file" accept=".key,.pem" />' +
885
+ '</div>' +
886
+ '<div class="field">' +
887
+ '<label class="field-label" for="cert-cert-file">证书文件 (.crt/.pem)</label>' +
888
+ '<input id="cert-cert-file" type="file" class="field-input field-file" accept=".crt,.pem,.cert" />' +
889
+ '</div>' +
890
+ '<button id="upload-cert-button" class="btn btn-primary btn-block">上传证书</button>' +
891
+ '<p id="cert-message" class="hint hidden"></p>' +
867
892
  '</div>' +
868
- '<button id="upload-cert-button" class="btn btn-primary btn-block">上传证书</button>' +
869
- '<p id="cert-message" class="hint hidden"></p>' +
870
893
  '</div>' +
871
894
 
872
895
  // Command presets tab
@@ -1743,7 +1766,15 @@
1743
1766
  if (!session) return "";
1744
1767
  if (session.archived) return "已归档";
1745
1768
  if (session.permissionBlocked) return "等待授权";
1746
- return session.status;
1769
+ var statusMap = {
1770
+ "stopped": "已停止",
1771
+ "running": "运行中",
1772
+ "idle": "空闲",
1773
+ "thinking": "思考中",
1774
+ "waiting-input": "等待输入",
1775
+ "initializing": "启动中"
1776
+ };
1777
+ return statusMap[session.status] || session.status;
1747
1778
  }
1748
1779
 
1749
1780
  function getSessionStatusClass(session) {
@@ -2312,19 +2343,6 @@
2312
2343
  var selectedIndex = -1;
2313
2344
  var folderItems = [];
2314
2345
 
2315
- function saveWorkingDir(path) {
2316
- state.workingDir = path;
2317
- try {
2318
- localStorage.setItem("wand-working-dir", path);
2319
- } catch (e) {
2320
- // Ignore localStorage errors
2321
- }
2322
- // Also add to recent paths (defined later, will be called after function is available)
2323
- if (typeof addRecentPath === "function") {
2324
- addRecentPath(path);
2325
- }
2326
- }
2327
-
2328
2346
  // Helper functions for path validation feedback
2329
2347
  function showValidationError(message) {
2330
2348
  if (folderPickerInput) {
@@ -2349,21 +2367,7 @@
2349
2367
  }
2350
2368
 
2351
2369
  // 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([]); });
2357
- }
2358
-
2359
- function addRecentPath(path) {
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() {});
2366
- }
2370
+ // NOTE: fetchRecentPaths and addRecentPath are defined at outer scope
2367
2371
 
2368
2372
  function renderRecentPathsHtml(items) {
2369
2373
  if (!items.length) return "";
@@ -2690,6 +2694,32 @@
2690
2694
  setupVisualViewportHandlers();
2691
2695
  }
2692
2696
 
2697
+ function saveWorkingDir(path) {
2698
+ state.workingDir = path;
2699
+ try {
2700
+ localStorage.setItem("wand-working-dir", path);
2701
+ } catch (e) {
2702
+ // Ignore localStorage errors
2703
+ }
2704
+ addRecentPath(path);
2705
+ }
2706
+
2707
+ function fetchRecentPaths(callback) {
2708
+ fetch("/api/recent-paths", { credentials: "same-origin" })
2709
+ .then(function(res) { return res.json(); })
2710
+ .then(function(items) { callback(items || []); })
2711
+ .catch(function() { callback([]); });
2712
+ }
2713
+
2714
+ function addRecentPath(path) {
2715
+ return fetch("/api/recent-paths", {
2716
+ method: "POST",
2717
+ headers: { "Content-Type": "application/json" },
2718
+ credentials: "same-origin",
2719
+ body: JSON.stringify({ path: path })
2720
+ }).catch(function() {});
2721
+ }
2722
+
2693
2723
  function activateSessionItem(sessionId) {
2694
2724
  var session = state.sessions.find(function(s) { return s.id === sessionId; });
2695
2725
  if (session && session.status !== "running" && !isStructuredSession(session)) {
@@ -3490,13 +3520,13 @@
3490
3520
  }
3491
3521
 
3492
3522
  function getSessionKindLabel(session) {
3493
- return isStructuredSession(session) ? "Structured" : "PTY";
3523
+ return isStructuredSession(session) ? "结构化" : "终端";
3494
3524
  }
3495
3525
 
3496
3526
  function getSessionKindDescription(session) {
3497
3527
  return isStructuredSession(session)
3498
- ? "Structured · block transcript"
3499
- : "PTY · terminal session";
3528
+ ? "结构化 · 块级记录"
3529
+ : "终端 · PTY 会话";
3500
3530
  }
3501
3531
 
3502
3532
  function isRecoverableToolError(toolResult, nextResult) {
@@ -3830,8 +3860,9 @@
3830
3860
  var exitEl = document.getElementById("session-exit-display");
3831
3861
  var cwdText = selectedSession && selectedSession.cwd ? selectedSession.cwd : "未设置目录";
3832
3862
  var modeText = selectedSession ? getModeLabel(selectedSession.mode) : "默认";
3833
- var kindText = selectedSession ? getSessionKindLabel(selectedSession) : "PTY";
3834
- var exitText = "exit=" + (selectedSession && selectedSession.exitCode !== undefined ? selectedSession.exitCode : "n/a");
3863
+ var kindText = selectedSession ? getSessionKindLabel(selectedSession) : "终端";
3864
+ var isStructured = selectedSession && isStructuredSession(selectedSession);
3865
+ var exitText = isStructured ? "" : "退出码=" + (selectedSession && selectedSession.exitCode !== undefined ? selectedSession.exitCode : "n/a");
3835
3866
  if (cwdEl && cwdEl.textContent !== cwdText) cwdEl.textContent = cwdText;
3836
3867
  if (modeEl && modeEl.textContent !== modeText) modeEl.textContent = modeText;
3837
3868
  if (kindEl && kindEl.textContent !== kindText) kindEl.textContent = kindText;
@@ -3919,15 +3950,23 @@
3919
3950
  function selectSession(id) {
3920
3951
  var foundSession = state.sessions.find(function(item) { return item.id === id; });
3921
3952
  console.log("[WAND] selectSession id:", id, "found:", !!foundSession, "sessionKind:", foundSession && foundSession.sessionKind, "runner:", foundSession && foundSession.runner, "isStructured:", isStructuredSession(foundSession));
3953
+ if (!foundSession) {
3954
+ console.warn("[WAND] selectSession: session not found, skipping", id);
3955
+ return;
3956
+ }
3922
3957
  state.selectedId = id;
3923
3958
  persistSelectedId();
3959
+ // Clear queued inputs from the previous session to prevent cross-session leaks
3960
+ state.messageQueue = [];
3961
+ state.pendingMessages = [];
3962
+ updateQueueCounter();
3924
3963
  resetChatRenderCache();
3925
3964
  state.currentMessages = [];
3926
3965
  if (chatRenderTimer) { clearTimeout(chatRenderTimer); chatRenderTimer = null; }
3927
3966
  // Reset todo progress bar
3928
3967
  var todoEl = document.getElementById("todo-progress");
3929
3968
  if (todoEl) todoEl.classList.add("hidden");
3930
- var session = state.sessions.find(function(item) { return item.id === id; });
3969
+ var session = foundSession;
3931
3970
  state.preferredCommand = getPreferredTool();
3932
3971
  state.chatMode = getSafeModeForTool("claude", session && session.mode ? session.mode : state.chatMode);
3933
3972
  if (state.terminalInteractive && session && session.status !== "running") {
@@ -3986,6 +4025,8 @@
3986
4025
  var focusTrapHandler = null;
3987
4026
 
3988
4027
  function openSessionModal() {
4028
+ // Close settings modal first if open (mutual exclusion)
4029
+ closeSettingsModal();
3989
4030
  state.modalOpen = true;
3990
4031
  state.sessionsDrawerOpen = false;
3991
4032
  updateDrawerState();
@@ -4060,6 +4101,8 @@
4060
4101
  }
4061
4102
 
4062
4103
  function openSettingsModal() {
4104
+ // Close session modal first if open (mutual exclusion)
4105
+ closeSessionModal();
4063
4106
  var modal = document.getElementById("settings-modal");
4064
4107
  if (modal) {
4065
4108
  modal.classList.remove("hidden");
@@ -4216,7 +4259,7 @@
4216
4259
  '<span class="preset-detail">' + escapeHtml(p.command) + (p.mode ? ' (' + escapeHtml(p.mode) + ')' : '') + '</span>' +
4217
4260
  '</div>';
4218
4261
  }
4219
- if (!html) html = '<p class="settings-hint">没有命令预设。</p>';
4262
+ if (!html) html = '<div class="empty-state-compact"><span class="empty-icon">\u2699</span><span>\u6ca1\u6709\u547d\u4ee4\u9884\u8bbe</span><span class="hint">\u5728 config.json \u7684 commandPresets \u4e2d\u914d\u7f6e</span></div>';
4220
4263
  presetsList.innerHTML = html;
4221
4264
  }
4222
4265
  })
@@ -4518,7 +4561,10 @@
4518
4561
  });
4519
4562
  }
4520
4563
 
4564
+ var _sessionCreating = false;
4565
+
4521
4566
  function runCommand() {
4567
+ if (_sessionCreating) return;
4522
4568
  var cwdEl = document.getElementById("cwd");
4523
4569
  var errorEl = document.getElementById("modal-error");
4524
4570
  var command = getPreferredTool();
@@ -4540,6 +4586,7 @@
4540
4586
 
4541
4587
  function startStructuredSessionFromModal(cwd, mode, errorEl) {
4542
4588
  console.log("[WAND] startStructuredSessionFromModal cwd:", cwd, "mode:", mode);
4589
+ _sessionCreating = true;
4543
4590
  state.modeValue = mode;
4544
4591
  state.chatMode = mode;
4545
4592
  state.sessionTool = "claude";
@@ -4555,11 +4602,13 @@
4555
4602
  .then(function() { focusInputBox(true); })
4556
4603
  .catch(function(error) {
4557
4604
  showError(errorEl, (error && error.message) || "无法启动结构化会话,请确认 Claude 已正确安装。");
4558
- });
4605
+ })
4606
+ .finally(function() { _sessionCreating = false; });
4559
4607
  }
4560
4608
 
4561
4609
  function runPtyCommandFromModal(command, cwd, mode, errorEl) {
4562
4610
  console.log("[WAND] runPtyCommandFromModal command:", command, "cwd:", cwd, "mode:", mode);
4611
+ _sessionCreating = true;
4563
4612
  state.modeValue = mode;
4564
4613
  state.chatMode = mode;
4565
4614
  state.sessionTool = command;
@@ -4602,7 +4651,8 @@
4602
4651
  })
4603
4652
  .catch(function() {
4604
4653
  showError(errorEl, "无法启动会话,请确认 Claude 已正确安装。");
4605
- });
4654
+ })
4655
+ .finally(function() { _sessionCreating = false; });
4606
4656
  }
4607
4657
 
4608
4658
  function initBlankChatCwd() {
@@ -5626,10 +5676,12 @@
5626
5676
  }
5627
5677
  });
5628
5678
  // Inline keyboard visibility follows current view
5629
- var inlineKeyboard = document.getElementById("inline-keyboard");
5679
+ var inlineKeyboard = document.querySelector(".inline-shortcuts-wrap");
5630
5680
  if (inlineKeyboard) inlineKeyboard.classList.toggle("hidden", structured || state.currentView !== "terminal");
5681
+ var expandedRow = document.querySelector(".inline-shortcuts-expanded-row");
5682
+ if (expandedRow) expandedRow.classList.toggle("hidden", structured || state.currentView !== "terminal");
5631
5683
  var inputHint = document.querySelector(".input-hint");
5632
- if (inputHint) inputHint.classList.toggle("hidden", structured ? false : state.currentView === "terminal");
5684
+ if (inputHint) inputHint.classList.toggle("hidden", structured ? true : state.currentView === "terminal");
5633
5685
  var container = document.getElementById("output");
5634
5686
  if (container) container.classList.toggle("interactive", !structured && state.terminalInteractive);
5635
5687
  }
@@ -5986,9 +6038,12 @@
5986
6038
  });
5987
6039
  }
5988
6040
 
6041
+ var _resumeInProgress = false;
6042
+
5989
6043
  function resumeSession(sessionId, errorEl) {
5990
6044
  console.log("[WAND] resumeSession sessionId:", sessionId);
5991
- if (!sessionId) return Promise.resolve(null);
6045
+ if (!sessionId || _resumeInProgress) return Promise.resolve(null);
6046
+ _resumeInProgress = true;
5992
6047
  return fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/resume", {
5993
6048
  method: "POST",
5994
6049
  headers: { "Content-Type": "application/json" },
@@ -6014,7 +6069,8 @@
6014
6069
  if (errorEl) showError(errorEl, message);
6015
6070
  else showToast(message, "error");
6016
6071
  return null;
6017
- });
6072
+ })
6073
+ .finally(function() { _resumeInProgress = false; });
6018
6074
  }
6019
6075
 
6020
6076
  function resumeClaudeSessionById(claudeSessionId, errorEl) {
@@ -8151,12 +8207,16 @@
8151
8207
  smartScrollToBottom(chatMessages);
8152
8208
  });
8153
8209
  } else if (msgCount === existingCount && outputHash !== prevHash) {
8154
- // Same message count but content changed (streaming update). Re-render in place
8155
- // by index so assistant growth, tool cards, and retroactive message fixes all show up.
8210
+ // Same message count but content changed (streaming update).
8211
+ // Optimization: only re-render the newest N messages (column-reverse: first children)
8212
+ // that actually differ, starting from the top (newest). Most streaming updates only
8213
+ // touch the latest assistant turn, so we can skip scanning all older messages.
8156
8214
  var existingEls = Array.from(chatMessages.querySelectorAll(".chat-message"));
8157
8215
  var reversedMessages = messages.slice().reverse();
8158
8216
  var replacedAny = false;
8159
- for (var mi = 0; mi < reversedMessages.length && mi < existingEls.length; mi++) {
8217
+ // Scan from newest (index 0 in reversed) up to MAX_STREAMING_SCAN messages
8218
+ var MAX_STREAMING_SCAN = Math.min(4, reversedMessages.length, existingEls.length);
8219
+ for (var mi = 0; mi < MAX_STREAMING_SCAN; mi++) {
8160
8220
  var currentEl = existingEls[mi];
8161
8221
  var tmpWrap = document.createElement("div");
8162
8222
  var srOrigIdx = reversedMessages.length - 1 - mi;
@@ -8167,8 +8227,16 @@
8167
8227
  chatMessages.replaceChild(replacementEl, currentEl);
8168
8228
  attachCopyHandler(replacementEl);
8169
8229
  replacedAny = true;
8230
+ } else if (mi > 0) {
8231
+ // Once we hit an unchanged older message, stop scanning
8232
+ break;
8170
8233
  }
8171
8234
  }
8235
+ // Fallback: if hash changed but no visible diff found in the top N messages,
8236
+ // the change is deeper — trigger a full render to avoid stale display.
8237
+ if (!replacedAny && reversedMessages.length > MAX_STREAMING_SCAN) {
8238
+ fullRenderChat();
8239
+ }
8172
8240
  if (replacedAny) {
8173
8241
  requestAnimationFrame(function() {
8174
8242
  smartScrollToBottom(chatMessages);
@@ -8955,6 +9023,98 @@
8955
9023
  return '<div class="structured-tool-hint">已自动恢复一次 ' + escapeHtml(getToolDisplayName(toolName)) + ' 参数问题</div>';
8956
9024
  }
8957
9025
 
9026
+ // ── 连续同类工具调用分组 ──
9027
+ var GROUPABLE_TOOLS = { Read: 1, Glob: 1, Grep: 1, WebFetch: 1, WebSearch: 1, TodoRead: 1 };
9028
+
9029
+ function groupConsecutiveTools(content) {
9030
+ var groups = [];
9031
+ var i = 0;
9032
+ while (i < content.length) {
9033
+ var block = content[i];
9034
+ if (block.type === "tool_result") { i++; continue; }
9035
+ if (block.type === "tool_use" && GROUPABLE_TOOLS[block.name]) {
9036
+ var run = [{ block: block, index: i }];
9037
+ var j = i + 1;
9038
+ while (j < content.length) {
9039
+ if (content[j].type === "tool_result") { j++; continue; }
9040
+ if (content[j].type === "tool_use" && GROUPABLE_TOOLS[content[j].name]) {
9041
+ run.push({ block: content[j], index: j });
9042
+ j++;
9043
+ } else { break; }
9044
+ }
9045
+ if (run.length >= 2) {
9046
+ groups.push({ type: "group", items: run, endIndex: j });
9047
+ } else {
9048
+ groups.push({ type: "single", block: block, index: i });
9049
+ }
9050
+ i = j;
9051
+ } else {
9052
+ groups.push({ type: "single", block: block, index: i });
9053
+ i++;
9054
+ }
9055
+ }
9056
+ return groups;
9057
+ }
9058
+
9059
+ var TOOL_GROUP_LABELS = { Read: "读取", Glob: "搜索", Grep: "搜索", WebFetch: "抓取", WebSearch: "搜索", TodoRead: "待办" };
9060
+
9061
+ function renderToolGroup(items, role, toolResults) {
9062
+ // Count by tool name
9063
+ var counts = {};
9064
+ for (var k = 0; k < items.length; k++) {
9065
+ var n = items[k].block.name;
9066
+ counts[n] = (counts[n] || 0) + 1;
9067
+ }
9068
+ // Check if all done or still pending
9069
+ var allDone = true;
9070
+ var anyError = false;
9071
+ for (var k = 0; k < items.length; k++) {
9072
+ var b = items[k].block;
9073
+ var tr = pickToolResultForDisplay(toolResults, b.id);
9074
+ if (!tr) { allDone = false; }
9075
+ else if (tr.is_error) { anyError = true; }
9076
+ }
9077
+ var statusIcon = !allDone ? "…" : (anyError ? "✗" : "✓");
9078
+ var statusClass = !allDone ? "pending" : (anyError ? "error" : "done");
9079
+ // Summary text
9080
+ var parts = [];
9081
+ for (var name in counts) {
9082
+ parts.push(counts[name] + " " + (TOOL_GROUP_LABELS[name] || name));
9083
+ }
9084
+ var summaryText = parts.join(" · ");
9085
+
9086
+ // Render each item's inline-tool card
9087
+ var innerHtml = "";
9088
+ for (var k = 0; k < items.length; k++) {
9089
+ try {
9090
+ innerHtml += renderContentBlock(items[k].block, role, toolResults, items[k].index);
9091
+ } catch (e) {
9092
+ innerHtml += '<div class="render-error">工具渲染失败</div>';
9093
+ }
9094
+ }
9095
+
9096
+ return '<div class="tool-group" data-expanded="false" data-status="' + statusClass + '">' +
9097
+ '<div class="tool-group-summary" onclick="__toolGroupToggle(this.parentNode)">' +
9098
+ '<span class="tool-group-status">' + statusIcon + '</span>' +
9099
+ '<span class="tool-group-text">' + escapeHtml(summaryText) + '</span>' +
9100
+ '<span class="tool-group-count">' + items.length + ' 个调用</span>' +
9101
+ '<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>' +
9102
+ '</div>' +
9103
+ '<div class="tool-group-body">' + innerHtml + '</div>' +
9104
+ '</div>';
9105
+ }
9106
+
9107
+ // global toggle
9108
+ window.__toolGroupToggle = function(el) {
9109
+ if (!el) return;
9110
+ var expanded = el.getAttribute("data-expanded") === "true";
9111
+ el.setAttribute("data-expanded", expanded ? "false" : "true");
9112
+ var body = el.querySelector(".tool-group-body");
9113
+ if (body) body.style.display = expanded ? "none" : "block";
9114
+ var chevron = el.querySelector(".tool-group-chevron");
9115
+ if (chevron) chevron.style.transform = expanded ? "" : "rotate(180deg)";
9116
+ };
9117
+
8958
9118
  function renderStructuredMessage(msg, roundUsage) {
8959
9119
  var role = msg.role;
8960
9120
  var avatar = role === "assistant" ? '<div class="chat-message-avatar">赛博虎妞</div>' : "";
@@ -8973,10 +9133,15 @@
8973
9133
  var blocksHtml = "";
8974
9134
 
8975
9135
  try {
8976
- for (var i = 0; i < msg.content.length; i++) {
8977
- var block = msg.content[i];
9136
+ var groups = groupConsecutiveTools(msg.content);
9137
+ for (var g = 0; g < groups.length; g++) {
9138
+ var grp = groups[g];
8978
9139
  try {
8979
- blocksHtml += renderContentBlock(block, role, toolResults, i);
9140
+ if (grp.type === "group") {
9141
+ blocksHtml += renderToolGroup(grp.items, role, toolResults);
9142
+ } else {
9143
+ blocksHtml += renderContentBlock(grp.block, role, toolResults, grp.index);
9144
+ }
8980
9145
  } catch (e) {
8981
9146
  blocksHtml += '<div class="render-error">消息块渲染失败</div>';
8982
9147
  }
@@ -8989,17 +9154,6 @@
8989
9154
  }
8990
9155
 
8991
9156
  var usageHtml = "";
8992
- if (role === "assistant" && roundUsage) {
8993
- var u = roundUsage;
8994
- var parts = [];
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));
8999
- if (parts.length > 0) {
9000
- usageHtml = '<div class="message-usage">' + parts.join(" · ") + '</div>';
9001
- }
9002
- }
9003
9157
 
9004
9158
  return '<div class="chat-message ' + role + '">' +
9005
9159
  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;
@@ -4743,7 +4802,7 @@
4743
4802
  .modal-backdrop {
4744
4803
  position: fixed;
4745
4804
  inset: 0;
4746
- z-index: 100;
4805
+ z-index: 500;
4747
4806
  background: rgba(42, 28, 18, 0.52);
4748
4807
  backdrop-filter: blur(12px);
4749
4808
  -webkit-backdrop-filter: blur(12px);
@@ -5625,6 +5684,7 @@
5625
5684
  .modal { max-height: 80vh; }
5626
5685
  .modal-header { padding: 8px 10px; min-height: 36px; }
5627
5686
  .modal-body { padding: 8px 10px; }
5687
+ .field-row { grid-template-columns: 1fr; gap: 0; }
5628
5688
 
5629
5689
  .btn { min-height: 36px; padding: 8px 12px; }
5630
5690
  .btn-sm { min-height: 28px; padding: 4px 8px; }
@@ -6228,18 +6288,21 @@
6228
6288
  background: none;
6229
6289
  border: none;
6230
6290
  border-bottom: 2px solid transparent;
6291
+ border-radius: var(--radius-sm) var(--radius-sm) 0 0;
6231
6292
  cursor: pointer;
6232
- transition: color 0.15s, border-color 0.15s;
6293
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
6233
6294
  white-space: nowrap;
6234
6295
  }
6235
6296
 
6236
6297
  .settings-tab:hover {
6237
6298
  color: var(--text-primary);
6299
+ background: rgba(197, 101, 61, 0.06);
6238
6300
  }
6239
6301
 
6240
6302
  .settings-tab.active {
6241
6303
  color: var(--accent);
6242
6304
  border-bottom-color: var(--accent);
6305
+ background: var(--accent-muted);
6243
6306
  }
6244
6307
 
6245
6308
  .settings-panel {
@@ -6253,8 +6316,11 @@
6253
6316
  .settings-about-info {
6254
6317
  display: flex;
6255
6318
  flex-direction: column;
6256
- gap: 10px;
6319
+ gap: 0;
6257
6320
  margin-bottom: 18px;
6321
+ background: var(--bg-secondary);
6322
+ border-radius: var(--radius-md);
6323
+ padding: 2px 14px;
6258
6324
  }
6259
6325
 
6260
6326
  .settings-about-row {
@@ -6262,6 +6328,12 @@
6262
6328
  justify-content: space-between;
6263
6329
  align-items: center;
6264
6330
  font-size: 0.8125rem;
6331
+ padding: 9px 0;
6332
+ border-bottom: 1px solid var(--border-subtle);
6333
+ }
6334
+
6335
+ .settings-about-row:last-child {
6336
+ border-bottom: none;
6265
6337
  }
6266
6338
 
6267
6339
  .settings-label {
@@ -6297,8 +6369,8 @@
6297
6369
  .settings-section-title {
6298
6370
  font-size: 0.8125rem;
6299
6371
  font-weight: 600;
6300
- color: var(--fg-primary);
6301
- margin-bottom: 10px;
6372
+ color: var(--text-primary);
6373
+ margin: 0 0 12px 0;
6302
6374
  letter-spacing: 0.02em;
6303
6375
  }
6304
6376
 
@@ -6308,13 +6380,6 @@
6308
6380
  margin-top: 10px;
6309
6381
  }
6310
6382
 
6311
- .settings-section-title {
6312
- font-size: 0.875rem;
6313
- font-weight: 600;
6314
- color: var(--text-primary);
6315
- margin: 0 0 12px 0;
6316
- }
6317
-
6318
6383
  .settings-divider {
6319
6384
  border: none;
6320
6385
  border-top: 1px solid var(--border-subtle);
@@ -6338,6 +6403,49 @@
6338
6403
  margin-bottom: 0;
6339
6404
  }
6340
6405
 
6406
+ .field-row {
6407
+ display: grid;
6408
+ grid-template-columns: 1fr 1fr;
6409
+ gap: 12px;
6410
+ margin-bottom: 14px;
6411
+ }
6412
+ .field-row .field {
6413
+ margin-bottom: 0;
6414
+ }
6415
+
6416
+ .settings-card {
6417
+ background: var(--bg-secondary);
6418
+ border-radius: var(--radius-md);
6419
+ padding: 16px;
6420
+ margin-bottom: 14px;
6421
+ }
6422
+ .settings-card .field:last-of-type {
6423
+ margin-bottom: 12px;
6424
+ }
6425
+ .settings-card .settings-section-title {
6426
+ margin-top: 0;
6427
+ }
6428
+ .settings-card .field-input {
6429
+ background: rgba(255, 255, 255, 0.6);
6430
+ }
6431
+ .settings-card .btn-block {
6432
+ margin-bottom: 0;
6433
+ }
6434
+
6435
+ .empty-state-compact {
6436
+ display: flex;
6437
+ flex-direction: column;
6438
+ align-items: center;
6439
+ gap: 6px;
6440
+ padding: 32px 16px;
6441
+ color: var(--text-muted);
6442
+ font-size: 0.8125rem;
6443
+ }
6444
+ .empty-state-compact .empty-icon {
6445
+ font-size: 1.5rem;
6446
+ opacity: 0.5;
6447
+ }
6448
+
6341
6449
  .field-checkbox {
6342
6450
  width: 18px;
6343
6451
  height: 18px;
@@ -7522,13 +7630,61 @@
7522
7630
  }
7523
7631
 
7524
7632
  /* ── 结构化会话状态条 ── */
7633
+ /* ── 输入框顶部波浪脉冲(回复中) ── */
7634
+ .input-composer.in-flight {
7635
+ border-color: transparent;
7636
+ }
7637
+ .input-composer.in-flight::before {
7638
+ content: "";
7639
+ position: absolute;
7640
+ top: -2px;
7641
+ left: -60%;
7642
+ width: 220%;
7643
+ height: 3px;
7644
+ border-radius: 14px 14px 0 0;
7645
+ background:
7646
+ radial-gradient(ellipse 280px 4px,
7647
+ rgba(var(--accent-rgb, 197, 101, 61), 0.45) 0%,
7648
+ rgba(var(--accent-rgb, 197, 101, 61), 0.12) 35%,
7649
+ transparent 60%)
7650
+ no-repeat;
7651
+ background-size: 280px 4px;
7652
+ animation: composerWaveSlide 7s cubic-bezier(0.35, 0, 0.65, 1) infinite;
7653
+ z-index: 2;
7654
+ pointer-events: none;
7655
+ }
7656
+ .input-composer.in-flight::after {
7657
+ content: "";
7658
+ position: absolute;
7659
+ top: -2px;
7660
+ left: -60%;
7661
+ width: 220%;
7662
+ height: 3px;
7663
+ border-radius: 14px 14px 0 0;
7664
+ background:
7665
+ radial-gradient(ellipse 220px 3px,
7666
+ rgba(var(--accent-rgb, 197, 101, 61), 0.25) 0%,
7667
+ rgba(var(--accent-rgb, 197, 101, 61), 0.06) 35%,
7668
+ transparent 60%)
7669
+ no-repeat;
7670
+ background-size: 220px 3px;
7671
+ animation: composerWaveSlide 9s cubic-bezier(0.35, 0, 0.65, 1) infinite reverse;
7672
+ z-index: 2;
7673
+ pointer-events: none;
7674
+ }
7675
+ @keyframes composerWaveSlide {
7676
+ 0% { background-position: 0% center; }
7677
+ 100% { background-position: 100% center; }
7678
+ }
7679
+
7680
+ /* ── 结构化会话状态条(输入框右上角) ── */
7525
7681
  .structured-status-bar {
7526
7682
  display: flex;
7527
7683
  align-items: center;
7528
- gap: 8px;
7529
- margin: 0;
7530
- padding: 4px 10px;
7531
- border-radius: var(--radius-sm);
7684
+ justify-content: flex-end;
7685
+ gap: 5px;
7686
+ margin: 0 4px 2px 0;
7687
+ padding: 0;
7532
7688
  background: transparent;
7533
7689
  border: none;
7534
7690
  font-size: 0.6875rem;
@@ -7537,35 +7693,25 @@
7537
7693
  overflow: hidden;
7538
7694
  }
7539
7695
 
7540
- .structured-status-bar .status-bar-label {
7696
+ .structured-status-bar .status-bar-dot {
7697
+ width: 5px;
7698
+ height: 5px;
7699
+ border-radius: 50%;
7700
+ background: rgba(var(--accent-rgb, 197, 101, 61), 0.8);
7701
+ animation: statusDotPulse 1.2s ease-in-out infinite;
7541
7702
  flex-shrink: 0;
7542
- font-weight: 500;
7543
- color: var(--text-muted);
7544
7703
  }
7545
7704
 
7546
- .structured-status-bar .status-bar-track {
7547
- flex: 1;
7548
- height: 2px;
7549
- border-radius: 1px;
7550
- background: rgba(var(--accent-rgb, 99, 102, 241), 0.08);
7551
- overflow: hidden;
7552
- position: relative;
7553
- }
7554
-
7555
- .structured-status-bar .status-bar-fill {
7556
- position: absolute;
7557
- top: 0;
7558
- left: 0;
7559
- width: 30%;
7560
- height: 100%;
7561
- border-radius: 1px;
7562
- background: linear-gradient(90deg, transparent, var(--accent-soft), transparent);
7563
- animation: marqueeScroll 1.5s ease-in-out infinite;
7705
+ @keyframes statusDotPulse {
7706
+ 0%, 100% { opacity: 0.4; transform: scale(0.9); }
7707
+ 50% { opacity: 1; transform: scale(1.1); }
7564
7708
  }
7565
7709
 
7566
- @keyframes marqueeScroll {
7567
- 0% { left: -40%; }
7568
- 100% { left: 100%; }
7710
+ .structured-status-bar .status-bar-label {
7711
+ flex-shrink: 0;
7712
+ font-weight: 500;
7713
+ color: var(--text-muted);
7714
+ font-size: 0.625rem;
7569
7715
  }
7570
7716
 
7571
7717
  .structured-status-bar .status-bar-timer {
@@ -7578,8 +7724,6 @@
7578
7724
 
7579
7725
  /* 完成态 */
7580
7726
  .structured-status-bar.completed {
7581
- background: transparent;
7582
- border-color: transparent;
7583
7727
  animation: statusBarFadeOut 2s ease-out 1s forwards;
7584
7728
  }
7585
7729
 
@@ -7587,22 +7731,10 @@
7587
7731
  color: var(--success);
7588
7732
  }
7589
7733
 
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
7734
  @keyframes statusBarFadeOut {
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; }
7735
+ 0% { opacity: 1; max-height: 20px; }
7736
+ 70% { opacity: 0; max-height: 20px; }
7737
+ 100% { opacity: 0; max-height: 0; margin: 0; }
7606
7738
  }
7607
7739
 
7608
7740
  /* 结束标记 */
@@ -3,12 +3,8 @@
3
3
  * Handles debounced output events, backpressure control, and client subscriptions.
4
4
  */
5
5
  import { WebSocketServer } from "ws";
6
- import type { SessionSnapshot } from "./types.js";
7
- export interface ProcessEvent {
8
- type: "output" | "status" | "started" | "ended" | "usage" | "task" | "notification";
9
- sessionId: string;
10
- data?: unknown;
11
- }
6
+ import type { SessionSnapshot, ProcessEvent } from "./types.js";
7
+ export type { ProcessEvent } from "./types.js";
12
8
  export declare class WsBroadcastManager {
13
9
  private wss;
14
10
  private clients;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.5.4",
3
+ "version": "1.5.7",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {