@co0ontty/wand 1.4.0 → 1.5.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/dist/config.js CHANGED
@@ -15,7 +15,7 @@ export const defaultConfig = () => ({
15
15
  startupCommands: [],
16
16
  allowedCommandPrefixes: [],
17
17
  shortcutLogMaxBytes: 10 * 1024 * 1024,
18
- experimentalDomTerminal: false,
18
+ language: "",
19
19
  commandPresets: [
20
20
  {
21
21
  label: "Claude",
@@ -107,7 +107,8 @@ function mergeWithDefaults(input) {
107
107
  command: normalizePresetCommand(preset.command),
108
108
  mode: isExecutionMode(preset.mode) ? preset.mode : undefined
109
109
  }))
110
- : defaults.commandPresets
110
+ : defaults.commandPresets,
111
+ language: typeof input.language === "string" ? input.language.trim() : defaults.language,
111
112
  };
112
113
  }
113
114
  export function isExecutionMode(value) {
@@ -1559,6 +1559,13 @@ export class ProcessManager extends EventEmitter {
1559
1559
  const escaped = autonomousPrompt.replace(/'/g, "'\\''");
1560
1560
  result += ` --append-system-prompt '${escaped}'`;
1561
1561
  }
1562
+ // Append language preference if configured
1563
+ const language = this.config.language?.trim();
1564
+ if (language) {
1565
+ const langPrompt = `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`;
1566
+ const escaped = langPrompt.replace(/'/g, "'\\''");
1567
+ result += ` --append-system-prompt '${escaped}'`;
1568
+ }
1562
1569
  return result;
1563
1570
  }
1564
1571
  }
package/dist/server.js CHANGED
@@ -253,14 +253,14 @@ export async function startServer(config, configPath) {
253
253
  const configDir = resolveConfigDir(configPath);
254
254
  const avatarSeed = await ensureAvatarSeed(configDir);
255
255
  const processes = new ProcessManager(config, storage, configDir);
256
- const structuredSessions = new StructuredSessionManager(storage);
256
+ const structuredSessions = new StructuredSessionManager(storage, config);
257
257
  const useHttps = config.https === true;
258
258
  const protocol = useHttps ? "https" : "http";
259
259
  const nodeModulesDir = path.join(RUNTIME_ROOT_DIR, "node_modules");
260
260
  app.use(express.json({ limit: "1mb" }));
261
- app.use("/vendor/xterm", express.static(path.join(nodeModulesDir, "xterm")));
261
+ app.use("/vendor/xterm", express.static(path.join(nodeModulesDir, "@xterm", "xterm")));
262
262
  app.use("/vendor/xterm-addon-fit", express.static(path.join(nodeModulesDir, "@xterm", "addon-fit")));
263
- app.use("/vendor/xterm-addon-serialize", express.static(path.join(nodeModulesDir, "xterm-addon-serialize")));
263
+ app.use("/vendor/xterm-addon-serialize", express.static(path.join(nodeModulesDir, "@xterm", "addon-serialize")));
264
264
  // ── Web UI and PWA endpoints ──
265
265
  app.get("/", (_req, res) => {
266
266
  res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
@@ -336,7 +336,6 @@ export async function startServer(config, configPath) {
336
336
  defaultCwd: config.defaultCwd,
337
337
  commandPresets: config.commandPresets,
338
338
  structuredRunners: [{ label: "Claude Structured", runner: "claude-cli-print" }],
339
- experimentalDomTerminal: config.experimentalDomTerminal ?? false,
340
339
  updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
341
340
  latestVersion: cachedUpdateInfo?.latest ?? null,
342
341
  currentVersion: PKG_VERSION,
@@ -360,7 +359,7 @@ export async function startServer(config, configPath) {
360
359
  });
361
360
  app.post("/api/settings/config", async (req, res) => {
362
361
  const body = req.body;
363
- const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "experimentalDomTerminal"];
362
+ const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "language"];
364
363
  let changed = false;
365
364
  for (const field of allowedFields) {
366
365
  if (field in body && body[field] !== undefined) {
@@ -391,8 +390,8 @@ export async function startServer(config, configPath) {
391
390
  else if (field === "shell") {
392
391
  config.shell = String(body.shell);
393
392
  }
394
- else if (field === "experimentalDomTerminal") {
395
- config.experimentalDomTerminal = body.experimentalDomTerminal === true;
393
+ else if (field === "language") {
394
+ config.language = typeof body.language === "string" ? body.language.trim() : "";
396
395
  }
397
396
  changed = true;
398
397
  }
@@ -1,5 +1,5 @@
1
1
  import { WandStorage } from "./storage.js";
2
- import { ExecutionMode, SessionRunner, SessionSnapshot } from "./types.js";
2
+ import { ExecutionMode, SessionRunner, SessionSnapshot, WandConfig } from "./types.js";
3
3
  import { ProcessEvent } from "./ws-broadcast.js";
4
4
  interface CreateStructuredSessionOptions {
5
5
  cwd: string;
@@ -9,10 +9,11 @@ interface CreateStructuredSessionOptions {
9
9
  }
10
10
  export declare class StructuredSessionManager {
11
11
  private readonly storage;
12
+ private readonly config;
12
13
  private readonly sessions;
13
14
  private readonly pendingChildren;
14
15
  private emitEvent;
15
- constructor(storage: WandStorage);
16
+ constructor(storage: WandStorage, config: WandConfig);
16
17
  setEventEmitter(emitEvent: (event: ProcessEvent) => void): void;
17
18
  list(): SessionSnapshot[];
18
19
  get(id: string): SessionSnapshot | null;
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
- const STREAM_EMIT_DEBOUNCE_MS = 50;
3
+ const STREAM_EMIT_DEBOUNCE_MS = 16;
4
4
  function isRunningAsRoot() {
5
5
  return process.getuid?.() === 0 || process.geteuid?.() === 0;
6
6
  }
@@ -10,11 +10,13 @@ function shouldAutoApproveForMode(mode) {
10
10
  }
11
11
  export class StructuredSessionManager {
12
12
  storage;
13
+ config;
13
14
  sessions = new Map();
14
15
  pendingChildren = new Map();
15
16
  emitEvent = null;
16
- constructor(storage) {
17
+ constructor(storage, config) {
17
18
  this.storage = storage;
19
+ this.config = config;
18
20
  for (const snapshot of this.storage.loadSessions()) {
19
21
  if ((snapshot.sessionKind ?? "pty") !== "structured")
20
22
  continue;
@@ -369,6 +371,11 @@ export class StructuredSessionManager {
369
371
  if (session.mode === "managed") {
370
372
  args.push("--append-system-prompt", "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.");
371
373
  }
374
+ // Append language preference if configured
375
+ const language = this.config.language?.trim();
376
+ if (language) {
377
+ args.push("--append-system-prompt", `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`);
378
+ }
372
379
  if (session.claudeSessionId) {
373
380
  args.push("--resume", session.claudeSessionId);
374
381
  }
@@ -456,9 +463,9 @@ export class StructuredSessionManager {
456
463
  if (extracted.content.length > 0) {
457
464
  turnState.blocks.push(...extracted.content);
458
465
  }
459
- if (extracted.usage) {
460
- turnState.usage = extracted.usage;
461
- }
466
+ // NOTE: usage from streaming "assistant" events contains partial/incremental
467
+ // token counts (e.g. output_tokens=1 during streaming) and is NOT accurate.
468
+ // We only use the authoritative usage from the final "result" event.
462
469
  syncSnapshot();
463
470
  scheduleEmit();
464
471
  return;
package/dist/types.d.ts CHANGED
@@ -45,8 +45,8 @@ export interface WandConfig {
45
45
  commandPresets: CommandPreset[];
46
46
  /** Max total size (bytes) for shortcut interaction logs per session (default: 10 MB). Set 0 to disable logging. */
47
47
  shortcutLogMaxBytes?: number;
48
- /** Experimental: use DOM-based terminal rendering on mobile for native text selection (default: false) */
49
- experimentalDomTerminal?: boolean;
48
+ /** Preferred response language for Claude (e.g. "中文", "English"). Empty string means no override. */
49
+ language?: string;
50
50
  }
51
51
  export interface CommandRequest {
52
52
  command: string;
@@ -821,11 +821,21 @@
821
821
  '<label class="field-label" for="cfg-shell">Shell</label>' +
822
822
  '<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
823
823
  '</div>' +
824
- '<div class="field field-inline">' +
825
- '<label class="field-label" for="cfg-dom-terminal">终端 DOM 渲染 <span style="font-size:0.7em;color:var(--warning);font-weight:600;">实验性</span></label>' +
826
- '<input id="cfg-dom-terminal" type="checkbox" class="field-checkbox" />' +
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>' +
827
838
  '</div>' +
828
- '<p class="hint" style="margin-top:-8px;margin-bottom:8px;">移动端使用 DOM 渲染终端,支持原生文本选择与复制。保存后刷新页面生效。</p>' +
829
839
  '<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
830
840
  '<p id="config-message" class="hint hidden"></p>' +
831
841
  '</div>' +
@@ -1916,21 +1926,23 @@
1916
1926
  var statusSpan = el.querySelector(".inline-tool-status");
1917
1927
  if (statusSpan) {
1918
1928
  if (el.dataset.status === "error") {
1919
- statusSpan.textContent = "⚠️";
1929
+ statusSpan.textContent = "";
1920
1930
  } else if (el.dataset.status === "done") {
1921
- statusSpan.textContent = expanded ? "" : "✅";
1931
+ statusSpan.textContent = "";
1922
1932
  }
1923
1933
  }
1924
1934
  };
1925
1935
  // Toggle function for terminal tool blocks
1926
1936
  window.__terminalExpand = function(el) {
1927
- var body = el.querySelector(".term-body");
1937
+ var container = el.closest(".inline-terminal");
1938
+ if (!container) return;
1939
+ var body = container.querySelector(".term-body");
1928
1940
  if (body) {
1929
1941
  var isHidden = body.style.display === "none";
1930
1942
  body.style.display = isHidden ? "block" : "none";
1931
- el.dataset.expanded = isHidden ? "true" : "false";
1943
+ container.dataset.expanded = isHidden ? "true" : "false";
1932
1944
  var toggleIcon = el.querySelector(".term-toggle-icon");
1933
- if (toggleIcon) toggleIcon.textContent = isHidden ? "▼" : "";
1945
+ if (toggleIcon) toggleIcon.textContent = isHidden ? "▼" : "";
1934
1946
  }
1935
1947
  };
1936
1948
  // Update streaming thinking content (called from WebSocket handler)
@@ -4196,8 +4208,8 @@
4196
4208
  if (modeEl) modeEl.value = cfg.defaultMode || "default";
4197
4209
  if (cwdEl) cwdEl.value = cfg.defaultCwd || "";
4198
4210
  if (shellEl) shellEl.value = cfg.shell || "";
4199
- var domTermEl = document.getElementById("cfg-dom-terminal");
4200
- if (domTermEl) domTermEl.checked = cfg.experimentalDomTerminal === true;
4211
+ var langEl = document.getElementById("cfg-language");
4212
+ if (langEl) langEl.value = cfg.language || "";
4201
4213
 
4202
4214
  // Cert status
4203
4215
  var certStatus = document.getElementById("cert-status");
@@ -4235,7 +4247,7 @@
4235
4247
  defaultMode: (document.getElementById("cfg-mode") || {}).value,
4236
4248
  defaultCwd: (document.getElementById("cfg-cwd") || {}).value,
4237
4249
  shell: (document.getElementById("cfg-shell") || {}).value,
4238
- experimentalDomTerminal: (document.getElementById("cfg-dom-terminal") || {}).checked,
4250
+ language: (document.getElementById("cfg-language") || {}).value || "",
4239
4251
  };
4240
4252
 
4241
4253
  fetch("/api/settings/config", {
@@ -7357,7 +7369,8 @@
7357
7369
  switch (msg.type) {
7358
7370
  case 'output':
7359
7371
  // Update session output (for terminal display and local message parsing)
7360
- if (msg.data && msg.data.output && msg.sessionId) {
7372
+ // NOTE: For structured sessions, output may be "" during streaming — check messages too
7373
+ if (msg.data && (msg.data.output || msg.data.messages) && msg.sessionId) {
7361
7374
  var snapshot = { id: msg.sessionId, output: msg.data.output };
7362
7375
  if (Object.prototype.hasOwnProperty.call(msg.data, 'permissionBlocked')) {
7363
7376
  snapshot.permissionBlocked = !!msg.data.permissionBlocked;
@@ -7381,7 +7394,12 @@
7381
7394
  }
7382
7395
  }
7383
7396
  updateTaskDisplay();
7384
- scheduleChatRender();
7397
+ // Structured sessions: render immediately for responsiveness
7398
+ if (msg.data.sessionKind === 'structured') {
7399
+ renderChat();
7400
+ } else {
7401
+ scheduleChatRender();
7402
+ }
7385
7403
  }
7386
7404
 
7387
7405
  }
@@ -8446,10 +8464,8 @@
8446
8464
  // ===== Terminal copy button for mobile =====
8447
8465
  // ===== Mobile DOM terminal view =====
8448
8466
  function initMobileDomTerminal(container) {
8449
- var isTouch = window.matchMedia("(pointer: coarse)").matches;
8450
- if (!isTouch) return;
8451
- // Gated by experimental config flag
8452
- if (!state.config || !state.config.experimentalDomTerminal) return;
8467
+ // DOM terminal feature removed — always return
8468
+ return;
8453
8469
 
8454
8470
  // Create DOM view container
8455
8471
  var domView = document.createElement("div");
@@ -8848,7 +8864,7 @@
8848
8864
  if (msg.role === "thinking") {
8849
8865
  return '<div class="chat-message thinking">' +
8850
8866
  '<div class="thinking-inline thinking-pty collapsed" data-thinking="" onclick="__thinkingToggle(this)">' +
8851
- '<span class="thinking-inline-icon">🧠</span>' +
8867
+ '<span class="thinking-inline-icon">⦿</span>' +
8852
8868
  '<span class="thinking-inline-preview">' + escapeHtml(msg.content) + '</span>' +
8853
8869
  '<span class="thinking-inline-action">展开</span>' +
8854
8870
  '</div>' +
@@ -8859,7 +8875,7 @@
8859
8875
  if (msg.role === "prompt") {
8860
8876
  return '<div class="chat-message prompt">' +
8861
8877
  '<div class="prompt-card">' +
8862
- '<div class="prompt-icon">💡</div>' +
8878
+ '<div class="prompt-icon">→</div>' +
8863
8879
  '<div class="prompt-content">试试:<span class="prompt-text">' + escapeHtml(msg.content) + '</span></div>' +
8864
8880
  '</div>' +
8865
8881
  '</div>';
@@ -8871,7 +8887,7 @@
8871
8887
  }
8872
8888
 
8873
8889
  // Legacy string content (from PTY parsing)
8874
- var avatar = msg.role === "assistant" ? '<div class="chat-message-avatar">AI</div>' : "";
8890
+ var avatar = msg.role === "assistant" ? '<div class="chat-message-avatar">赛博虎妞</div>' : "";
8875
8891
  var bubbleContent = msg.role === "assistant" ? renderMarkdown(msg.content) : escapeHtml(msg.content);
8876
8892
  return '<div class="chat-message ' + msg.role + '">' +
8877
8893
  avatar +
@@ -8924,7 +8940,7 @@
8924
8940
 
8925
8941
  function renderStructuredMessage(msg) {
8926
8942
  var role = msg.role;
8927
- var avatar = role === "assistant" ? '<div class="chat-message-avatar">AI</div>' : "";
8943
+ var avatar = role === "assistant" ? '<div class="chat-message-avatar">赛博虎妞</div>' : "";
8928
8944
 
8929
8945
  if (!msg.content || msg.content.length === 0) {
8930
8946
  if (role === "assistant") {
@@ -8951,7 +8967,7 @@
8951
8967
  } catch (e) {
8952
8968
  return '<div class="chat-message ' + role + '">' +
8953
8969
  avatar +
8954
- '<div class="chat-message-bubble"><div class="render-error">消息渲染失败</div></div>' +
8970
+ '<div class="chat-message-content"><div class="render-error">消息渲染失败</div></div>' +
8955
8971
  '</div>';
8956
8972
  }
8957
8973
 
@@ -8970,7 +8986,7 @@
8970
8986
 
8971
8987
  return '<div class="chat-message ' + role + '">' +
8972
8988
  avatar +
8973
- '<div class="chat-message-bubble">' + blocksHtml + '</div>' +
8989
+ '<div class="chat-message-content">' + blocksHtml + '</div>' +
8974
8990
  usageHtml +
8975
8991
  '</div>';
8976
8992
  }
@@ -8991,13 +9007,13 @@
8991
9007
  if (isStreaming) {
8992
9008
  return '<div class="thinking-inline thinking-streaming" data-thinking="">' +
8993
9009
  '<div class="thinking-streaming-inner">' +
8994
- '<span class="thinking-streaming-icon spinning">🧠</span>' +
9010
+ '<span class="thinking-streaming-icon spinning">⦿</span>' +
8995
9011
  '<div class="thinking-streaming-text"></div>' +
8996
9012
  '</div>' +
8997
9013
  '</div>';
8998
9014
  }
8999
9015
  return '<div class="thinking-inline collapsed" data-thinking="' + escapeHtml(thinkingText) + '" onclick="__thinkingToggle(this)">' +
9000
- '<span class="thinking-inline-icon">🧠</span>' +
9016
+ '<span class="thinking-inline-icon">⦿</span>' +
9001
9017
  '<span class="thinking-inline-preview">' + escapeHtml(preview) + '</span>' +
9002
9018
  '<span class="thinking-inline-action">展开</span>' +
9003
9019
  '</div>';
@@ -9025,7 +9041,7 @@
9025
9041
 
9026
9042
  var isError = toolResult && toolResult.is_error;
9027
9043
  var hasResult = resultContent.length > 0;
9028
- var statusIcon = isError ? "⚠️" : (hasResult ? "" : "");
9044
+ var statusIcon = isError ? "" : (hasResult ? "" : "");
9029
9045
 
9030
9046
  // Build the inline preview line
9031
9047
  var icon = "";
@@ -9090,7 +9106,7 @@
9090
9106
  var fullResult = resultContent;
9091
9107
 
9092
9108
  var expandedHtml = "";
9093
- var shouldExpand = hasResult;
9109
+ var shouldExpand = false; // All inline tools collapsed by default
9094
9110
  if (hasResult) {
9095
9111
  expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';">' +
9096
9112
  '<div class="inline-tool-result">' + formatInlineResult(resultContent, toolName) + '</div>' +
@@ -9104,7 +9120,7 @@
9104
9120
 
9105
9121
  var extraInfoHtml = meta ? '<span class="inline-tool-meta">' + escapeHtml(meta) + '</span>' : '';
9106
9122
  var extraClass = isError ? 'inline-tool-error-inline' : '';
9107
- if (hasResult) extraClass += ' inline-tool-open';
9123
+ if (shouldExpand) extraClass += ' inline-tool-open';
9108
9124
 
9109
9125
  return '<div class="inline-tool ' + extraClass + '" ' +
9110
9126
  'data-result="' + escapeHtml(fullResult) + '" ' +
@@ -9161,13 +9177,16 @@
9161
9177
  exitCodeHtml = '<div class="term-exit ' + codeClass + '">exit ' + exitCode + '</div>';
9162
9178
  }
9163
9179
 
9164
- return '<div class="inline-terminal" data-expanded="true">' +
9180
+ // Show command preview in header (truncate long commands)
9181
+ var cmdPreview = command.length > 80 ? command.slice(0, 77) + "…" : command;
9182
+
9183
+ return '<div class="inline-terminal" data-expanded="false">' +
9165
9184
  '<div class="term-header" onclick="__terminalExpand(this)">' +
9166
9185
  statusDot +
9167
- '<span class="term-title">执行命令</span>' +
9168
- '<span class="term-toggle-icon">▼</span>' +
9186
+ '<span class="term-cmd-preview"><span class="term-prompt">$</span> ' + escapeHtml(cmdPreview) + '</span>' +
9187
+ '<span class="term-toggle-icon">▶</span>' +
9169
9188
  '</div>' +
9170
- '<div class="term-body">' +
9189
+ '<div class="term-body" style="display:none;">' +
9171
9190
  '<div class="term-command"><span class="term-prompt">$</span> ' + cmdDisplay + '</div>' +
9172
9191
  (outputHtml ? '<div class="term-output">' + outputHtml + '</div>' : '') +
9173
9192
  exitCodeHtml +
@@ -9230,15 +9249,15 @@
9230
9249
  if (isError) {
9231
9250
  statusClass = "diff-error";
9232
9251
  statusText = toolResultText.indexOf("haven't granted") !== -1 || toolResultText.indexOf("permission") !== -1
9233
- ? "等待授权"
9234
- : "❌ 修改失败";
9252
+ ? "等待授权"
9253
+ : "失败";
9235
9254
  } else {
9236
9255
  statusClass = "diff-success";
9237
- statusText = "已修改";
9256
+ statusText = "已修改";
9238
9257
  }
9239
9258
  } else {
9240
9259
  statusClass = "diff-pending";
9241
- statusText = "⏳ 执行中…";
9260
+ statusText = "执行中";
9242
9261
  }
9243
9262
 
9244
9263
  // If only one column has content, show full width
@@ -9247,7 +9266,7 @@
9247
9266
 
9248
9267
  return '<div class="inline-diff" data-tool-name="' + escapeHtml(toolName) + '">' +
9249
9268
  '<div class="diff-header">' +
9250
- '<span class="diff-file-icon">📄</span>' +
9269
+ '<span class="diff-file-icon"></span>' +
9251
9270
  '<span class="diff-file-name">' + escapeHtml(fileName) + '</span>' +
9252
9271
  '<span class="diff-path">' + escapeHtml(path) + '</span>' +
9253
9272
  '<span class="diff-status ' + statusClass + '">' + statusText + '</span>' +
@@ -9306,7 +9325,7 @@
9306
9325
  }
9307
9326
  return '<div class="tool-use-card ask-user" data-tool-use-id="' + escapeHtml(toolId) + '">' +
9308
9327
  '<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
9309
- '<span class="tool-use-icon">❓</span>' +
9328
+ '<span class="tool-use-icon">?</span>' +
9310
9329
  '<span class="tool-use-name">提问</span>' +
9311
9330
  '</div>' +
9312
9331
  '<div class="tool-use-body ask-user-body">' +
@@ -9397,23 +9416,23 @@
9397
9416
 
9398
9417
  function getToolIcon(toolName) {
9399
9418
  var icons = {
9400
- "Read": "📄",
9401
- "Write": "✏️",
9402
- "Edit": "📝",
9403
- "MultiEdit": "📝",
9404
- "Bash": "💻",
9405
- "Grep": "🔍",
9406
- "Glob": "📂",
9407
- "WebFetch": "🌐",
9408
- "WebSearch": "🔎",
9409
- "Task": "📋",
9410
- "TodoWrite": "📝",
9411
- "TodoRead": "📋",
9412
- "NotebookEdit": "📓",
9413
- "Agent": "🤖",
9414
- "Exit": "🚪"
9419
+ "Read": "R",
9420
+ "Write": "W",
9421
+ "Edit": "E",
9422
+ "MultiEdit": "E",
9423
+ "Bash": "$",
9424
+ "Grep": "G",
9425
+ "Glob": "F",
9426
+ "WebFetch": "",
9427
+ "WebSearch": "",
9428
+ "Task": "T",
9429
+ "TodoWrite": "",
9430
+ "TodoRead": "",
9431
+ "NotebookEdit": "N",
9432
+ "Agent": "A",
9433
+ "Exit": "×"
9415
9434
  };
9416
- return icons[toolName] || "🔧";
9435
+ return icons[toolName] || "·";
9417
9436
  }
9418
9437
 
9419
9438
  function generateInputSummary(toolName, input) {
@@ -2231,6 +2231,7 @@
2231
2231
 
2232
2232
  .chat-message.assistant {
2233
2233
  align-self: flex-start;
2234
+ max-width: 95%;
2234
2235
  }
2235
2236
 
2236
2237
  .chat-message.system-info {
@@ -2296,9 +2297,11 @@
2296
2297
  }
2297
2298
 
2298
2299
  .chat-message.assistant .chat-message-avatar {
2299
- background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
2300
- color: var(--text-inverse);
2301
- box-shadow: 0 2px 8px rgba(197, 101, 61, 0.3);
2300
+ font-size: 0.75rem;
2301
+ font-weight: 600;
2302
+ color: var(--accent);
2303
+ padding: 0 2px 4px 2px;
2304
+ letter-spacing: 0.03em;
2302
2305
  }
2303
2306
 
2304
2307
  /* ===== 消息气泡 ===== */
@@ -2351,23 +2354,44 @@
2351
2354
  box-shadow: var(--shadow-md);
2352
2355
  }
2353
2356
 
2357
+ /* ===== 结构化消息内容(无气泡包裹) ===== */
2358
+ .chat-message-content {
2359
+ display: flex;
2360
+ flex-direction: column;
2361
+ gap: 2px;
2362
+ max-width: 100%;
2363
+ font-size: 0.875rem;
2364
+ line-height: var(--line-height-relaxed);
2365
+ word-wrap: break-word;
2366
+ color: var(--text-primary);
2367
+ }
2368
+
2369
+ .chat-message-content > .thinking-inline,
2370
+ .chat-message-content > .inline-tool,
2371
+ .chat-message-content > .terminal-tool,
2372
+ .chat-message-content > .diff-tool,
2373
+ .chat-message-content > .tool-use-card {
2374
+ margin: 1px 0;
2375
+ }
2376
+
2354
2377
  /* ===== 消息使用量信息 ===== */
2355
2378
  .message-usage {
2356
- margin-top: 12px;
2357
- padding-top: 10px;
2358
- border-top: 1px dashed var(--border-subtle);
2379
+ margin-top: 8px;
2380
+ padding-top: 6px;
2381
+ border-top: 1px solid var(--border-subtle);
2359
2382
  font-family: var(--font-mono);
2360
- font-size: 0.6875rem;
2361
- color: var(--text-tertiary);
2383
+ font-size: 0.625rem;
2384
+ color: var(--text-muted);
2362
2385
  display: flex;
2363
- gap: 12px;
2386
+ gap: 10px;
2364
2387
  align-items: center;
2365
2388
  flex-wrap: wrap;
2389
+ opacity: 0.5;
2366
2390
  }
2367
2391
 
2368
2392
  .message-usage::before {
2369
- content: '';
2370
- color: var(--accent-soft);
2393
+ content: '·';
2394
+ color: var(--text-muted);
2371
2395
  font-size: 0.625rem;
2372
2396
  }
2373
2397
 
@@ -2488,9 +2512,9 @@
2488
2512
 
2489
2513
  /* Tool Use Card */
2490
2514
  .tool-use-card {
2491
- margin: 6px 0;
2515
+ margin: 3px 0;
2492
2516
  border: 1px solid var(--border-subtle);
2493
- border-radius: var(--radius-sm);
2517
+ border-radius: 8px;
2494
2518
  overflow: hidden;
2495
2519
  width: 100%;
2496
2520
  box-sizing: border-box;
@@ -2498,9 +2522,8 @@
2498
2522
  transition: all 0.3s var(--ease-out-expo);
2499
2523
  }
2500
2524
  .tool-use-card:hover {
2501
- border-color: rgba(79, 122, 88, 0.3);
2502
- transform: translateX(4px);
2503
- box-shadow: -4px 4px 16px rgba(79, 122, 88, 0.08);
2525
+ border-color: rgba(79, 122, 88, 0.25);
2526
+ box-shadow: 0 2px 8px rgba(79, 122, 88, 0.06);
2504
2527
  }
2505
2528
  .tool-use-card.loading {
2506
2529
  border-color: rgba(79, 122, 88, 0.4);
@@ -2515,8 +2538,8 @@
2515
2538
  display: flex;
2516
2539
  align-items: center;
2517
2540
  gap: 8px;
2518
- padding: 10px 12px;
2519
- background: rgba(79, 122, 88, 0.06);
2541
+ padding: 8px 12px;
2542
+ background: rgba(79, 122, 88, 0.04);
2520
2543
  cursor: pointer;
2521
2544
  font-size: 0.8125rem;
2522
2545
  user-select: none;
@@ -2524,7 +2547,7 @@
2524
2547
  -webkit-tap-highlight-color: transparent;
2525
2548
  touch-action: manipulation;
2526
2549
  transition: all 0.2s var(--ease-out-expo);
2527
- min-height: 44px;
2550
+ min-height: 36px;
2528
2551
  flex-wrap: nowrap;
2529
2552
  }
2530
2553
  .tool-use-header:hover {
@@ -2543,12 +2566,18 @@
2543
2566
  background: rgba(178, 79, 69, 0.1);
2544
2567
  }
2545
2568
  .tool-use-icon {
2546
- font-size: 1.1rem;
2569
+ font-size: 0.6875rem;
2570
+ font-family: var(--font-mono);
2571
+ font-weight: 600;
2547
2572
  flex-shrink: 0;
2548
2573
  display: flex;
2549
2574
  align-items: center;
2550
2575
  justify-content: center;
2551
- min-width: 20px;
2576
+ min-width: 18px;
2577
+ height: 18px;
2578
+ border-radius: 4px;
2579
+ background: rgba(79, 122, 88, 0.1);
2580
+ color: var(--success);
2552
2581
  }
2553
2582
  .tool-use-spinner {
2554
2583
  display: inline-block;
@@ -2745,22 +2774,22 @@
2745
2774
  .thinking-inline {
2746
2775
  display: flex;
2747
2776
  align-items: baseline;
2748
- gap: 6px;
2749
- margin: 8px 0;
2750
- padding: 6px 10px;
2751
- border-radius: var(--radius-sm);
2752
- background: rgba(138, 108, 178, 0.05);
2777
+ gap: 5px;
2778
+ margin: 2px 0;
2779
+ padding: 3px 8px;
2780
+ border-radius: 6px;
2781
+ background: rgba(138, 108, 178, 0.04);
2753
2782
  cursor: pointer;
2754
- font-size: 0.8125rem;
2755
- line-height: 1.5;
2756
- color: rgba(138, 108, 178, 0.7);
2783
+ font-size: 0.75rem;
2784
+ line-height: 1.4;
2785
+ color: rgba(138, 108, 178, 0.55);
2757
2786
  transition: background var(--transition-fast), color var(--transition-fast);
2758
2787
  word-break: break-word;
2759
2788
  overflow-wrap: break-word;
2760
2789
  }
2761
2790
  .thinking-inline:hover {
2762
- background: rgba(138, 108, 178, 0.1);
2763
- color: rgba(138, 108, 178, 0.9);
2791
+ background: rgba(138, 108, 178, 0.08);
2792
+ color: rgba(138, 108, 178, 0.8);
2764
2793
  }
2765
2794
  .thinking-inline.expanded {
2766
2795
  color: var(--text-secondary);
@@ -2771,7 +2800,7 @@
2771
2800
  }
2772
2801
  .thinking-inline-icon {
2773
2802
  flex-shrink: 0;
2774
- font-size: 0.875rem;
2803
+ font-size: 0.625rem;
2775
2804
  }
2776
2805
  .thinking-inline-preview {
2777
2806
  flex: 1;
@@ -2786,42 +2815,42 @@
2786
2815
  }
2787
2816
  .thinking-inline-action {
2788
2817
  flex-shrink: 0;
2789
- font-size: 0.75rem;
2790
- color: rgba(138, 108, 178, 0.5);
2791
- padding: 1px 6px;
2818
+ font-size: 0.625rem;
2819
+ color: rgba(138, 108, 178, 0.4);
2820
+ padding: 0 4px;
2792
2821
  border-radius: 3px;
2793
- background: rgba(138, 108, 178, 0.1);
2822
+ background: rgba(138, 108, 178, 0.08);
2794
2823
  margin-left: auto;
2795
2824
  }
2796
2825
  .thinking-inline:hover .thinking-inline-action {
2797
- color: rgba(138, 108, 178, 0.8);
2826
+ color: rgba(138, 108, 178, 0.7);
2798
2827
  }
2799
2828
  .thinking-inline.thinking-pty {
2800
- background: rgba(138, 108, 178, 0.05);
2829
+ background: rgba(138, 108, 178, 0.03);
2801
2830
  }
2802
2831
 
2803
2832
  /* Streaming thinking: 3-line scrollable area during thinking */
2804
2833
  .thinking-streaming {
2805
2834
  display: flex;
2806
2835
  align-items: flex-start;
2807
- gap: 6px;
2808
- margin: 8px 0;
2809
- padding: 6px 10px;
2810
- border-radius: var(--radius-sm);
2811
- background: rgba(138, 108, 178, 0.06);
2836
+ gap: 5px;
2837
+ margin: 2px 0;
2838
+ padding: 3px 8px;
2839
+ border-radius: 6px;
2840
+ background: rgba(138, 108, 178, 0.04);
2812
2841
  cursor: default;
2813
- font-size: 0.8125rem;
2814
- line-height: 1.5;
2815
- color: rgba(138, 108, 178, 0.7);
2842
+ font-size: 0.75rem;
2843
+ line-height: 1.4;
2844
+ color: rgba(138, 108, 178, 0.55);
2816
2845
  }
2817
2846
  .thinking-streaming-inner {
2818
2847
  display: flex;
2819
2848
  align-items: flex-start;
2820
- gap: 6px;
2849
+ gap: 5px;
2821
2850
  width: 100%;
2822
2851
  }
2823
2852
  .thinking-streaming-icon {
2824
- font-size: 0.875rem;
2853
+ font-size: 0.625rem;
2825
2854
  flex-shrink: 0;
2826
2855
  margin-top: 1px;
2827
2856
  }
@@ -2851,33 +2880,38 @@
2851
2880
  .inline-tool {
2852
2881
  display: flex;
2853
2882
  flex-direction: column;
2854
- margin: 4px 0;
2855
- border-radius: var(--radius-sm);
2883
+ margin: 1px 0;
2884
+ border-radius: 6px;
2856
2885
  cursor: pointer;
2857
2886
  transition: background var(--transition-fast);
2858
2887
  }
2859
2888
  .inline-tool-row {
2860
2889
  display: flex;
2861
2890
  align-items: center;
2862
- gap: 6px;
2863
- padding: 4px 8px;
2864
- font-size: 0.8125rem;
2891
+ gap: 5px;
2892
+ padding: 2px 6px;
2893
+ font-size: 0.6875rem;
2865
2894
  color: var(--text-muted);
2866
2895
  font-family: var(--font-mono);
2867
- line-height: 1.5;
2896
+ line-height: 1.4;
2897
+ opacity: 0.7;
2868
2898
  }
2869
2899
  .inline-tool:hover .inline-tool-row {
2870
- background: rgba(0, 0, 0, 0.03);
2871
- border-radius: var(--radius-sm);
2900
+ opacity: 1;
2901
+ background: rgba(0, 0, 0, 0.02);
2902
+ border-radius: 6px;
2872
2903
  }
2873
2904
  .inline-tool-status {
2874
- font-size: 0.75rem;
2905
+ font-size: 0.625rem;
2875
2906
  flex-shrink: 0;
2876
2907
  }
2877
2908
  .inline-tool-icon {
2878
2909
  flex-shrink: 0;
2879
2910
  color: var(--text-muted);
2880
- margin: 0 2px;
2911
+ margin: 0 1px;
2912
+ opacity: 0.6;
2913
+ width: 12px;
2914
+ height: 12px;
2881
2915
  }
2882
2916
  .inline-tool-title {
2883
2917
  flex: 1;
@@ -2885,13 +2919,13 @@
2885
2919
  overflow: hidden;
2886
2920
  text-overflow: ellipsis;
2887
2921
  white-space: nowrap;
2888
- color: var(--text-secondary);
2922
+ color: var(--text-muted);
2889
2923
  }
2890
2924
  .inline-tool-meta {
2891
2925
  flex-shrink: 0;
2892
- font-size: 0.7rem;
2926
+ font-size: 0.625rem;
2893
2927
  color: var(--text-muted);
2894
- opacity: 0.7;
2928
+ opacity: 0.5;
2895
2929
  max-width: 200px;
2896
2930
  overflow: hidden;
2897
2931
  text-overflow: ellipsis;
@@ -2958,9 +2992,9 @@
2958
2992
 
2959
2993
  /* ── Inline Terminal Display (Bash) ── */
2960
2994
  .inline-terminal {
2961
- margin: 6px 0;
2995
+ margin: 3px 0;
2962
2996
  border: 1px solid rgba(255, 255, 255, 0.06);
2963
- border-radius: var(--radius-sm);
2997
+ border-radius: 8px;
2964
2998
  background: #1a1714;
2965
2999
  overflow: hidden;
2966
3000
  }
@@ -2968,12 +3002,15 @@
2968
3002
  display: flex;
2969
3003
  align-items: center;
2970
3004
  gap: 6px;
2971
- padding: 6px 10px;
3005
+ padding: 5px 10px;
2972
3006
  background: rgba(255, 255, 255, 0.03);
2973
3007
  font-size: 0.75rem;
2974
3008
  cursor: pointer;
2975
3009
  user-select: none;
2976
3010
  }
3011
+ .term-header:hover {
3012
+ background: rgba(255, 255, 255, 0.05);
3013
+ }
2977
3014
  .term-status-dot {
2978
3015
  width: 6px;
2979
3016
  height: 6px;
@@ -2988,6 +3025,22 @@
2988
3025
  0%, 100% { opacity: 1; }
2989
3026
  50% { opacity: 0.4; }
2990
3027
  }
3028
+ .term-cmd-preview {
3029
+ flex: 1;
3030
+ min-width: 0;
3031
+ font-family: var(--font-mono);
3032
+ font-size: 0.6875rem;
3033
+ color: rgba(255, 255, 255, 0.45);
3034
+ overflow: hidden;
3035
+ text-overflow: ellipsis;
3036
+ white-space: nowrap;
3037
+ }
3038
+ .term-cmd-preview .term-prompt {
3039
+ color: rgba(110, 224, 154, 0.6);
3040
+ font-weight: 500;
3041
+ margin-right: 4px;
3042
+ font-size: 0.6875rem;
3043
+ }
2991
3044
  .term-title {
2992
3045
  flex: 1;
2993
3046
  font-family: var(--font-mono);
@@ -2995,8 +3048,8 @@
2995
3048
  color: rgba(255, 255, 255, 0.3);
2996
3049
  }
2997
3050
  .term-toggle-icon {
2998
- font-size: 0.625rem;
2999
- color: rgba(255, 255, 255, 0.3);
3051
+ font-size: 0.5rem;
3052
+ color: rgba(255, 255, 255, 0.25);
3000
3053
  transition: transform var(--transition-fast);
3001
3054
  }
3002
3055
  .term-body {
@@ -3040,9 +3093,9 @@
3040
3093
 
3041
3094
  /* ── Inline Diff Display (Edit, Write, MultiEdit) ── */
3042
3095
  .inline-diff {
3043
- margin: 6px 0;
3044
- border: 1px solid rgba(0, 0, 0, 0.08);
3045
- border-radius: var(--radius-sm);
3096
+ margin: 3px 0;
3097
+ border: 1px solid rgba(0, 0, 0, 0.06);
3098
+ border-radius: 8px;
3046
3099
  overflow: hidden;
3047
3100
  background: var(--bg-secondary);
3048
3101
  }
@@ -3055,8 +3108,7 @@
3055
3108
  font-size: 0.75rem;
3056
3109
  }
3057
3110
  .diff-file-icon {
3058
- flex-shrink: 0;
3059
- font-size: 0.875rem;
3111
+ display: none;
3060
3112
  }
3061
3113
  .diff-file-name {
3062
3114
  font-family: var(--font-mono);
@@ -5480,7 +5532,7 @@
5480
5532
  .btn-sm { min-height: 32px; }
5481
5533
 
5482
5534
  .chat-message-bubble { padding: 8px 10px; font-size: 0.75rem; }
5483
- .chat-message-avatar { width: 24px; height: 24px; font-size: 11px; }
5535
+ .chat-message-avatar { font-size: 0.6875rem; }
5484
5536
 
5485
5537
  /* 模态框移动端优化 */
5486
5538
  .modal-backdrop {
@@ -37,7 +37,7 @@ ${cssStyles}
37
37
  <div id="app"></div>
38
38
  ${scriptOpen} src="/vendor/xterm/lib/xterm.js">${scriptClose}
39
39
  ${scriptOpen} src="/vendor/xterm-addon-fit/lib/addon-fit.js">${scriptClose}
40
- ${scriptOpen} src="/vendor/xterm-addon-serialize/lib/xterm-addon-serialize.js">${scriptClose}
40
+ ${scriptOpen} src="/vendor/xterm-addon-serialize/lib/addon-serialize.js">${scriptClose}
41
41
  ${scriptOpen}>
42
42
  ${scriptContent}
43
43
  ${scriptClose}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,12 +35,12 @@
35
35
  "dependencies": {
36
36
  "@types/cookie": "^0.6.0",
37
37
  "@xterm/addon-fit": "^0.11.0",
38
+ "@xterm/addon-serialize": "^0.14.0",
39
+ "@xterm/xterm": "^5.5.0",
38
40
  "express": "^4.21.2",
39
41
  "node-pty": "^1.1.0",
40
42
  "puppeteer": "^24.40.0",
41
- "ws": "^8.19.0",
42
- "xterm": "^5.3.0",
43
- "xterm-addon-serialize": "^0.11.0"
43
+ "ws": "^8.19.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/express": "^4.17.21",