@co0ontty/wand 4.80.1 → 4.81.0-beta.g94cd760

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,6 +1,6 @@
1
1
  {
2
- "commit": "1fbbefb6858936b5ae6af8fc19a86c66ad2a997c",
3
- "builtAt": "2026-09-30T13:28:55.310Z",
4
- "version": "4.80.1",
5
- "channel": "stable"
2
+ "commit": "94cd7604eb5f5ffc01687755a23e337888cee038",
3
+ "builtAt": "2026-09-30T22:00:20.244Z",
4
+ "version": "4.81.0-beta.g94cd760",
5
+ "channel": "beta"
6
6
  }
@@ -72,10 +72,6 @@ export declare function recordIterationPromptForTask(storage: WandStorage, input
72
72
  * 已被上一轮提交用掉的条目会被标出来,方便模型少写重复内容。
73
73
  */
74
74
  export declare function iterationPromptDigest(entries: readonly WandIterationPrompt[]): string;
75
- /** 迭代为空时给模型的一句话,避免它把「没有输入」理解成「没有改动」。 */
76
- export declare const EMPTY_ITERATION_DIGEST_HINT = "\uFF08\u672C\u8FED\u4EE3\u8FD8\u6CA1\u6709\u8BB0\u5F55\u5230\u63D0\u793A\u8BCD\uFF0C\u6539\u4E3A\u4F9D\u636E\u6587\u4EF6\u6539\u52A8\u5224\u65AD\u3002\uFF09";
77
- /** 默认迭代的名字;UI 与文档共用,避免各处硬编码。 */
78
- export declare const DEFAULT_ITERATION_LABEL = "\u9ED8\u8BA4\u8FED\u4EE3";
79
75
  /** 面板里的一行:就是一条提示词记录 + 是否已被上一轮提交用掉。 */
80
76
  export interface IterationCommitEntry {
81
77
  id: string;
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import { getErrorMessage } from "./error-utils.js";
10
10
  import { runGitAsync } from "./git-utils.js";
11
11
  import { shouldGenerateSessionTopicFromInput, summarizeSessionTitleFromInput } from "./session-topic.js";
12
- import { DEFAULT_ITERATION_NAME, WAND_ITERATION_PROMPT_DETAIL_MAX_LENGTH, WAND_ITERATION_PROMPT_TITLE_MAX_LENGTH, } from "./task-types.js";
12
+ import { WAND_ITERATION_PROMPT_DETAIL_MAX_LENGTH, WAND_ITERATION_PROMPT_TITLE_MAX_LENGTH, } from "./task-types.js";
13
13
  /** 用户在上次选择后记住的模式;两条互不影响,互相切不会被覆盖。 */
14
14
  export const COMMIT_CONTEXT_MODE_PREF_KEY = "pref:commitContextMode";
15
15
  export function isCommitContextMode(value) {
@@ -215,10 +215,6 @@ export function iterationPromptDigest(entries) {
215
215
  }
216
216
  return kept.map((line, index) => `${index + 1}. ${line}`).join("\n");
217
217
  }
218
- /** 迭代为空时给模型的一句话,避免它把「没有输入」理解成「没有改动」。 */
219
- export const EMPTY_ITERATION_DIGEST_HINT = `(本迭代还没有记录到提示词,改为依据文件改动判断。)`;
220
- /** 默认迭代的名字;UI 与文档共用,避免各处硬编码。 */
221
- export const DEFAULT_ITERATION_LABEL = DEFAULT_ITERATION_NAME;
222
218
  function toCommitEntry(entry) {
223
219
  return {
224
220
  id: entry.id,
@@ -1,8 +1,4 @@
1
- /**
2
- * Truncates tool_result content in messages for WebSocket transport.
3
- * Cards that are collapsed by default have their large results replaced
4
- * with a summary, and clients fetch full content on-demand via API.
5
- */
1
+ /** Keep tool payloads out of all chat snapshots and fetch them only when opened. */
6
2
  import type { CardExpandDefaults, ContentBlock, ConversationTurn } from "./types.js";
7
3
  export interface WindowedMessages {
8
4
  /** 已截断 + 窗口化后的 turn 列表(最近 windowSize 条)。 */
@@ -72,12 +68,10 @@ export declare function sliceTurnBlocksForTransport(turn: ConversationTurn, star
72
68
  */
73
69
  export declare function windowMessagesForTransport(all: ConversationTurn[] | undefined, cardDefaults: CardExpandDefaults, windowSize?: number): WindowedMessages;
74
70
  /**
75
- * Truncate messages for WebSocket transport. Tool results for collapsed card
76
- * types are replaced with a short summary when they exceed the threshold.
77
- *
78
- * @param messages - Original messages array (not mutated)
79
- * @param cardDefaults - Current card expand defaults from config
80
- * @param streamingTurnIndex - Index of the currently streaming turn (-1 if none).
81
- * Tool results in the streaming turn are never truncated.
71
+ * Project ordinary activity tools to a lean transport shape, regardless of
72
+ * result size or streaming/error state. Interactive questions, task lists,
73
+ * subagents and image cards retain their independent visible contracts.
82
74
  */
75
+ export declare function compactToolMessagesForTransport(messages: ConversationTurn[], knownToolNames?: ReadonlyMap<string, string>): ConversationTurn[];
76
+ /** Existing transport behavior for clients that have not opted into compact tool cards. */
83
77
  export declare function truncateMessagesForTransport(messages: ConversationTurn[], cardDefaults: CardExpandDefaults, streamingTurnIndex?: number): ConversationTurn[];
@@ -1,8 +1,5 @@
1
- /**
2
- * Truncates tool_result content in messages for WebSocket transport.
3
- * Cards that are collapsed by default have their large results replaced
4
- * with a summary, and clients fetch full content on-demand via API.
5
- */
1
+ /** Keep tool payloads out of all chat snapshots and fetch them only when opened. */
2
+ import { createHash } from "node:crypto";
6
3
  import { contentHasStructuredImage } from "./structured-content.js";
7
4
  const TRUNCATION_THRESHOLD = 200;
8
5
  const SUMMARY_LENGTH = 100;
@@ -69,6 +66,10 @@ function blockTransportBytes(block, cardDefaults, toolNameById) {
69
66
  bytes += JSON.stringify(use.input)?.length ?? 0;
70
67
  if (use.description)
71
68
  bytes += use.description.length;
69
+ if (use.semantic)
70
+ bytes += JSON.stringify(use.semantic).length;
71
+ if (use.activity)
72
+ bytes += JSON.stringify(use.activity).length;
72
73
  return bytes;
73
74
  }
74
75
  case "tool_result": {
@@ -346,54 +347,138 @@ function isToolDefaultCollapsed(toolName, defaults) {
346
347
  return false;
347
348
  }
348
349
  }
349
- function getContentString(content) {
350
- return typeof content === "string" ? content : JSON.stringify(content);
350
+ function inputString(input, ...keys) {
351
+ for (const key of keys) {
352
+ const value = input[key];
353
+ if (typeof value !== "string")
354
+ continue;
355
+ const trimmed = value.trim();
356
+ if (trimmed)
357
+ return trimmed;
358
+ }
359
+ return "";
360
+ }
361
+ function toolActivity(use, hasImage) {
362
+ const name = use.name.toLowerCase();
363
+ const input = use.input ?? {};
364
+ const path = inputString(input, "file_path", "path", "filename", "file", "notebook_path");
365
+ const hasMultipleFiles = ["paths", "file_paths", "files"].some((key) => Array.isArray(input[key]) && input[key].length > 1);
366
+ const singleFileKey = !hasMultipleFiles && path
367
+ ? createHash("sha256").update(path.replace(/\\/g, "/")).digest("hex").slice(0, 20)
368
+ : !hasMultipleFiles ? use.activity?.fileKey : undefined;
369
+ const kind = singleFileKey && /^(edit|write|multiedit|notebookedit|file_change|file_edit)$/.test(name)
370
+ ? "edit_file"
371
+ : singleFileKey && /^(read|read_file)$/.test(name)
372
+ ? "read_file"
373
+ : /^(bash|exec|exec_command|command_execution|shell_command|terminal|run_command)$/.test(name)
374
+ ? "run_command"
375
+ : "other";
376
+ const fileKey = kind === "edit_file" || kind === "read_file" ? singleFileKey : undefined;
377
+ const imagePath = /\.(?:png|jpe?g|gif|webp|svg|bmp|avif)(?:[?#].*)?$/i.test(path);
378
+ const occurredAt = use.occurredAt ?? use.activity?.occurredAt;
379
+ return {
380
+ kind,
381
+ label: activityLabel(kind),
382
+ ...(fileKey ? { fileKey } : {}),
383
+ ...(hasImage || imagePath || use.activity?.hasImage ? { hasImage: true } : {}),
384
+ ...(kind === "run_command" && occurredAt ? { occurredAt } : {}),
385
+ };
386
+ }
387
+ function activityLabel(kind) {
388
+ switch (kind) {
389
+ case "edit_file": return "修改文件";
390
+ case "read_file": return "查看文件";
391
+ case "run_command": return "运行命令";
392
+ default: return "使用工具";
393
+ }
394
+ }
395
+ const INDEPENDENT_TOOL_NAMES = new Set([
396
+ "AskUserQuestion", "TodoWrite", "TaskCreate", "TaskUpdate", "TaskList",
397
+ "Pi/todo", "Task", "Agent", "Pi/subagent",
398
+ ]);
399
+ function toolInputShowsImage(input) {
400
+ const path = inputString(input, "file_path", "path", "url");
401
+ return /\.(?:png|jpe?g|gif|webp|svg|bmp|avif)(?:[?#].*)?$/i.test(path);
351
402
  }
352
403
  /**
353
- * Truncate messages for WebSocket transport. Tool results for collapsed card
354
- * types are replaced with a short summary when they exceed the threshold.
355
- *
356
- * @param messages - Original messages array (not mutated)
357
- * @param cardDefaults - Current card expand defaults from config
358
- * @param streamingTurnIndex - Index of the currently streaming turn (-1 if none).
359
- * Tool results in the streaming turn are never truncated.
404
+ * Project ordinary activity tools to a lean transport shape, regardless of
405
+ * result size or streaming/error state. Interactive questions, task lists,
406
+ * subagents and image cards retain their independent visible contracts.
360
407
  */
408
+ export function compactToolMessagesForTransport(messages, knownToolNames) {
409
+ const toolNameMap = new Map(knownToolNames);
410
+ const imageToolIds = new Set();
411
+ const independentToolIds = new Set();
412
+ for (const turn of messages) {
413
+ for (const block of turn.content) {
414
+ if (block.type === "tool_use") {
415
+ toolNameMap.set(block.id, block.name);
416
+ if (INDEPENDENT_TOOL_NAMES.has(block.name) || block.semantic || block.__subagent
417
+ || toolInputShowsImage(block.input ?? {}))
418
+ independentToolIds.add(block.id);
419
+ }
420
+ else if (block.type === "tool_result" && contentHasStructuredImage(block.content)) {
421
+ imageToolIds.add(block.tool_use_id);
422
+ }
423
+ }
424
+ }
425
+ return messages.map((turn) => {
426
+ const truncatedContent = turn.content.map((block) => {
427
+ if (block.type === "tool_use") {
428
+ if (independentToolIds.has(block.id) || imageToolIds.has(block.id))
429
+ return block;
430
+ return {
431
+ ...block,
432
+ description: undefined,
433
+ input: {},
434
+ activity: toolActivity(block, imageToolIds.has(block.id)),
435
+ __subagent: block.__subagent ? {
436
+ ...block.__subagent,
437
+ taskDescription: undefined,
438
+ } : undefined,
439
+ };
440
+ }
441
+ if (block.type === "tool_result" && toolNameMap.has(block.tool_use_id)
442
+ && !INDEPENDENT_TOOL_NAMES.has(toolNameMap.get(block.tool_use_id))
443
+ && !independentToolIds.has(block.tool_use_id)
444
+ && !imageToolIds.has(block.tool_use_id)
445
+ && !block.__subagent) {
446
+ return { ...block, content: "", _truncated: true };
447
+ }
448
+ return block;
449
+ });
450
+ return { ...turn, content: truncatedContent };
451
+ });
452
+ }
453
+ function getContentString(content) {
454
+ return typeof content === "string" ? content : JSON.stringify(content);
455
+ }
456
+ /** Existing transport behavior for clients that have not opted into compact tool cards. */
361
457
  export function truncateMessagesForTransport(messages, cardDefaults, streamingTurnIndex = -1) {
362
458
  return messages.map((turn, turnIndex) => {
363
- // Never truncate the currently streaming turn
364
459
  if (turnIndex === streamingTurnIndex)
365
460
  return turn;
366
461
  const toolNameMap = new Map();
367
462
  for (const block of turn.content) {
368
- if (block.type === "tool_use") {
463
+ if (block.type === "tool_use")
369
464
  toolNameMap.set(block.id, block.name);
370
- }
371
465
  }
372
466
  let changed = false;
373
467
  const truncatedContent = turn.content.map((block) => {
374
468
  if (block.type !== "tool_result")
375
469
  return block;
376
- const result = block;
377
- // Never truncate errors
378
- if (result.is_error)
470
+ if (block.is_error)
379
471
  return block;
380
- const toolName = toolNameMap.get(result.tool_use_id) || "";
472
+ const toolName = toolNameMap.get(block.tool_use_id) ?? "";
381
473
  if (!isToolDefaultCollapsed(toolName, cardDefaults))
382
474
  return block;
383
- // 带图片的 tool_result 永不截断:图片 part 被 JSON.stringify 截半后客户端再也
384
- // 抽不出 data URI(线上表现就是「读图不显示,只剩一串 base64 残文」)。图片要么
385
- // 整块送达(客户端会强制展开渲染),要么由 _truncated + 懒加载单独取回。
386
- if (contentHasStructuredImage(result.content))
475
+ if (contentHasStructuredImage(block.content))
387
476
  return block;
388
- const contentStr = getContentString(result.content);
477
+ const contentStr = getContentString(block.content);
389
478
  if (contentStr.length <= TRUNCATION_THRESHOLD)
390
479
  return block;
391
480
  changed = true;
392
- return {
393
- ...result,
394
- content: contentStr.slice(0, SUMMARY_LENGTH) + "…",
395
- _truncated: true,
396
- };
481
+ return { ...block, content: `${contentStr.slice(0, SUMMARY_LENGTH)}…`, _truncated: true };
397
482
  });
398
483
  return changed ? { ...turn, content: truncatedContent } : turn;
399
484
  });
@@ -8,7 +8,7 @@
8
8
  * 以前每个 provider 各抄一份 handler(5 份几乎相同的 200 行);现在只有一个
9
9
  * 恢复流程,provider 之间的差异收敛成 `StructuredResumeSpec` 里的几个字段。
10
10
  */
11
- import type { Express } from "express";
11
+ import type { Express, Request } from "express";
12
12
  import type { ProcessManager } from "./process-manager.js";
13
13
  import type { StructuredSessionManager } from "./structured-session-manager.js";
14
14
  import type { WandStorage } from "./storage.js";
@@ -20,6 +20,6 @@ export interface ResumeRouteDependencies {
20
20
  defaultMode: ExecutionMode;
21
21
  onSessionCreated?: (cwd: string | undefined | null) => void;
22
22
  /** 会话快照 → 响应 DTO;由调用方注入以免这里再依赖消息截断层。 */
23
- toDetailDTO: (snapshot: SessionSnapshot) => unknown;
23
+ toDetailDTO: (snapshot: SessionSnapshot, req: Request) => unknown;
24
24
  }
25
25
  export declare function registerStructuredResumeRoutes(app: Express, deps: ResumeRouteDependencies): void;
@@ -114,7 +114,7 @@ export function registerStructuredResumeRoutes(app, deps) {
114
114
  ...parseSessionCreationOrigin(body),
115
115
  });
116
116
  onSessionCreated?.(cwd);
117
- res.status(201).json({ resumedClaudeSessionId: nativeId, ...toDetailDTO(snapshot) });
117
+ res.status(201).json({ resumedClaudeSessionId: nativeId, ...toDetailDTO(snapshot, req) });
118
118
  }
119
119
  catch (error) {
120
120
  res.status(400).json({ error: getErrorMessage(error, `无法按 ${spec.label} 会话 ID 恢复会话。`) });