@co0ontty/wand 1.5.7 → 1.6.1

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
@@ -41,15 +41,16 @@ wand config:set port 9443
41
41
  | `port` | `8443` | 监听端口 |
42
42
  | `https` | `false` | 启用 HTTPS(自签证书自动生成) |
43
43
  | `password` | (随机生成) | 登录密码 |
44
- | `replyLanguage` | `""` | Claude 回复语言偏好 |
44
+ | `language` | `""` | Claude 回复语言偏好 |
45
45
 
46
46
  ## 功能
47
47
 
48
48
  - **双视图模式** — 终端原始输出和结构化对话视图可随时切换
49
- - **会话管理** — 创建、归档、恢复会话;支持从 Claude 原生历史记录恢复
50
- - **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略
51
- - **文件浏览器** — 内置路径浏览、搜索和收藏功能
49
+ - **会话管理** — 创建、归档、恢复会话;支持从 Claude 原生历史记录恢复;会话列表显示摘要
50
+ - **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略;工具调用自动分组
51
+ - **文件浏览器** — 内置路径浏览和搜索功能
52
52
  - **多种运行模式** — full-access / default / auto-edit 等 Claude 运行模式
53
+ - **个性化** — 像素风猫咪头像、回复语言偏好设置
53
54
  - **PWA 支持** — 可添加到主屏幕作为独立应用使用
54
55
  - **HTTPS** — 可选自签证书,适合远程或移动端访问
55
56
 
@@ -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) {
@@ -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
@@ -189,6 +189,8 @@ export interface SessionSnapshot {
189
189
  file: number;
190
190
  total: number;
191
191
  };
192
+ /** 会话摘要:从首条用户消息或当前任务提取 */
193
+ summary?: string;
192
194
  }
193
195
  export type SessionLifecycleState = "initializing" | "running" | "idle" | "thinking" | "waiting-input" | "archived";
194
196
  export interface SessionLifecycle {
@@ -90,6 +90,8 @@
90
90
  inputQueue: Promise.resolve(),
91
91
  pendingMessages: [], // WebSocket 断线期间的消息队列
92
92
  messageQueue: [], // 用户消息排队等待发送
93
+ crossSessionQueue: [], // 跨会话排队消息 [{ id, text, cwd, mode, tool }]
94
+ structuredInputQueue: [], // 结构化会话同会话排队消息
93
95
  drafts: {},
94
96
  isSyncingInputBox: false,
95
97
  loginPending: false,
@@ -678,7 +680,6 @@
678
680
  '</div>' +
679
681
  '<div class="todo-progress-body hidden" id="todo-progress-body">' +
680
682
  '<ul class="todo-progress-list" id="todo-progress-list"></ul>' +
681
- '<div id="recent-actions" class="recent-actions"></div>' +
682
683
  '</div>' +
683
684
  '</div>' +
684
685
  '<div class="input-composer">' +
@@ -1816,7 +1817,9 @@
1816
1817
  '<div class="session-item-row">' +
1817
1818
  checkbox +
1818
1819
  '<div class="session-main">' +
1819
- '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>' +
1820
+ (session.summary
1821
+ ? '<div class="session-title">' + escapeHtml(session.summary) + '</div>'
1822
+ : '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>') +
1820
1823
  '<div class="session-meta">' +
1821
1824
  modeBadge +
1822
1825
  '<span>' + escapeHtml(modeName) + '</span>' +
@@ -1995,23 +1998,23 @@
1995
1998
  var optionLabel = btnEl.dataset.optionLabel;
1996
1999
  if (optionLabel && state.selectedId) {
1997
2000
  btnEl.classList.add("selected");
1998
- var allOptions = document.querySelectorAll(".ask-user-option");
1999
- allOptions.forEach(function(opt) {
2000
- opt.classList.add("selected");
2001
- opt.style.pointerEvents = "none";
2002
- });
2003
- var cardBody = btnEl.closest(".tool-use-card.ask-user");
2004
- if (cardBody) {
2001
+ // Only disable options within the same question group, not globally
2002
+ var questionGroup = btnEl.closest(".ask-user-question-group");
2003
+ if (questionGroup) {
2004
+ questionGroup.querySelectorAll(".ask-user-option").forEach(function(opt) {
2005
+ opt.classList.add("selected");
2006
+ opt.style.pointerEvents = "none";
2007
+ });
2005
2008
  var sentDiv = document.createElement("div");
2006
2009
  sentDiv.className = "ask-user-answer-sent";
2007
- sentDiv.innerHTML = "\\u2713 \\u5df2\\u53d1\\u9001: " + escapeHtml(optionLabel);
2008
- cardBody.appendChild(sentDiv);
2010
+ sentDiv.innerHTML = "\u2713 \u5df2\u53d1\u9001: " + escapeHtml(optionLabel);
2011
+ questionGroup.appendChild(sentDiv);
2009
2012
  }
2010
2013
  fetch("/api/sessions/" + state.selectedId + "/input", {
2011
2014
  method: "POST",
2012
2015
  headers: { "Content-Type": "application/json" },
2013
2016
  credentials: "same-origin",
2014
- body: JSON.stringify({ input: optionLabel + "\\n", view: state.currentView })
2017
+ body: JSON.stringify({ input: optionLabel + "\n", view: state.currentView })
2015
2018
  }).catch(function(err) {
2016
2019
  console.error("[wand] Error sending answer:", err);
2017
2020
  });
@@ -3679,6 +3682,11 @@
3679
3682
  reconcileInteractiveState();
3680
3683
  updateTaskDisplay();
3681
3684
  }
3685
+ // When a session transitions to a non-running state, try flushing cross-session queue
3686
+ if (snapshot.status && snapshot.status !== "running" && state.crossSessionQueue.length > 0) {
3687
+ // Use setTimeout(0) to let the current event processing complete first
3688
+ setTimeout(flushCrossSessionQueue, 0);
3689
+ }
3682
3690
  }
3683
3691
 
3684
3692
  function subscribeToSession(sessionId) {
@@ -3818,6 +3826,11 @@
3818
3826
  loadOutput(state.selectedId);
3819
3827
  }
3820
3828
  }
3829
+
3830
+ // Try to flush cross-session queue on every session list refresh
3831
+ if (state.crossSessionQueue.length > 0) {
3832
+ flushCrossSessionQueue();
3833
+ }
3821
3834
  })
3822
3835
  .catch(function(e) {
3823
3836
  console.error("[wand] loadSessions failed:", e);
@@ -3830,6 +3843,8 @@
3830
3843
  if (listEl) listEl.innerHTML = renderSessions();
3831
3844
  if (countEl) countEl.textContent = String(state.sessions.length);
3832
3845
  updateShellChrome();
3846
+ // Re-render cross-session queue (container may have been destroyed by DOM rebuild)
3847
+ if (state.crossSessionQueue.length > 0) renderCrossSessionQueue();
3833
3848
  }
3834
3849
 
3835
3850
  function updateShellChrome() {
@@ -3938,6 +3953,9 @@
3938
3953
 
3939
3954
  var selectedSession = state.sessions.find(function(s) { return s.id === id; });
3940
3955
  state.currentMessages = getPreferredMessages(selectedSession, data.output, false);
3956
+ if (selectedSession && selectedSession.sessionKind === "structured") {
3957
+ appendQueuedPlaceholders(state.currentMessages);
3958
+ }
3941
3959
 
3942
3960
  if (state.terminal) {
3943
3961
  syncTerminalBuffer(id, data.output || "", { mode: "replace" });
@@ -3959,7 +3977,8 @@
3959
3977
  // Clear queued inputs from the previous session to prevent cross-session leaks
3960
3978
  state.messageQueue = [];
3961
3979
  state.pendingMessages = [];
3962
- updateQueueCounter();
3980
+ state.structuredInputQueue = [];
3981
+ updateStructuredQueueCounter();
3963
3982
  resetChatRenderCache();
3964
3983
  state.currentMessages = [];
3965
3984
  if (chatRenderTimer) { clearTimeout(chatRenderTimer); chatRenderTimer = null; }
@@ -5022,11 +5041,236 @@
5022
5041
  return !!selectedSession && selectedSession.status === "running";
5023
5042
  }
5024
5043
 
5044
+ // ── 跨会话排队 ──
5045
+
5046
+ var _queueLaunching = false; // 防止并发 launch
5047
+
5048
+ function hasAnyBusySession() {
5049
+ return state.sessions.some(function(s) {
5050
+ return s.status === "running" && !s.archived;
5051
+ });
5052
+ }
5053
+
5054
+ function enqueueCrossSessionMessage(text) {
5055
+ if (state.crossSessionQueue.length >= 10) {
5056
+ showToast("排队消息已满(最多 10 条),请等待当前会话完成。", "warning");
5057
+ return;
5058
+ }
5059
+ var id = "csq-" + Date.now() + "-" + Math.random().toString(36).slice(2, 8);
5060
+ state.crossSessionQueue.push({
5061
+ id: id,
5062
+ text: text,
5063
+ cwd: getEffectiveCwd(),
5064
+ mode: state.chatMode || "managed",
5065
+ tool: getPreferredTool(),
5066
+ queuedAt: Date.now()
5067
+ });
5068
+ renderCrossSessionQueue();
5069
+ }
5070
+
5071
+ function launchQueueItem(item) {
5072
+ if (_queueLaunching) return;
5073
+ _queueLaunching = true;
5074
+ fetch("/api/commands", {
5075
+ method: "POST",
5076
+ headers: { "Content-Type": "application/json" },
5077
+ credentials: "same-origin",
5078
+ body: JSON.stringify({
5079
+ command: item.tool,
5080
+ cwd: item.cwd,
5081
+ mode: item.mode,
5082
+ initialInput: item.text
5083
+ })
5084
+ })
5085
+ .then(function(res) { return res.json(); })
5086
+ .then(function(data) {
5087
+ _queueLaunching = false;
5088
+ if (data.error) {
5089
+ showToast(data.error, "error");
5090
+ // 失败回填队首,不丢消息
5091
+ state.crossSessionQueue.unshift(item);
5092
+ renderCrossSessionQueue();
5093
+ return null;
5094
+ }
5095
+ return activateSession(data);
5096
+ })
5097
+ .catch(function(error) {
5098
+ _queueLaunching = false;
5099
+ showToast((error && error.message) || "无法启动排队会话。", "error");
5100
+ state.crossSessionQueue.unshift(item);
5101
+ renderCrossSessionQueue();
5102
+ });
5103
+ }
5104
+
5105
+ function sendQueueItemNow(queueId) {
5106
+ var idx = state.crossSessionQueue.findIndex(function(q) { return q.id === queueId; });
5107
+ if (idx < 0) return;
5108
+ var item = state.crossSessionQueue.splice(idx, 1)[0];
5109
+ renderCrossSessionQueue();
5110
+ // 立即发送不受 _queueLaunching 限制
5111
+ fetch("/api/commands", {
5112
+ method: "POST",
5113
+ headers: { "Content-Type": "application/json" },
5114
+ credentials: "same-origin",
5115
+ body: JSON.stringify({
5116
+ command: item.tool,
5117
+ cwd: item.cwd,
5118
+ mode: item.mode,
5119
+ initialInput: item.text
5120
+ })
5121
+ })
5122
+ .then(function(res) { return res.json(); })
5123
+ .then(function(data) {
5124
+ if (data.error) {
5125
+ showToast(data.error, "error");
5126
+ return null;
5127
+ }
5128
+ return activateSession(data);
5129
+ })
5130
+ .catch(function(error) {
5131
+ showToast((error && error.message) || "无法启动排队会话。", "error");
5132
+ });
5133
+ }
5134
+
5135
+ function cancelQueueItem(queueId) {
5136
+ var idx = state.crossSessionQueue.findIndex(function(q) { return q.id === queueId; });
5137
+ if (idx < 0) return;
5138
+ state.crossSessionQueue.splice(idx, 1);
5139
+ renderCrossSessionQueue();
5140
+ if (state.crossSessionQueue.length === 0) {
5141
+ showToast("排队已清空。", "info");
5142
+ }
5143
+ }
5144
+
5145
+ function flushCrossSessionQueue() {
5146
+ if (state.crossSessionQueue.length === 0) return;
5147
+ if (hasAnyBusySession()) return;
5148
+ if (_queueLaunching) return;
5149
+ var item = state.crossSessionQueue.shift();
5150
+ renderCrossSessionQueue();
5151
+ launchQueueItem(item);
5152
+ }
5153
+
5154
+ function formatQueueAge(queuedAt) {
5155
+ var sec = Math.floor((Date.now() - queuedAt) / 1000);
5156
+ if (sec < 60) return sec + "s";
5157
+ var min = Math.floor(sec / 60);
5158
+ if (min < 60) return min + "m";
5159
+ return Math.floor(min / 60) + "h";
5160
+ }
5161
+
5162
+ function renderCrossSessionQueue() {
5163
+ var container = document.querySelector(".cross-session-queue");
5164
+ var inputPanel = document.querySelector(".input-panel");
5165
+ var statusBar = document.querySelector(".structured-status-bar");
5166
+ var composer = document.querySelector(".input-composer");
5167
+ var blankChat = document.getElementById("blank-chat");
5168
+
5169
+ if (state.crossSessionQueue.length === 0) {
5170
+ if (container) container.remove();
5171
+ return;
5172
+ }
5173
+
5174
+ // Determine parent: input-panel (session view) or blank-chat (welcome view)
5175
+ var isInputPanelVisible = inputPanel && !inputPanel.classList.contains("hidden");
5176
+ var parent = isInputPanelVisible ? inputPanel : blankChat;
5177
+ // Insert above status bar if present, otherwise above composer
5178
+ var insertBefore = isInputPanelVisible ? (statusBar || composer) : null;
5179
+
5180
+ if (!parent) return;
5181
+
5182
+ // If container exists but is in the wrong parent, move it
5183
+ if (container && container.parentNode !== parent) {
5184
+ container.remove();
5185
+ container = null;
5186
+ }
5187
+
5188
+ if (!container) {
5189
+ container = document.createElement("div");
5190
+ container.className = "cross-session-queue";
5191
+ if (insertBefore) {
5192
+ parent.insertBefore(container, insertBefore);
5193
+ } else {
5194
+ parent.appendChild(container);
5195
+ }
5196
+ } else if (isInputPanelVisible && insertBefore && container.nextSibling !== insertBefore) {
5197
+ // Ensure queue stays above status bar
5198
+ parent.insertBefore(container, insertBefore);
5199
+ }
5200
+
5201
+ var total = state.crossSessionQueue.length;
5202
+ var items = state.crossSessionQueue.map(function(item, i) {
5203
+ var preview = item.text.length > 60 ? item.text.slice(0, 60) + "…" : item.text;
5204
+ var age = formatQueueAge(item.queuedAt);
5205
+ return '<div class="queue-item" data-queue-id="' + escapeHtml(item.id) + '">' +
5206
+ '<span class="queue-item-dot"></span>' +
5207
+ '<span class="queue-item-text" title="' + escapeHtml(item.text) + '">' + escapeHtml(preview) + '</span>' +
5208
+ '<span class="queue-item-age">' + age + '</span>' +
5209
+ '<button class="queue-item-send-now" data-queue-id="' + escapeHtml(item.id) + '" title="立即发送" type="button">发送</button>' +
5210
+ '<button class="queue-item-cancel" data-queue-id="' + escapeHtml(item.id) + '" title="取消" type="button">×</button>' +
5211
+ '</div>';
5212
+ }).join("");
5213
+
5214
+ var header = total > 1
5215
+ ? '<div class="queue-header">' +
5216
+ '<span class="queue-header-label">排队 ' + total + ' 条</span>' +
5217
+ '<button class="queue-header-clear" id="queue-clear-all" type="button" title="清空排队">清空</button>' +
5218
+ '</div>'
5219
+ : '';
5220
+
5221
+ container.innerHTML = header + items;
5222
+ }
5223
+
5224
+ // 定时刷新排队项的等待时间 + 尝试 flush
5225
+ setInterval(function() {
5226
+ if (state.crossSessionQueue.length > 0) {
5227
+ // 只更新 age 文本,不重建整个 DOM
5228
+ var ages = document.querySelectorAll(".queue-item-age");
5229
+ state.crossSessionQueue.forEach(function(item, i) {
5230
+ if (ages[i]) ages[i].textContent = formatQueueAge(item.queuedAt);
5231
+ });
5232
+ // 尝试 flush 作为保底(防止 ended 事件 flush 失败)
5233
+ flushCrossSessionQueue();
5234
+ }
5235
+ }, 5000);
5236
+
5237
+ // Delegate click events for cross-session queue items
5238
+ document.addEventListener("click", function(e) {
5239
+ if (e.target.closest("#queue-clear-all")) {
5240
+ e.preventDefault();
5241
+ state.crossSessionQueue = [];
5242
+ renderCrossSessionQueue();
5243
+ showToast("排队已清空。", "info");
5244
+ return;
5245
+ }
5246
+ var sendNow = e.target.closest(".queue-item-send-now");
5247
+ if (sendNow) {
5248
+ e.preventDefault();
5249
+ sendQueueItemNow(sendNow.dataset.queueId);
5250
+ return;
5251
+ }
5252
+ var cancel = e.target.closest(".queue-item-cancel");
5253
+ if (cancel) {
5254
+ e.preventDefault();
5255
+ cancelQueueItem(cancel.dataset.queueId);
5256
+ return;
5257
+ }
5258
+ });
5259
+
5025
5260
  // Send message from the welcome screen input
5026
5261
  function welcomeInputSend() {
5027
5262
  var welcomeInput = document.getElementById("welcome-input");
5028
5263
  var value = welcomeInput ? welcomeInput.value.trim() : "";
5029
5264
  if (!value) return;
5265
+
5266
+ // Cross-session queue: if any session is busy, queue instead of creating
5267
+ if (hasAnyBusySession()) {
5268
+ welcomeInput.value = "";
5269
+ enqueueCrossSessionMessage(value);
5270
+ showToast("已排队,将在当前会话完成后自动发送。", "info");
5271
+ return;
5272
+ }
5273
+
5030
5274
  // Clear todo progress bar at the start of a new session
5031
5275
  var todoEl = document.getElementById("todo-progress");
5032
5276
  if (todoEl) todoEl.classList.add("hidden");
@@ -5092,7 +5336,14 @@
5092
5336
  return;
5093
5337
  }
5094
5338
 
5095
- // No selected session, create a new one
5339
+ // No selected session, create a new one (or queue if busy)
5340
+ if (value && hasAnyBusySession()) {
5341
+ if (inputBox) inputBox.value = "";
5342
+ if (welcomeInput) welcomeInput.value = "";
5343
+ enqueueCrossSessionMessage(value);
5344
+ showToast("已排队,将在当前会话完成后自动发送。", "info");
5345
+ return;
5346
+ }
5096
5347
  var mode = state.chatMode || "managed";
5097
5348
  var defaultCwd = getEffectiveCwd();
5098
5349
  var preferredTool = getPreferredTool();
@@ -5252,10 +5503,27 @@
5252
5503
  return Promise.resolve();
5253
5504
  }
5254
5505
  if (session.structuredState && session.structuredState.inFlight && session.status === "running") {
5255
- // Disable send button while processing, show subtle indicator
5256
- var sendBtn = document.getElementById("send-input-button");
5257
- if (sendBtn) sendBtn.disabled = true;
5258
- showToast("正在等待上一条消息处理完成…", "info");
5506
+ // Queue the message for sending after current processing completes
5507
+ if (state.structuredInputQueue.length >= 10) {
5508
+ showToast("排队消息已满(最多 10 条),请等待当前消息处理完成。", "warning");
5509
+ return Promise.resolve();
5510
+ }
5511
+ state.structuredInputQueue.push(input);
5512
+ if (inputBox) {
5513
+ inputBox.value = "";
5514
+ autoResizeInput(inputBox);
5515
+ }
5516
+ setDraftValue("");
5517
+ // Show the queued message in chat view with a "queued" marker
5518
+ var queuedTurn = { role: "user", content: [{ type: "text", text: input, __queued: true }] };
5519
+ var curMsgs = Array.isArray(state.currentMessages) ? state.currentMessages.slice() : [];
5520
+ curMsgs.push(queuedTurn);
5521
+ state.currentMessages = curMsgs;
5522
+ // Also update session.messages so the queued turn survives WS updates
5523
+ session.messages = curMsgs;
5524
+ renderChat(true);
5525
+ showToast("已排队(第 " + state.structuredInputQueue.length + " 条),将在当前消息处理完成后自动发送。", "info");
5526
+ updateStructuredQueueCounter();
5259
5527
  return Promise.resolve();
5260
5528
  }
5261
5529
 
@@ -5263,8 +5531,14 @@
5263
5531
  var userTurn = { role: "user", content: [{ type: "text", text: input }] };
5264
5532
  var thinkingTurn = { role: "assistant", content: [{ type: "text", text: "", __processing: true }] };
5265
5533
  var userMsgs = Array.isArray(session.messages) ? session.messages.slice() : [];
5534
+ // Filter out __queued placeholders — they'll be re-appended after the new turns
5535
+ userMsgs = userMsgs.filter(function(m) {
5536
+ return !(m.role === "user" && m.content && m.content.some(function(b) { return b.__queued; }));
5537
+ });
5266
5538
  userMsgs.push(userTurn);
5267
5539
  userMsgs.push(thinkingTurn);
5540
+ // Re-append remaining queued messages after the current send
5541
+ appendQueuedPlaceholders(userMsgs);
5268
5542
  session.messages = userMsgs;
5269
5543
  state.currentMessages = userMsgs;
5270
5544
  // Mark inFlight optimistically to prevent double-send via WS updates
@@ -5276,9 +5550,7 @@
5276
5550
  inputBox.value = "";
5277
5551
  autoResizeInput(inputBox);
5278
5552
  }
5279
- // Disable send button so user can't double-send
5280
- var sendBtnEl = document.getElementById("send-input-button");
5281
- if (sendBtnEl) sendBtnEl.disabled = true;
5553
+ // Keep send button enabled so user can queue more messages
5282
5554
  updateInputHint("思考中…");
5283
5555
  setDraftValue("");
5284
5556
  renderChat(true);
@@ -5298,17 +5570,28 @@
5298
5570
  updateSessionSnapshot(snapshot);
5299
5571
  if (snapshot.messages && snapshot.messages.length > 0) {
5300
5572
  state.currentMessages = snapshot.messages;
5573
+ // Re-append queued user messages
5574
+ appendQueuedPlaceholders(state.currentMessages);
5301
5575
  }
5302
5576
  renderChat(true);
5303
5577
  updateInputHint("Enter 发送 · Shift+Enter 换行");
5304
5578
  }
5305
5579
  })
5306
5580
  .catch(function(error) {
5307
- var errSendBtn = document.getElementById("send-input-button");
5308
- if (errSendBtn) errSendBtn.disabled = false;
5581
+ // Reset inFlight so user can send again
5582
+ if (session.structuredState) {
5583
+ session.structuredState.inFlight = false;
5584
+ }
5585
+ // Clear remaining queued messages since the session is likely broken
5586
+ if (state.structuredInputQueue.length > 0) {
5587
+ var dropped = state.structuredInputQueue.length;
5588
+ state.structuredInputQueue = [];
5589
+ updateStructuredQueueCounter();
5590
+ showToast("发送失败,已清空 " + dropped + " 条排队消息。", "error");
5591
+ } else {
5592
+ showToast((error && error.message) || "无法发送结构化消息。", "error");
5593
+ }
5309
5594
  updateInputHint("Enter 发送 · Shift+Enter 换行");
5310
- showToast((error && error.message) || "无法发送结构化消息。", "error");
5311
- throw error;
5312
5595
  });
5313
5596
  }
5314
5597
 
@@ -5317,6 +5600,63 @@
5317
5600
  if (hint) hint.textContent = text;
5318
5601
  }
5319
5602
 
5603
+ function updateStructuredQueueCounter() {
5604
+ var counter = document.getElementById("queue-counter");
5605
+ var count = state.structuredInputQueue.length;
5606
+ if (counter) {
5607
+ counter.textContent = "队列: " + count;
5608
+ if (count > 0) {
5609
+ counter.classList.remove("hidden");
5610
+ } else {
5611
+ counter.classList.add("hidden");
5612
+ }
5613
+ }
5614
+ }
5615
+
5616
+ // Append queued user message placeholders to currentMessages so they
5617
+ // remain visible across WS updates and re-renders.
5618
+ function appendQueuedPlaceholders(messages) {
5619
+ if (state.structuredInputQueue.length === 0) return messages;
5620
+ for (var qi = 0; qi < state.structuredInputQueue.length; qi++) {
5621
+ messages.push({ role: "user", content: [{ type: "text", text: state.structuredInputQueue[qi], __queued: true }] });
5622
+ }
5623
+ return messages;
5624
+ }
5625
+
5626
+ function flushStructuredInputQueue() {
5627
+ if (state.structuredInputQueue.length === 0) return;
5628
+ var session = state.sessions.find(function(s) { return s.id === state.selectedId; });
5629
+ if (!session || session.sessionKind !== "structured") {
5630
+ state.structuredInputQueue = [];
5631
+ updateStructuredQueueCounter();
5632
+ return;
5633
+ }
5634
+ // Only flush if not inFlight
5635
+ if (session.structuredState && session.structuredState.inFlight) return;
5636
+ var nextInput = state.structuredInputQueue.shift();
5637
+ updateStructuredQueueCounter();
5638
+ if (nextInput) {
5639
+ // Remove __queued marker from the matching user turn already in chat.
5640
+ // postStructuredInput will find it's not inFlight now and do the
5641
+ // normal send path, which re-adds the user turn + thinking turn.
5642
+ // So we need to remove the queued placeholder first to avoid duplicates.
5643
+ var msgs = Array.isArray(state.currentMessages) ? state.currentMessages : [];
5644
+ for (var qi = msgs.length - 1; qi >= 0; qi--) {
5645
+ var qm = msgs[qi];
5646
+ if (qm.role === "user" && qm.content && qm.content.some(function(b) {
5647
+ return b.__queued && b.text === nextInput;
5648
+ })) {
5649
+ msgs.splice(qi, 1);
5650
+ break;
5651
+ }
5652
+ }
5653
+ state.currentMessages = msgs;
5654
+ if (session.messages) session.messages = msgs;
5655
+ // Pass null for inputBox to avoid clearing user's current typing
5656
+ postStructuredInput(nextInput, null, session);
5657
+ }
5658
+ }
5659
+
5320
5660
  function getInputErrorMessage(error) {
5321
5661
  if (error && (error.errorCode === "SESSION_NOT_RUNNING" || error.errorCode === "SESSION_NO_PTY")) {
5322
5662
  return "会话已结束;若存在 Claude 历史会话,将在你下次发送消息时自动恢复。";
@@ -5398,12 +5738,10 @@
5398
5738
  function queueDirectInput(input, shortcutKey) {
5399
5739
  if (!input || !state.selectedId) return Promise.resolve();
5400
5740
  state.messageQueue.push(input);
5401
- updateQueueCounter();
5402
5741
  state.inputQueue = state.inputQueue.then(function() {
5403
5742
  return postInput(input, shortcutKey).finally(function() {
5404
5743
  var idx = state.messageQueue.indexOf(input);
5405
5744
  if (idx > -1) state.messageQueue.splice(idx, 1);
5406
- updateQueueCounter();
5407
5745
  scheduleMobileDomUpdate();
5408
5746
  });
5409
5747
  });
@@ -7412,6 +7750,10 @@
7412
7750
  updateSessionSnapshot(snapshot);
7413
7751
  if (msg.sessionId === state.selectedId) {
7414
7752
  state.currentMessages = getPreferredMessages(snapshot, msg.data.output, false);
7753
+ // Re-append queued user messages that haven't been sent yet
7754
+ if (msg.data.sessionKind === 'structured') {
7755
+ appendQueuedPlaceholders(state.currentMessages);
7756
+ }
7415
7757
  // Structured session with inFlight: keep __processing placeholder
7416
7758
  // so the loading indicator stays visible until assistant content arrives
7417
7759
  if (msg.data.sessionKind === 'structured') {
@@ -7471,10 +7813,7 @@
7471
7813
  }
7472
7814
  updateSessionSnapshot(endedSnapshot);
7473
7815
 
7474
- // Re-enable send button when structured session finishes
7475
7816
  if (msg.sessionId === state.selectedId) {
7476
- var endedSendBtn = document.getElementById("send-input-button");
7477
- if (endedSendBtn) endedSendBtn.disabled = false;
7478
7817
  updateInputHint("Enter 发送 · Shift+Enter 换行");
7479
7818
  // Trigger status bar completion animation
7480
7819
  scheduleChatRender(true);
@@ -7506,16 +7845,32 @@
7506
7845
  });
7507
7846
  }
7508
7847
 
7509
- // Clear stale queued inputs so they cannot race with the ended session.
7510
- // Each queued item's postInput will hit the server and get an error, but
7511
- // clearing the queues here prevents them from growing unbounded.
7848
+ // Clear stale queued inputs for PTY sessions.
7849
+ // For structured sessions, each "ended" means one turn completed (not
7850
+ // the session terminated), so we must NOT clear the structured queue —
7851
+ // instead, flush the next queued message.
7512
7852
  state.messageQueue = [];
7513
7853
  state.pendingMessages = [];
7514
7854
 
7855
+ var endedSessionObj = state.sessions.find(function(s) { return s.id === msg.sessionId; });
7856
+ var isStructuredEnded = endedSessionObj && endedSessionObj.sessionKind === "structured";
7857
+
7858
+ if (isStructuredEnded && msg.sessionId === state.selectedId &&
7859
+ state.structuredInputQueue.length > 0) {
7860
+ // Structured session turn completed — flush next queued message
7861
+ setTimeout(flushStructuredInputQueue, 50);
7862
+ } else if (!isStructuredEnded) {
7863
+ // PTY session ended — clear structured queue too
7864
+ state.structuredInputQueue = [];
7865
+ updateStructuredQueueCounter();
7866
+ }
7867
+
7515
7868
  // Disable terminal interactive mode immediately so the terminal stops
7516
7869
  // capturing keystrokes before loadSessions() completes.
7517
7870
  if (msg.sessionId === state.selectedId) {
7518
- setTerminalInteractive(false);
7871
+ if (!isStructuredEnded) {
7872
+ setTerminalInteractive(false);
7873
+ }
7519
7874
  state.currentTask = null;
7520
7875
  updateTaskDisplay();
7521
7876
  }
@@ -7526,9 +7881,14 @@
7526
7881
  updateShellChrome();
7527
7882
  }
7528
7883
 
7529
- loadSessions();
7884
+ loadSessions().then(function() {
7885
+ // After sessions list is refreshed, try to flush cross-session queue
7886
+ flushCrossSessionQueue();
7887
+ });
7530
7888
  if (msg.sessionId === state.selectedId) {
7531
- loadOutput(msg.sessionId);
7889
+ if (!isStructuredEnded) {
7890
+ loadOutput(msg.sessionId);
7891
+ }
7532
7892
  }
7533
7893
  break;
7534
7894
  }
@@ -7625,6 +7985,11 @@
7625
7985
  // Re-render chat when structured session inFlight state changes
7626
7986
  if (statusUpdate.structuredState) {
7627
7987
  scheduleChatRender();
7988
+ // Flush queued structured messages when inFlight clears
7989
+ if (!statusUpdate.structuredState.inFlight) {
7990
+ updateInputHint("Enter 发送 · Shift+Enter 换行");
7991
+ setTimeout(flushStructuredInputQueue, 50);
7992
+ }
7628
7993
  }
7629
7994
  }
7630
7995
  }
@@ -7896,6 +8261,9 @@
7896
8261
  var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
7897
8262
  if (selectedSession) {
7898
8263
  state.currentMessages = getPreferredMessages(selectedSession, selectedSession.output, true);
8264
+ if (selectedSession.sessionKind === "structured") {
8265
+ appendQueuedPlaceholders(state.currentMessages);
8266
+ }
7899
8267
  }
7900
8268
  renderChat();
7901
8269
  }, 30);
@@ -8375,53 +8743,8 @@
8375
8743
  list.innerHTML = html;
8376
8744
  }
8377
8745
 
8378
- // Extract recent important actions for key points summary
8379
- var recentActions = [];
8380
- var actionTools = ["Write", "Edit", "Bash", "WebFetch", "WebSearch"];
8381
- var msgCount = messages.length;
8382
- for (var ai = 0; ai < msgCount && recentActions.length < 5; ai++) {
8383
- var m = messages[ai];
8384
- if (!m.content || !Array.isArray(m.content)) continue;
8385
- for (var bi = 0; bi < m.content.length && recentActions.length < 5; bi++) {
8386
- var blk = m.content[bi];
8387
- if (blk.type !== "tool_use") continue;
8388
- var toolName = blk.name || "";
8389
- if (actionTools.indexOf(toolName) === -1) continue;
8390
- var desc = blk.description || generateInputSummary(toolName, blk.input) || toolName;
8391
- if (desc && desc.length > 50) desc = desc.slice(0, 47) + "...";
8392
- var icon = getToolIcon(toolName);
8393
- recentActions.push({ icon: icon, text: desc });
8394
- }
8395
- }
8396
-
8397
- var actionsEl = document.getElementById("recent-actions");
8398
- if (actionsEl) {
8399
- if (recentActions.length > 0) {
8400
- var actionsHtml = '<div class="recent-actions-label">最近操作</div>';
8401
- actionsHtml += '<div class="recent-actions-list">';
8402
- for (var ri = 0; ri < recentActions.length; ri++) {
8403
- var a = recentActions[ri];
8404
- actionsHtml += '<span class="recent-action-pill">' + a.icon + ' ' + escapeHtml(a.text) + '</span>';
8405
- }
8406
- actionsHtml += '</div>';
8407
- actionsEl.innerHTML = actionsHtml;
8408
- } else {
8409
- actionsEl.innerHTML = '';
8410
- }
8411
- }
8412
8746
  }
8413
8747
 
8414
- function updateQueueCounter() {
8415
- var counter = document.getElementById("queue-counter");
8416
- if (!counter) return;
8417
- var count = state.messageQueue.length;
8418
- if (count > 0) {
8419
- counter.textContent = "队列: " + count;
8420
- counter.classList.remove("hidden");
8421
- } else {
8422
- counter.classList.add("hidden");
8423
- }
8424
- }
8425
8748
 
8426
8749
  function attachCopyHandler(el) {
8427
8750
  el.querySelectorAll(".code-copy").forEach(function(btn) {
@@ -8944,6 +9267,67 @@
8944
9267
  return messages;
8945
9268
  }
8946
9269
 
9270
+ // ── 像素风猫咪头像 ──
9271
+ var PIXEL_AVATAR = (function() {
9272
+ var _ = "transparent";
9273
+ function buildSvg(grid, size) {
9274
+ var s = size || 3;
9275
+ var w = grid[0].length * s;
9276
+ var h = grid.length * s;
9277
+ var rects = "";
9278
+ for (var y = 0; y < grid.length; y++) {
9279
+ for (var x = 0; x < grid[y].length; x++) {
9280
+ if (grid[y][x] !== _) {
9281
+ rects += '<rect x="' + (x * s) + '" y="' + (y * s) + '" width="' + s + '" height="' + s + '" fill="' + grid[y][x] + '"/>';
9282
+ }
9283
+ }
9284
+ }
9285
+ return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + w + ' ' + h + '" class="pixel-avatar-svg">' + rects + '</svg>';
9286
+ }
9287
+ // 加菲猫 (勤劳初二 / AI) — 橙色系
9288
+ var o = "#F0923A", d = "#C46A1A", w = "#FFFFFF", k = "#2D2D2D", p = "#F28B9A", n = "#E87D5A";
9289
+ var garfield = [
9290
+ [_,d,_,_,_,_,_,_,d,_],
9291
+ [d,o,d,_,_,_,_,d,o,d],
9292
+ [d,o,o,o,o,o,o,o,o,d],
9293
+ [o,o,w,k,o,o,w,k,o,o],
9294
+ [o,o,w,w,o,o,w,w,o,o],
9295
+ [o,o,o,o,p,p,o,o,o,o],
9296
+ [o,d,o,n,o,o,n,o,d,o],
9297
+ [_,o,o,o,o,o,o,o,o,_],
9298
+ [_,_,o,d,o,o,d,o,_,_],
9299
+ [_,_,_,o,_,_,o,_,_,_],
9300
+ ];
9301
+ // 美短 (赛博虎妞 / 用户) — 灰色系
9302
+ var g = "#9EAAB8", dg = "#6B7B8D", lg = "#C5CED8", gn = "#7EC88B";
9303
+ var shorthair = [
9304
+ [_,dg,_,_,_,_,_,_,dg,_],
9305
+ [dg,g,dg,_,_,_,_,dg,g,dg],
9306
+ [dg,g,g,g,g,g,g,g,g,dg],
9307
+ [g,g,w,gn,g,g,w,gn,g,g],
9308
+ [g,g,w,w,g,g,w,w,g,g],
9309
+ [g,g,g,g,p,p,g,g,g,g],
9310
+ [g,dg,g,lg,g,g,lg,g,dg,g],
9311
+ [_,g,g,g,g,g,g,g,g,_],
9312
+ [_,_,g,dg,g,g,dg,g,_,_],
9313
+ [_,_,_,g,_,_,g,_,_,_],
9314
+ ];
9315
+ return {
9316
+ assistant: buildSvg(garfield),
9317
+ user: buildSvg(shorthair)
9318
+ };
9319
+ })();
9320
+
9321
+ function chatAvatar(role) {
9322
+ var isUser = role === "user";
9323
+ var svg = isUser ? PIXEL_AVATAR.user : PIXEL_AVATAR.assistant;
9324
+ var name = isUser ? "赛博虎妞" : "勤劳初二";
9325
+ return '<div class="chat-message-avatar ' + role + '">' +
9326
+ '<div class="pixel-avatar">' + svg + '</div>' +
9327
+ '<span class="avatar-name">' + name + '</span>' +
9328
+ '</div>';
9329
+ }
9330
+
8947
9331
  function renderChatMessage(msg, roundUsage) {
8948
9332
  // Thinking card (deep thought) — from PTY parsing
8949
9333
  if (msg.role === "thinking") {
@@ -8972,7 +9356,7 @@
8972
9356
  }
8973
9357
 
8974
9358
  // Legacy string content (from PTY parsing)
8975
- var avatar = msg.role === "assistant" ? '<div class="chat-message-avatar">赛博虎妞</div>' : "";
9359
+ var avatar = chatAvatar(msg.role);
8976
9360
  var bubbleContent = msg.role === "assistant" ? renderMarkdown(msg.content) : escapeHtml(msg.content);
8977
9361
  return '<div class="chat-message ' + msg.role + '">' +
8978
9362
  avatar +
@@ -9117,7 +9501,10 @@
9117
9501
 
9118
9502
  function renderStructuredMessage(msg, roundUsage) {
9119
9503
  var role = msg.role;
9120
- var avatar = role === "assistant" ? '<div class="chat-message-avatar">赛博虎妞</div>' : "";
9504
+ var avatar = chatAvatar(role);
9505
+
9506
+ // Check if this is a queued user message
9507
+ var isQueued = role === "user" && msg.content && msg.content.some(function(b) { return b.__queued; });
9121
9508
 
9122
9509
  if (!msg.content || msg.content.length === 0) {
9123
9510
  if (role === "assistant") {
@@ -9154,10 +9541,12 @@
9154
9541
  }
9155
9542
 
9156
9543
  var usageHtml = "";
9544
+ var queuedClass = isQueued ? " queued" : "";
9545
+ var queuedBadge = isQueued ? '<span class="queued-badge">排队中</span>' : "";
9157
9546
 
9158
- return '<div class="chat-message ' + role + '">' +
9547
+ return '<div class="chat-message ' + role + queuedClass + '">' +
9159
9548
  avatar +
9160
- '<div class="chat-message-content">' + blocksHtml + '</div>' +
9549
+ '<div class="chat-message-content">' + blocksHtml + queuedBadge + '</div>' +
9161
9550
  usageHtml +
9162
9551
  '</div>';
9163
9552
  }
@@ -9481,27 +9870,29 @@
9481
9870
  if (toolName === "AskUserQuestion" && block.input && block.input.questions) {
9482
9871
  var questions = block.input.questions;
9483
9872
  if (questions && questions.length > 0) {
9484
- var question = questions[0];
9485
- var questionText = question.question ? '<div class="ask-user-title">' + escapeHtml(question.question) + '</div>' : "";
9486
- var optionsHtml = "";
9487
- if (question.options && question.options.length > 0) {
9488
- optionsHtml = '<div class="ask-user-options">';
9489
- question.options.forEach(function(opt, idx) {
9490
- var label = opt.label ? escapeHtml(opt.label) : "选项 " + (idx + 1);
9491
- optionsHtml += '<button class="ask-user-option" data-option-index="' + idx + '" data-option-label="' + escapeHtml(label) + '" onclick="__askOption(this)">' +
9492
- '<div class="ask-user-option-label">' + label + '</div>' +
9493
- '</button>';
9494
- });
9495
- optionsHtml += '</div>';
9496
- }
9873
+ var questionsHtml = "";
9874
+ questions.forEach(function(question, qIdx) {
9875
+ var questionText = question.question ? '<div class="ask-user-title">' + escapeHtml(question.question) + '</div>' : "";
9876
+ var optionsHtml = "";
9877
+ if (question.options && question.options.length > 0) {
9878
+ optionsHtml = '<div class="ask-user-options">';
9879
+ question.options.forEach(function(opt, idx) {
9880
+ var label = opt.label ? escapeHtml(opt.label) : "选项 " + (idx + 1);
9881
+ optionsHtml += '<button class="ask-user-option" data-option-index="' + idx + '" data-question-index="' + qIdx + '" data-option-label="' + escapeHtml(label) + '" onclick="__askOption(this)">' +
9882
+ '<div class="ask-user-option-label">' + label + '</div>' +
9883
+ '</button>';
9884
+ });
9885
+ optionsHtml += '</div>';
9886
+ }
9887
+ questionsHtml += '<div class="ask-user-question-group">' + questionText + optionsHtml + '</div>';
9888
+ });
9497
9889
  return '<div class="tool-use-card ask-user" data-tool-use-id="' + escapeHtml(toolId) + '">' +
9498
9890
  '<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
9499
9891
  '<span class="tool-use-icon">?</span>' +
9500
9892
  '<span class="tool-use-name">提问</span>' +
9501
9893
  '</div>' +
9502
9894
  '<div class="tool-use-body ask-user-body">' +
9503
- questionText +
9504
- optionsHtml +
9895
+ questionsHtml +
9505
9896
  '</div>' +
9506
9897
  '</div>';
9507
9898
  }
@@ -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
  /* ===== 消息气泡 ===== */
@@ -2720,6 +2766,12 @@
2720
2766
  .ask-user-body {
2721
2767
  padding: 10px 12px;
2722
2768
  }
2769
+ .ask-user-question-group {
2770
+ margin-bottom: 10px;
2771
+ }
2772
+ .ask-user-question-group:last-child {
2773
+ margin-bottom: 0;
2774
+ }
2723
2775
  .ask-user-title {
2724
2776
  font-size: 0.875rem;
2725
2777
  font-weight: 500;
@@ -3724,6 +3776,22 @@
3724
3776
  50% { opacity: 0.7; }
3725
3777
  }
3726
3778
 
3779
+ /* Queued message in chat view */
3780
+ .chat-message.user.queued {
3781
+ opacity: 0.6;
3782
+ }
3783
+ .queued-badge {
3784
+ display: inline-block;
3785
+ font-size: 0.625rem;
3786
+ color: var(--accent);
3787
+ background: var(--accent-muted);
3788
+ padding: 1px 6px;
3789
+ border-radius: 8px;
3790
+ font-weight: 600;
3791
+ margin-top: 4px;
3792
+ animation: queuePulse 1.5s ease-in-out infinite;
3793
+ }
3794
+
3727
3795
  .input-hint {
3728
3796
  font-size: 0.5625rem;
3729
3797
  color: var(--text-muted);
@@ -3904,39 +3972,6 @@
3904
3972
  .todo-item-icon.active { color: var(--accent); }
3905
3973
  .todo-item-icon.done { color: #4a7a4f; }
3906
3974
 
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
3975
 
3941
3976
  @keyframes spin {
3942
3977
  to { transform: rotate(360deg); }
@@ -7737,4 +7772,125 @@
7737
7772
  100% { opacity: 0; max-height: 0; margin: 0; }
7738
7773
  }
7739
7774
 
7775
+ /* ── 跨会话排队消息 ── */
7776
+ .cross-session-queue {
7777
+ display: flex;
7778
+ flex-direction: column;
7779
+ gap: 2px;
7780
+ margin: 0 8px 4px 8px;
7781
+ }
7782
+
7783
+ .queue-header {
7784
+ display: flex;
7785
+ align-items: center;
7786
+ gap: 5px;
7787
+ padding: 0 4px;
7788
+ font-size: 0.6rem;
7789
+ color: var(--text-muted);
7790
+ }
7791
+
7792
+ .queue-header-label {
7793
+ flex: 1;
7794
+ opacity: 0.6;
7795
+ }
7796
+
7797
+ .queue-header-clear {
7798
+ border: none;
7799
+ background: transparent;
7800
+ color: var(--text-muted);
7801
+ cursor: pointer;
7802
+ font-size: 0.5625rem;
7803
+ padding: 1px 4px;
7804
+ border-radius: 3px;
7805
+ opacity: 0.5;
7806
+ transition: color 0.15s, background 0.15s, opacity 0.15s;
7807
+ }
7808
+
7809
+ .queue-header-clear:hover {
7810
+ color: var(--error, #e55);
7811
+ background: rgba(229, 85, 85, 0.12);
7812
+ opacity: 1;
7813
+ }
7814
+
7815
+ .queue-item {
7816
+ display: flex;
7817
+ align-items: center;
7818
+ gap: 6px;
7819
+ padding: 4px 8px;
7820
+ font-size: 0.6875rem;
7821
+ color: var(--text-muted);
7822
+ opacity: 0.7;
7823
+ transition: opacity 0.15s ease;
7824
+ }
7825
+
7826
+ .queue-item:hover {
7827
+ opacity: 1;
7828
+ }
7829
+
7830
+ .queue-item-dot {
7831
+ flex-shrink: 0;
7832
+ width: 4px;
7833
+ height: 4px;
7834
+ border-radius: 50%;
7835
+ background: rgba(var(--accent-rgb, 197, 101, 61), 0.6);
7836
+ animation: statusDotPulse 1.2s ease-in-out infinite;
7837
+ }
7838
+
7839
+ .queue-item-text {
7840
+ flex: 1;
7841
+ min-width: 0;
7842
+ overflow: hidden;
7843
+ text-overflow: ellipsis;
7844
+ white-space: nowrap;
7845
+ font-size: 0.6875rem;
7846
+ color: var(--text-muted);
7847
+ }
7848
+
7849
+ .queue-item-age {
7850
+ flex-shrink: 0;
7851
+ font-size: 0.5625rem;
7852
+ color: var(--text-muted);
7853
+ font-variant-numeric: tabular-nums;
7854
+ opacity: 0.5;
7855
+ }
7856
+
7857
+ .queue-item-send-now {
7858
+ flex-shrink: 0;
7859
+ border: 1px solid rgba(var(--accent-rgb, 197, 101, 61), 0.3);
7860
+ background: transparent;
7861
+ color: var(--accent, #c5653d);
7862
+ cursor: pointer;
7863
+ padding: 1px 8px;
7864
+ border-radius: 4px;
7865
+ font-size: 0.6rem;
7866
+ line-height: 1.4;
7867
+ transition: color 0.15s, background 0.15s, border-color 0.15s;
7868
+ }
7869
+
7870
+ .queue-item-send-now:hover {
7871
+ color: var(--accent, #c5653d);
7872
+ background: rgba(var(--accent-rgb, 197, 101, 61), 0.12);
7873
+ border-color: rgba(var(--accent-rgb, 197, 101, 61), 0.5);
7874
+ }
7875
+
7876
+ .queue-item-cancel {
7877
+ flex-shrink: 0;
7878
+ border: none;
7879
+ background: transparent;
7880
+ color: var(--text-muted);
7881
+ cursor: pointer;
7882
+ padding: 1px 4px;
7883
+ border-radius: 3px;
7884
+ font-size: 0.6875rem;
7885
+ line-height: 1;
7886
+ opacity: 0.4;
7887
+ transition: color 0.15s, background 0.15s, opacity 0.15s;
7888
+ }
7889
+
7890
+ .queue-item-cancel:hover {
7891
+ color: var(--error, #e55);
7892
+ background: rgba(229, 85, 85, 0.12);
7893
+ opacity: 1;
7894
+ }
7895
+
7740
7896
  /* 结束标记 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.5.7",
3
+ "version": "1.6.1",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {