@co0ontty/wand 1.5.5 → 1.6.0

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;
@@ -397,6 +397,24 @@ function getLatestClaudeTaskId(excludeIds) {
397
397
  return null;
398
398
  }
399
399
  }
400
+ /** Derive a short summary for a session from user messages or current task. */
401
+ function deriveSessionSummary(messages, currentTaskTitle) {
402
+ // Prefer first user message as summary
403
+ for (const msg of messages) {
404
+ if (msg.role !== "user")
405
+ continue;
406
+ for (const block of msg.content) {
407
+ if (block.type === "text" && block.text.trim()) {
408
+ return block.text.trim().slice(0, 120);
409
+ }
410
+ }
411
+ break; // only check the first user turn
412
+ }
413
+ // Fallback to current task title
414
+ if (currentTaskTitle)
415
+ return currentTaskTitle.slice(0, 120);
416
+ return undefined;
417
+ }
400
418
  export class ProcessManager extends EventEmitter {
401
419
  config;
402
420
  storage;
@@ -1102,7 +1120,8 @@ export class ProcessManager extends EventEmitter {
1102
1120
  resumedToSessionId: record.resumedToSessionId ?? undefined,
1103
1121
  autoRecovered: record.autoRecovered ?? false,
1104
1122
  autoApprovePermissions: record.autoApprovePermissions || undefined,
1105
- approvalStats: record.approvalStats.total > 0 ? record.approvalStats : undefined
1123
+ approvalStats: record.approvalStats.total > 0 ? record.approvalStats : undefined,
1124
+ summary: deriveSessionSummary(messages, record.currentTask?.title ?? null),
1106
1125
  };
1107
1126
  }
1108
1127
  isPermissionBlocked(record) {
@@ -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;
@@ -4,6 +4,23 @@ const STREAM_EMIT_DEBOUNCE_MS = 16;
4
4
  function isRunningAsRoot() {
5
5
  return process.getuid?.() === 0 || process.geteuid?.() === 0;
6
6
  }
7
+ /** Enrich a snapshot with a derived summary from the first user message. */
8
+ function withSummary(snapshot) {
9
+ if (snapshot.summary)
10
+ return snapshot;
11
+ const messages = snapshot.messages ?? [];
12
+ for (const msg of messages) {
13
+ if (msg.role !== "user")
14
+ continue;
15
+ for (const block of msg.content) {
16
+ if (block.type === "text" && block.text.trim()) {
17
+ return { ...snapshot, summary: block.text.trim().slice(0, 120) };
18
+ }
19
+ }
20
+ break;
21
+ }
22
+ return snapshot;
23
+ }
7
24
  /** Should we auto-approve permissions for this mode? */
8
25
  function shouldAutoApproveForMode(mode) {
9
26
  return mode === "full-access" || mode === "managed" || mode === "auto-edit";
@@ -46,10 +63,13 @@ export class StructuredSessionManager {
46
63
  this.emitEvent = emitEvent;
47
64
  }
48
65
  list() {
49
- return Array.from(this.sessions.values()).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
66
+ return Array.from(this.sessions.values())
67
+ .map(withSummary)
68
+ .sort((a, b) => b.startedAt.localeCompare(a.startedAt));
50
69
  }
51
70
  get(id) {
52
- return this.sessions.get(id) ?? null;
71
+ const s = this.sessions.get(id);
72
+ return s ? withSummary(s) : null;
53
73
  }
54
74
  createSession(options) {
55
75
  const id = randomUUID();
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;
@@ -182,6 +189,8 @@ export interface SessionSnapshot {
182
189
  file: number;
183
190
  total: number;
184
191
  };
192
+ /** 会话摘要:从首条用户消息或当前任务提取 */
193
+ summary?: string;
185
194
  }
186
195
  export type SessionLifecycleState = "initializing" | "running" | "idle" | "thinking" | "waiting-input" | "archived";
187
196
  export interface SessionLifecycle {
@@ -678,7 +678,6 @@
678
678
  '</div>' +
679
679
  '<div class="todo-progress-body hidden" id="todo-progress-body">' +
680
680
  '<ul class="todo-progress-list" id="todo-progress-list"></ul>' +
681
- '<div id="recent-actions" class="recent-actions"></div>' +
682
681
  '</div>' +
683
682
  '</div>' +
684
683
  '<div class="input-composer">' +
@@ -804,31 +803,50 @@
804
803
 
805
804
  // General config tab
806
805
  '<div class="settings-panel" id="settings-tab-general">' +
807
- '<div class="field">' +
808
- '<label class="field-label" for="cfg-host">监听地址 (host)</label>' +
809
- '<input id="cfg-host" type="text" class="field-input" placeholder="127.0.0.1" />' +
810
- '</div>' +
811
- '<div class="field">' +
812
- '<label class="field-label" for="cfg-port">端口 (port)</label>' +
813
- '<input id="cfg-port" type="number" class="field-input" placeholder="8443" min="1" max="65535" />' +
806
+ '<div class="field-row">' +
807
+ '<div class="field">' +
808
+ '<label class="field-label" for="cfg-host">监听地址 (host)</label>' +
809
+ '<input id="cfg-host" type="text" class="field-input" placeholder="127.0.0.1" />' +
810
+ '</div>' +
811
+ '<div class="field">' +
812
+ '<label class="field-label" for="cfg-port">端口 (port)</label>' +
813
+ '<input id="cfg-port" type="number" class="field-input" placeholder="8443" min="1" max="65535" />' +
814
+ '</div>' +
814
815
  '</div>' +
815
816
  '<div class="field field-inline">' +
816
- '<label class="field-label" for="cfg-https">启用 HTTPS</label>' +
817
817
  '<input id="cfg-https" type="checkbox" class="field-checkbox" />' +
818
+ '<label class="field-label" for="cfg-https">启用 HTTPS</label>' +
818
819
  '</div>' +
819
- '<div class="field">' +
820
- '<label class="field-label" for="cfg-mode">默认执行模式</label>' +
821
- '<select id="cfg-mode" class="field-input">' +
822
- '<option value="default">default</option>' +
823
- '<option value="assist">assist</option>' +
824
- '<option value="agent">agent</option>' +
825
- '<option value="agent-max">agent-max</option>' +
826
- '<option value="auto-edit">auto-edit</option>' +
827
- '<option value="full-access">full-access</option>' +
828
- '<option value="native">native</option>' +
829
- '<option value="managed">managed</option>' +
830
- '</select>' +
820
+ '<div class="field-row">' +
821
+ '<div class="field">' +
822
+ '<label class="field-label" for="cfg-mode">默认执行模式</label>' +
823
+ '<select id="cfg-mode" class="field-input">' +
824
+ '<option value="default">default</option>' +
825
+ '<option value="assist">assist</option>' +
826
+ '<option value="agent">agent</option>' +
827
+ '<option value="agent-max">agent-max</option>' +
828
+ '<option value="auto-edit">auto-edit</option>' +
829
+ '<option value="full-access">full-access</option>' +
830
+ '<option value="native">native</option>' +
831
+ '<option value="managed">managed</option>' +
832
+ '</select>' +
833
+ '</div>' +
834
+ '<div class="field">' +
835
+ '<label class="field-label" for="cfg-language">回复语言</label>' +
836
+ '<select id="cfg-language" class="field-input">' +
837
+ '<option value="">自动(不指定)</option>' +
838
+ '<option value="中文">中文</option>' +
839
+ '<option value="English">English</option>' +
840
+ '<option value="日本語">日本語</option>' +
841
+ '<option value="한국어">한국어</option>' +
842
+ '<option value="Español">Español</option>' +
843
+ '<option value="Français">Français</option>' +
844
+ '<option value="Deutsch">Deutsch</option>' +
845
+ '<option value="Русский">Русский</option>' +
846
+ '</select>' +
847
+ '</div>' +
831
848
  '</div>' +
849
+ '<p class="field-hint" style="margin-top:-4px;">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
832
850
  '<div class="field">' +
833
851
  '<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
834
852
  '<input id="cfg-cwd" type="text" class="field-input" placeholder="/home/user" />' +
@@ -837,52 +855,40 @@
837
855
  '<label class="field-label" for="cfg-shell">Shell</label>' +
838
856
  '<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
839
857
  '</div>' +
840
- '<div class="field">' +
841
- '<label class="field-label" for="cfg-language">回复语言</label>' +
842
- '<select id="cfg-language" class="field-input">' +
843
- '<option value="">自动(不指定)</option>' +
844
- '<option value="中文">中文</option>' +
845
- '<option value="English">English</option>' +
846
- '<option value="日本語">日本語</option>' +
847
- '<option value="한국어">한국어</option>' +
848
- '<option value="Español">Español</option>' +
849
- '<option value="Français">Français</option>' +
850
- '<option value="Deutsch">Deutsch</option>' +
851
- '<option value="Русский">Русский</option>' +
852
- '</select>' +
853
- '<p class="hint" style="margin-top:4px;margin-bottom:0;">设置后,Claude 将尽量使用指定语言回复。</p>' +
854
- '</div>' +
855
858
  '<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
856
859
  '<p id="config-message" class="hint hidden"></p>' +
857
860
  '</div>' +
858
861
 
859
862
  // Security tab
860
863
  '<div class="settings-panel" id="settings-tab-security">' +
861
- '<h3 class="settings-section-title">修改密码</h3>' +
862
- '<div class="field">' +
863
- '<label class="field-label" for="new-password">新密码</label>' +
864
- '<input id="new-password" type="password" class="field-input" placeholder="输入新密码(至少 6 个字符)" autocomplete="new-password" />' +
865
- '</div>' +
866
- '<div class="field">' +
867
- '<label class="field-label" for="confirm-password">确认密码</label>' +
868
- '<input id="confirm-password" type="password" class="field-input" placeholder="再次输入新密码" autocomplete="new-password" />' +
869
- '</div>' +
870
- '<button id="save-password-button" class="btn btn-primary btn-block">保存密码</button>' +
871
- '<p id="settings-error" class="error-message hidden"></p>' +
872
- '<p id="settings-success" class="hint hidden" style="color: var(--success);"></p>' +
873
- '<hr class="settings-divider" />' +
874
- '<h3 class="settings-section-title">SSL 证书</h3>' +
875
- '<p class="settings-hint" id="cert-status">加载中...</p>' +
876
- '<div class="field">' +
877
- '<label class="field-label" for="cert-key-file">私钥文件 (.key)</label>' +
878
- '<input id="cert-key-file" type="file" class="field-input field-file" accept=".key,.pem" />' +
864
+ '<div class="settings-card">' +
865
+ '<h3 class="settings-section-title">\ud83d\udd12 修改密码</h3>' +
866
+ '<div class="field">' +
867
+ '<label class="field-label" for="new-password">新密码</label>' +
868
+ '<input id="new-password" type="password" class="field-input" placeholder="输入新密码(至少 6 个字符)" autocomplete="new-password" />' +
869
+ '</div>' +
870
+ '<div class="field">' +
871
+ '<label class="field-label" for="confirm-password">确认密码</label>' +
872
+ '<input id="confirm-password" type="password" class="field-input" placeholder="再次输入新密码" autocomplete="new-password" />' +
873
+ '</div>' +
874
+ '<button id="save-password-button" class="btn btn-primary btn-block">保存密码</button>' +
875
+ '<p id="settings-error" class="error-message hidden"></p>' +
876
+ '<p id="settings-success" class="hint hidden" style="color: var(--success);"></p>' +
879
877
  '</div>' +
880
- '<div class="field">' +
881
- '<label class="field-label" for="cert-cert-file">证书文件 (.crt/.pem)</label>' +
882
- '<input id="cert-cert-file" type="file" class="field-input field-file" accept=".crt,.pem,.cert" />' +
878
+ '<div class="settings-card">' +
879
+ '<h3 class="settings-section-title">\ud83d\udd10 SSL 证书</h3>' +
880
+ '<p class="settings-hint" id="cert-status">加载中...</p>' +
881
+ '<div class="field">' +
882
+ '<label class="field-label" for="cert-key-file">私钥文件 (.key)</label>' +
883
+ '<input id="cert-key-file" type="file" class="field-input field-file" accept=".key,.pem" />' +
884
+ '</div>' +
885
+ '<div class="field">' +
886
+ '<label class="field-label" for="cert-cert-file">证书文件 (.crt/.pem)</label>' +
887
+ '<input id="cert-cert-file" type="file" class="field-input field-file" accept=".crt,.pem,.cert" />' +
888
+ '</div>' +
889
+ '<button id="upload-cert-button" class="btn btn-primary btn-block">上传证书</button>' +
890
+ '<p id="cert-message" class="hint hidden"></p>' +
883
891
  '</div>' +
884
- '<button id="upload-cert-button" class="btn btn-primary btn-block">上传证书</button>' +
885
- '<p id="cert-message" class="hint hidden"></p>' +
886
892
  '</div>' +
887
893
 
888
894
  // Command presets tab
@@ -1809,7 +1815,9 @@
1809
1815
  '<div class="session-item-row">' +
1810
1816
  checkbox +
1811
1817
  '<div class="session-main">' +
1812
- '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>' +
1818
+ (session.summary
1819
+ ? '<div class="session-title">' + escapeHtml(session.summary) + '</div>'
1820
+ : '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>') +
1813
1821
  '<div class="session-meta">' +
1814
1822
  modeBadge +
1815
1823
  '<span>' + escapeHtml(modeName) + '</span>' +
@@ -3943,15 +3951,23 @@
3943
3951
  function selectSession(id) {
3944
3952
  var foundSession = state.sessions.find(function(item) { return item.id === id; });
3945
3953
  console.log("[WAND] selectSession id:", id, "found:", !!foundSession, "sessionKind:", foundSession && foundSession.sessionKind, "runner:", foundSession && foundSession.runner, "isStructured:", isStructuredSession(foundSession));
3954
+ if (!foundSession) {
3955
+ console.warn("[WAND] selectSession: session not found, skipping", id);
3956
+ return;
3957
+ }
3946
3958
  state.selectedId = id;
3947
3959
  persistSelectedId();
3960
+ // Clear queued inputs from the previous session to prevent cross-session leaks
3961
+ state.messageQueue = [];
3962
+ state.pendingMessages = [];
3963
+ updateQueueCounter();
3948
3964
  resetChatRenderCache();
3949
3965
  state.currentMessages = [];
3950
3966
  if (chatRenderTimer) { clearTimeout(chatRenderTimer); chatRenderTimer = null; }
3951
3967
  // Reset todo progress bar
3952
3968
  var todoEl = document.getElementById("todo-progress");
3953
3969
  if (todoEl) todoEl.classList.add("hidden");
3954
- var session = state.sessions.find(function(item) { return item.id === id; });
3970
+ var session = foundSession;
3955
3971
  state.preferredCommand = getPreferredTool();
3956
3972
  state.chatMode = getSafeModeForTool("claude", session && session.mode ? session.mode : state.chatMode);
3957
3973
  if (state.terminalInteractive && session && session.status !== "running") {
@@ -4010,6 +4026,8 @@
4010
4026
  var focusTrapHandler = null;
4011
4027
 
4012
4028
  function openSessionModal() {
4029
+ // Close settings modal first if open (mutual exclusion)
4030
+ closeSettingsModal();
4013
4031
  state.modalOpen = true;
4014
4032
  state.sessionsDrawerOpen = false;
4015
4033
  updateDrawerState();
@@ -4084,6 +4102,8 @@
4084
4102
  }
4085
4103
 
4086
4104
  function openSettingsModal() {
4105
+ // Close session modal first if open (mutual exclusion)
4106
+ closeSessionModal();
4087
4107
  var modal = document.getElementById("settings-modal");
4088
4108
  if (modal) {
4089
4109
  modal.classList.remove("hidden");
@@ -4240,7 +4260,7 @@
4240
4260
  '<span class="preset-detail">' + escapeHtml(p.command) + (p.mode ? ' (' + escapeHtml(p.mode) + ')' : '') + '</span>' +
4241
4261
  '</div>';
4242
4262
  }
4243
- if (!html) html = '<p class="settings-hint">没有命令预设。</p>';
4263
+ 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>';
4244
4264
  presetsList.innerHTML = html;
4245
4265
  }
4246
4266
  })
@@ -4542,7 +4562,10 @@
4542
4562
  });
4543
4563
  }
4544
4564
 
4565
+ var _sessionCreating = false;
4566
+
4545
4567
  function runCommand() {
4568
+ if (_sessionCreating) return;
4546
4569
  var cwdEl = document.getElementById("cwd");
4547
4570
  var errorEl = document.getElementById("modal-error");
4548
4571
  var command = getPreferredTool();
@@ -4564,6 +4587,7 @@
4564
4587
 
4565
4588
  function startStructuredSessionFromModal(cwd, mode, errorEl) {
4566
4589
  console.log("[WAND] startStructuredSessionFromModal cwd:", cwd, "mode:", mode);
4590
+ _sessionCreating = true;
4567
4591
  state.modeValue = mode;
4568
4592
  state.chatMode = mode;
4569
4593
  state.sessionTool = "claude";
@@ -4579,11 +4603,13 @@
4579
4603
  .then(function() { focusInputBox(true); })
4580
4604
  .catch(function(error) {
4581
4605
  showError(errorEl, (error && error.message) || "无法启动结构化会话,请确认 Claude 已正确安装。");
4582
- });
4606
+ })
4607
+ .finally(function() { _sessionCreating = false; });
4583
4608
  }
4584
4609
 
4585
4610
  function runPtyCommandFromModal(command, cwd, mode, errorEl) {
4586
4611
  console.log("[WAND] runPtyCommandFromModal command:", command, "cwd:", cwd, "mode:", mode);
4612
+ _sessionCreating = true;
4587
4613
  state.modeValue = mode;
4588
4614
  state.chatMode = mode;
4589
4615
  state.sessionTool = command;
@@ -4626,7 +4652,8 @@
4626
4652
  })
4627
4653
  .catch(function() {
4628
4654
  showError(errorEl, "无法启动会话,请确认 Claude 已正确安装。");
4629
- });
4655
+ })
4656
+ .finally(function() { _sessionCreating = false; });
4630
4657
  }
4631
4658
 
4632
4659
  function initBlankChatCwd() {
@@ -6012,9 +6039,12 @@
6012
6039
  });
6013
6040
  }
6014
6041
 
6042
+ var _resumeInProgress = false;
6043
+
6015
6044
  function resumeSession(sessionId, errorEl) {
6016
6045
  console.log("[WAND] resumeSession sessionId:", sessionId);
6017
- if (!sessionId) return Promise.resolve(null);
6046
+ if (!sessionId || _resumeInProgress) return Promise.resolve(null);
6047
+ _resumeInProgress = true;
6018
6048
  return fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/resume", {
6019
6049
  method: "POST",
6020
6050
  headers: { "Content-Type": "application/json" },
@@ -6040,7 +6070,8 @@
6040
6070
  if (errorEl) showError(errorEl, message);
6041
6071
  else showToast(message, "error");
6042
6072
  return null;
6043
- });
6073
+ })
6074
+ .finally(function() { _resumeInProgress = false; });
6044
6075
  }
6045
6076
 
6046
6077
  function resumeClaudeSessionById(claudeSessionId, errorEl) {
@@ -8177,12 +8208,16 @@
8177
8208
  smartScrollToBottom(chatMessages);
8178
8209
  });
8179
8210
  } else if (msgCount === existingCount && outputHash !== prevHash) {
8180
- // Same message count but content changed (streaming update). Re-render in place
8181
- // by index so assistant growth, tool cards, and retroactive message fixes all show up.
8211
+ // Same message count but content changed (streaming update).
8212
+ // Optimization: only re-render the newest N messages (column-reverse: first children)
8213
+ // that actually differ, starting from the top (newest). Most streaming updates only
8214
+ // touch the latest assistant turn, so we can skip scanning all older messages.
8182
8215
  var existingEls = Array.from(chatMessages.querySelectorAll(".chat-message"));
8183
8216
  var reversedMessages = messages.slice().reverse();
8184
8217
  var replacedAny = false;
8185
- for (var mi = 0; mi < reversedMessages.length && mi < existingEls.length; mi++) {
8218
+ // Scan from newest (index 0 in reversed) up to MAX_STREAMING_SCAN messages
8219
+ var MAX_STREAMING_SCAN = Math.min(4, reversedMessages.length, existingEls.length);
8220
+ for (var mi = 0; mi < MAX_STREAMING_SCAN; mi++) {
8186
8221
  var currentEl = existingEls[mi];
8187
8222
  var tmpWrap = document.createElement("div");
8188
8223
  var srOrigIdx = reversedMessages.length - 1 - mi;
@@ -8193,8 +8228,16 @@
8193
8228
  chatMessages.replaceChild(replacementEl, currentEl);
8194
8229
  attachCopyHandler(replacementEl);
8195
8230
  replacedAny = true;
8231
+ } else if (mi > 0) {
8232
+ // Once we hit an unchanged older message, stop scanning
8233
+ break;
8196
8234
  }
8197
8235
  }
8236
+ // Fallback: if hash changed but no visible diff found in the top N messages,
8237
+ // the change is deeper — trigger a full render to avoid stale display.
8238
+ if (!replacedAny && reversedMessages.length > MAX_STREAMING_SCAN) {
8239
+ fullRenderChat();
8240
+ }
8198
8241
  if (replacedAny) {
8199
8242
  requestAnimationFrame(function() {
8200
8243
  smartScrollToBottom(chatMessages);
@@ -8333,40 +8376,6 @@
8333
8376
  list.innerHTML = html;
8334
8377
  }
8335
8378
 
8336
- // Extract recent important actions for key points summary
8337
- var recentActions = [];
8338
- var actionTools = ["Write", "Edit", "Bash", "WebFetch", "WebSearch"];
8339
- var msgCount = messages.length;
8340
- for (var ai = 0; ai < msgCount && recentActions.length < 5; ai++) {
8341
- var m = messages[ai];
8342
- if (!m.content || !Array.isArray(m.content)) continue;
8343
- for (var bi = 0; bi < m.content.length && recentActions.length < 5; bi++) {
8344
- var blk = m.content[bi];
8345
- if (blk.type !== "tool_use") continue;
8346
- var toolName = blk.name || "";
8347
- if (actionTools.indexOf(toolName) === -1) continue;
8348
- var desc = blk.description || generateInputSummary(toolName, blk.input) || toolName;
8349
- if (desc && desc.length > 50) desc = desc.slice(0, 47) + "...";
8350
- var icon = getToolIcon(toolName);
8351
- recentActions.push({ icon: icon, text: desc });
8352
- }
8353
- }
8354
-
8355
- var actionsEl = document.getElementById("recent-actions");
8356
- if (actionsEl) {
8357
- if (recentActions.length > 0) {
8358
- var actionsHtml = '<div class="recent-actions-label">最近操作</div>';
8359
- actionsHtml += '<div class="recent-actions-list">';
8360
- for (var ri = 0; ri < recentActions.length; ri++) {
8361
- var a = recentActions[ri];
8362
- actionsHtml += '<span class="recent-action-pill">' + a.icon + ' ' + escapeHtml(a.text) + '</span>';
8363
- }
8364
- actionsHtml += '</div>';
8365
- actionsEl.innerHTML = actionsHtml;
8366
- } else {
8367
- actionsEl.innerHTML = '';
8368
- }
8369
- }
8370
8379
  }
8371
8380
 
8372
8381
  function updateQueueCounter() {
@@ -8902,6 +8911,67 @@
8902
8911
  return messages;
8903
8912
  }
8904
8913
 
8914
+ // ── 像素风猫咪头像 ──
8915
+ var PIXEL_AVATAR = (function() {
8916
+ var _ = "transparent";
8917
+ function buildSvg(grid, size) {
8918
+ var s = size || 3;
8919
+ var w = grid[0].length * s;
8920
+ var h = grid.length * s;
8921
+ var rects = "";
8922
+ for (var y = 0; y < grid.length; y++) {
8923
+ for (var x = 0; x < grid[y].length; x++) {
8924
+ if (grid[y][x] !== _) {
8925
+ rects += '<rect x="' + (x * s) + '" y="' + (y * s) + '" width="' + s + '" height="' + s + '" fill="' + grid[y][x] + '"/>';
8926
+ }
8927
+ }
8928
+ }
8929
+ return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + w + ' ' + h + '" class="pixel-avatar-svg">' + rects + '</svg>';
8930
+ }
8931
+ // 加菲猫 (勤劳初二 / AI) — 橙色系
8932
+ var o = "#F0923A", d = "#C46A1A", w = "#FFFFFF", k = "#2D2D2D", p = "#F28B9A", n = "#E87D5A";
8933
+ var garfield = [
8934
+ [_,d,_,_,_,_,_,_,d,_],
8935
+ [d,o,d,_,_,_,_,d,o,d],
8936
+ [d,o,o,o,o,o,o,o,o,d],
8937
+ [o,o,w,k,o,o,w,k,o,o],
8938
+ [o,o,w,w,o,o,w,w,o,o],
8939
+ [o,o,o,o,p,p,o,o,o,o],
8940
+ [o,d,o,n,o,o,n,o,d,o],
8941
+ [_,o,o,o,o,o,o,o,o,_],
8942
+ [_,_,o,d,o,o,d,o,_,_],
8943
+ [_,_,_,o,_,_,o,_,_,_],
8944
+ ];
8945
+ // 美短 (赛博虎妞 / 用户) — 灰色系
8946
+ var g = "#9EAAB8", dg = "#6B7B8D", lg = "#C5CED8", gn = "#7EC88B";
8947
+ var shorthair = [
8948
+ [_,dg,_,_,_,_,_,_,dg,_],
8949
+ [dg,g,dg,_,_,_,_,dg,g,dg],
8950
+ [dg,g,g,g,g,g,g,g,g,dg],
8951
+ [g,g,w,gn,g,g,w,gn,g,g],
8952
+ [g,g,w,w,g,g,w,w,g,g],
8953
+ [g,g,g,g,p,p,g,g,g,g],
8954
+ [g,dg,g,lg,g,g,lg,g,dg,g],
8955
+ [_,g,g,g,g,g,g,g,g,_],
8956
+ [_,_,g,dg,g,g,dg,g,_,_],
8957
+ [_,_,_,g,_,_,g,_,_,_],
8958
+ ];
8959
+ return {
8960
+ assistant: buildSvg(garfield),
8961
+ user: buildSvg(shorthair)
8962
+ };
8963
+ })();
8964
+
8965
+ function chatAvatar(role) {
8966
+ var isUser = role === "user";
8967
+ var svg = isUser ? PIXEL_AVATAR.user : PIXEL_AVATAR.assistant;
8968
+ var name = isUser ? "赛博虎妞" : "勤劳初二";
8969
+ return '<div class="chat-message-avatar ' + role + '">' +
8970
+ '<div class="pixel-avatar">' + svg + '</div>' +
8971
+ '<span class="avatar-name">' + name + '</span>' +
8972
+ '</div>';
8973
+ }
8974
+
8905
8975
  function renderChatMessage(msg, roundUsage) {
8906
8976
  // Thinking card (deep thought) — from PTY parsing
8907
8977
  if (msg.role === "thinking") {
@@ -8930,7 +9000,7 @@
8930
9000
  }
8931
9001
 
8932
9002
  // Legacy string content (from PTY parsing)
8933
- var avatar = msg.role === "assistant" ? '<div class="chat-message-avatar">赛博虎妞</div>' : "";
9003
+ var avatar = chatAvatar(msg.role);
8934
9004
  var bubbleContent = msg.role === "assistant" ? renderMarkdown(msg.content) : escapeHtml(msg.content);
8935
9005
  return '<div class="chat-message ' + msg.role + '">' +
8936
9006
  avatar +
@@ -9075,7 +9145,7 @@
9075
9145
 
9076
9146
  function renderStructuredMessage(msg, roundUsage) {
9077
9147
  var role = msg.role;
9078
- var avatar = role === "assistant" ? '<div class="chat-message-avatar">赛博虎妞</div>' : "";
9148
+ var avatar = chatAvatar(role);
9079
9149
 
9080
9150
  if (!msg.content || msg.content.length === 0) {
9081
9151
  if (role === "assistant") {
@@ -1162,6 +1162,17 @@
1162
1162
  letter-spacing: -0.01em;
1163
1163
  }
1164
1164
 
1165
+ .session-title {
1166
+ font-weight: 600;
1167
+ font-size: 0.8125rem;
1168
+ white-space: nowrap;
1169
+ overflow: hidden;
1170
+ text-overflow: ellipsis;
1171
+ color: var(--text-primary);
1172
+ letter-spacing: -0.01em;
1173
+ line-height: 1.3;
1174
+ }
1175
+
1165
1176
  /* ===== 会话元信息 ===== */
1166
1177
  .session-meta {
1167
1178
  display: flex;
@@ -2292,16 +2303,51 @@
2292
2303
  }
2293
2304
 
2294
2305
  /* ===== 消息头像 ===== */
2295
- .chat-message.user .chat-message-avatar {
2296
- display: none;
2306
+ .chat-message-avatar {
2307
+ display: flex;
2308
+ align-items: center;
2309
+ gap: 6px;
2310
+ padding: 0 2px 4px 2px;
2297
2311
  }
2298
2312
 
2299
- .chat-message.assistant .chat-message-avatar {
2300
- font-size: 0.75rem;
2313
+ .chat-message-avatar.assistant {
2314
+ flex-direction: row;
2315
+ }
2316
+
2317
+ .chat-message-avatar.user {
2318
+ flex-direction: row-reverse;
2319
+ }
2320
+
2321
+ .pixel-avatar {
2322
+ width: 24px;
2323
+ height: 24px;
2324
+ flex-shrink: 0;
2325
+ border-radius: 6px;
2326
+ overflow: hidden;
2327
+ background: var(--bg-tertiary);
2328
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
2329
+ }
2330
+
2331
+ .pixel-avatar-svg {
2332
+ display: block;
2333
+ width: 100%;
2334
+ height: 100%;
2335
+ image-rendering: pixelated;
2336
+ }
2337
+
2338
+ .avatar-name {
2339
+ font-size: 0.7rem;
2301
2340
  font-weight: 600;
2302
- color: var(--accent);
2303
- padding: 0 2px 4px 2px;
2304
2341
  letter-spacing: 0.03em;
2342
+ line-height: 1;
2343
+ }
2344
+
2345
+ .chat-message.assistant .avatar-name {
2346
+ color: var(--accent);
2347
+ }
2348
+
2349
+ .chat-message.user .avatar-name {
2350
+ color: var(--text-tertiary);
2305
2351
  }
2306
2352
 
2307
2353
  /* ===== 消息气泡 ===== */
@@ -3904,39 +3950,6 @@
3904
3950
  .todo-item-icon.active { color: var(--accent); }
3905
3951
  .todo-item-icon.done { color: #4a7a4f; }
3906
3952
 
3907
- /* Recent actions key points section */
3908
- .recent-actions {
3909
- margin-top: 8px;
3910
- padding-top: 8px;
3911
- border-top: 1px solid var(--border-subtle);
3912
- }
3913
- .recent-actions-label {
3914
- font-size: 0.6875rem;
3915
- font-weight: 600;
3916
- color: var(--text-muted);
3917
- text-transform: uppercase;
3918
- letter-spacing: 0.03em;
3919
- margin-bottom: 6px;
3920
- }
3921
- .recent-actions-list {
3922
- display: flex;
3923
- flex-wrap: wrap;
3924
- gap: 4px;
3925
- }
3926
- .recent-action-pill {
3927
- display: inline-flex;
3928
- align-items: center;
3929
- gap: 3px;
3930
- padding: 3px 8px;
3931
- font-size: 0.6875rem;
3932
- color: var(--text-secondary);
3933
- background: rgba(79, 122, 88, 0.08);
3934
- border-radius: 10px;
3935
- white-space: nowrap;
3936
- max-width: 220px;
3937
- overflow: hidden;
3938
- text-overflow: ellipsis;
3939
- }
3940
3953
 
3941
3954
  @keyframes spin {
3942
3955
  to { transform: rotate(360deg); }
@@ -4802,7 +4815,7 @@
4802
4815
  .modal-backdrop {
4803
4816
  position: fixed;
4804
4817
  inset: 0;
4805
- z-index: 100;
4818
+ z-index: 500;
4806
4819
  background: rgba(42, 28, 18, 0.52);
4807
4820
  backdrop-filter: blur(12px);
4808
4821
  -webkit-backdrop-filter: blur(12px);
@@ -5684,6 +5697,7 @@
5684
5697
  .modal { max-height: 80vh; }
5685
5698
  .modal-header { padding: 8px 10px; min-height: 36px; }
5686
5699
  .modal-body { padding: 8px 10px; }
5700
+ .field-row { grid-template-columns: 1fr; gap: 0; }
5687
5701
 
5688
5702
  .btn { min-height: 36px; padding: 8px 12px; }
5689
5703
  .btn-sm { min-height: 28px; padding: 4px 8px; }
@@ -6287,18 +6301,21 @@
6287
6301
  background: none;
6288
6302
  border: none;
6289
6303
  border-bottom: 2px solid transparent;
6304
+ border-radius: var(--radius-sm) var(--radius-sm) 0 0;
6290
6305
  cursor: pointer;
6291
- transition: color 0.15s, border-color 0.15s;
6306
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
6292
6307
  white-space: nowrap;
6293
6308
  }
6294
6309
 
6295
6310
  .settings-tab:hover {
6296
6311
  color: var(--text-primary);
6312
+ background: rgba(197, 101, 61, 0.06);
6297
6313
  }
6298
6314
 
6299
6315
  .settings-tab.active {
6300
6316
  color: var(--accent);
6301
6317
  border-bottom-color: var(--accent);
6318
+ background: var(--accent-muted);
6302
6319
  }
6303
6320
 
6304
6321
  .settings-panel {
@@ -6312,8 +6329,11 @@
6312
6329
  .settings-about-info {
6313
6330
  display: flex;
6314
6331
  flex-direction: column;
6315
- gap: 10px;
6332
+ gap: 0;
6316
6333
  margin-bottom: 18px;
6334
+ background: var(--bg-secondary);
6335
+ border-radius: var(--radius-md);
6336
+ padding: 2px 14px;
6317
6337
  }
6318
6338
 
6319
6339
  .settings-about-row {
@@ -6321,6 +6341,12 @@
6321
6341
  justify-content: space-between;
6322
6342
  align-items: center;
6323
6343
  font-size: 0.8125rem;
6344
+ padding: 9px 0;
6345
+ border-bottom: 1px solid var(--border-subtle);
6346
+ }
6347
+
6348
+ .settings-about-row:last-child {
6349
+ border-bottom: none;
6324
6350
  }
6325
6351
 
6326
6352
  .settings-label {
@@ -6356,8 +6382,8 @@
6356
6382
  .settings-section-title {
6357
6383
  font-size: 0.8125rem;
6358
6384
  font-weight: 600;
6359
- color: var(--fg-primary);
6360
- margin-bottom: 10px;
6385
+ color: var(--text-primary);
6386
+ margin: 0 0 12px 0;
6361
6387
  letter-spacing: 0.02em;
6362
6388
  }
6363
6389
 
@@ -6367,13 +6393,6 @@
6367
6393
  margin-top: 10px;
6368
6394
  }
6369
6395
 
6370
- .settings-section-title {
6371
- font-size: 0.875rem;
6372
- font-weight: 600;
6373
- color: var(--text-primary);
6374
- margin: 0 0 12px 0;
6375
- }
6376
-
6377
6396
  .settings-divider {
6378
6397
  border: none;
6379
6398
  border-top: 1px solid var(--border-subtle);
@@ -6397,6 +6416,49 @@
6397
6416
  margin-bottom: 0;
6398
6417
  }
6399
6418
 
6419
+ .field-row {
6420
+ display: grid;
6421
+ grid-template-columns: 1fr 1fr;
6422
+ gap: 12px;
6423
+ margin-bottom: 14px;
6424
+ }
6425
+ .field-row .field {
6426
+ margin-bottom: 0;
6427
+ }
6428
+
6429
+ .settings-card {
6430
+ background: var(--bg-secondary);
6431
+ border-radius: var(--radius-md);
6432
+ padding: 16px;
6433
+ margin-bottom: 14px;
6434
+ }
6435
+ .settings-card .field:last-of-type {
6436
+ margin-bottom: 12px;
6437
+ }
6438
+ .settings-card .settings-section-title {
6439
+ margin-top: 0;
6440
+ }
6441
+ .settings-card .field-input {
6442
+ background: rgba(255, 255, 255, 0.6);
6443
+ }
6444
+ .settings-card .btn-block {
6445
+ margin-bottom: 0;
6446
+ }
6447
+
6448
+ .empty-state-compact {
6449
+ display: flex;
6450
+ flex-direction: column;
6451
+ align-items: center;
6452
+ gap: 6px;
6453
+ padding: 32px 16px;
6454
+ color: var(--text-muted);
6455
+ font-size: 0.8125rem;
6456
+ }
6457
+ .empty-state-compact .empty-icon {
6458
+ font-size: 1.5rem;
6459
+ opacity: 0.5;
6460
+ }
6461
+
6400
6462
  .field-checkbox {
6401
6463
  width: 18px;
6402
6464
  height: 18px;
@@ -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.5",
3
+ "version": "1.6.0",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {