@co0ontty/wand 1.29.6 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -634,7 +634,10 @@ export class ProcessManager extends EventEmitter {
634
634
  knownClaudeProjectMtimes: knownClaudeProjectMtimes ?? undefined,
635
635
  approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
636
636
  selectedModel: selectedModel ?? null,
637
- ptyCols: opts?.cols !== undefined ? clampDimension(opts.cols, 20, 400) : 120,
637
+ // cols 上限 256:与 @wterm/dom WASM grid maxCols 硬编码一致,
638
+ // 防止服务端按 >256 cols 让 Claude 用 CSI 绝对列定位写到 wterm 实际
639
+ // 渲染不到的列上(表现为"内容神奇复制下行")。
640
+ ptyCols: opts?.cols !== undefined ? clampDimension(opts.cols, 20, 256) : 120,
638
641
  ptyRows: opts?.rows !== undefined ? clampDimension(opts.rows, 10, 160) : 36,
639
642
  };
640
643
  if (isClaudeProvider) {
@@ -986,7 +989,7 @@ export class ProcessManager extends EventEmitter {
986
989
  if (!record.ptyProcess || record.status !== "running") {
987
990
  return this.snapshot(record);
988
991
  }
989
- const safeCols = clampDimension(cols, 20, 400);
992
+ const safeCols = clampDimension(cols, 20, 256);
990
993
  const safeRows = clampDimension(rows, 10, 160);
991
994
  const changed = safeCols !== record.ptyCols || safeRows !== record.ptyRows;
992
995
  record.ptyProcess.resize(safeCols, safeRows);
@@ -83,6 +83,40 @@ export function thinkingEffortToCodexFlag(effort) {
83
83
  default: return null;
84
84
  }
85
85
  }
86
+ function captureTaskMeta(blocks, registry) {
87
+ for (const b of blocks) {
88
+ if (b.type !== "tool_use")
89
+ continue;
90
+ if (registry.has(b.id))
91
+ continue;
92
+ const input = b.input ?? {};
93
+ // Claude SDK 把这类"派 subagent 干活"的内置工具叫做 "Agent",CLI/旧版本里
94
+ // 也叫过 "Task"。判定不靠工具名(容易随版本变),而是看 input 是否含有
95
+ // `subagent_type` 字段——这是 Agent/Task 系列的唯一标志。
96
+ const agentType = typeof input.subagent_type === "string" ? input.subagent_type : undefined;
97
+ if (!agentType && b.name !== "Task" && b.name !== "Agent")
98
+ continue;
99
+ const description = typeof input.description === "string" ? input.description : undefined;
100
+ registry.set(b.id, { agentType, description });
101
+ }
102
+ }
103
+ /**
104
+ * Stamp every block with `__subagent` meta keyed to `parentToolUseId`. When
105
+ * the id has no entry yet (rare race: subagent emits before we see the parent
106
+ * Task tool_use), we still stamp the bare taskId so the UI can group blocks;
107
+ * agentType / description backfill on later updates.
108
+ */
109
+ function tagSubagentBlocks(blocks, parentToolUseId, registry) {
110
+ if (!parentToolUseId)
111
+ return blocks;
112
+ const meta = registry.get(parentToolUseId);
113
+ const stamp = {
114
+ taskId: parentToolUseId,
115
+ ...(meta?.agentType ? { agentType: meta.agentType } : {}),
116
+ ...(meta?.description ? { taskDescription: meta.description } : {}),
117
+ };
118
+ return blocks.map((block) => ({ ...block, __subagent: stamp }));
119
+ }
86
120
  const STREAM_EMIT_DEBOUNCE_MS = 16;
87
121
  /** Min interval between full saveSession() calls for an in-progress streaming turn.
88
122
  * saveSession serializes the entire messages array, so doing it on every NDJSON
@@ -1342,6 +1376,9 @@ export class StructuredSessionManager {
1342
1376
  const blocksByKey = new Map();
1343
1377
  const keyOrder = [];
1344
1378
  let toolResultSeq = 0;
1379
+ // 本轮 Task tool_use_id → meta map,由父 assistant 消息里的 Task tool_use
1380
+ // 填充;子 agent message(parent_tool_use_id 非空)来时用它给每个 block 盖章。
1381
+ const taskMetaRegistry = new Map();
1345
1382
  // 估算单个 ContentBlock 的"信息体积"——文字 / thinking / tool input 长度之和。
1346
1383
  // 用于 upsertBlocks 的防御性合并:同一 message.id 重发时,按位置取信息量更大的
1347
1384
  // 那个版本,保证已经吐出的文字 / tool_use input 不会被一条更短的同 id 事件
@@ -1372,22 +1409,25 @@ export class StructuredSessionManager {
1372
1409
  blocksByKey.set(key, blocks);
1373
1410
  return;
1374
1411
  }
1375
- // claude -p 在同一 message.id 的多次 assistant 事件里是**累积**协议:
1376
- // 每次 event 的 content 总是包含之前所有 blocks + 可能的新 block,
1377
- // 长度只增不减、同位置类型不变。两条额外的防御性规则:
1412
+ // claude -p 在同一 message.id 的多次 assistant 事件有两种观察到的协议:
1413
+ // a) **累积模式**:每次 event 的 content = 之前所有 blocks + 0~N block,
1414
+ // 同位置类型一致。流式 text/thinking 的逐字增量属于这种。
1415
+ // b) **拼接模式**:SDK 把 thinking 和后续的 tool_use 拆成两条 event 给同
1416
+ // 一 msg.id 发出,第二条只带 tool_use,**不包含**之前的 thinking。
1417
+ // Opus 4.7 + claude-agent-sdk 实际跑下来就是这种。
1378
1418
  //
1379
- // 1) blocks.length < prev.length —— 短数组覆盖。上游异常 frame,直接拒绝
1380
- // 本次更新,下一帧正常累积 emit 会自然修正。
1419
+ // 老逻辑("同 index 类型不一致 → 保留 prev")只对 a) 友好,碰上 b) 会让第
1420
+ // 二条事件里的 tool_use 直接被丢掉——表现是 Agent / Read 等 tool_use 永远
1421
+ // 不出现在 messages 里,subagent 多角色无法关联 agentType 到父 Task。
1381
1422
  //
1382
- // 2) index 类型不一致 —— 比如 prev[0]=text 而 incoming[0]=tool_use
1383
- // 正常累积下不会发生;一旦发生,**保留 prev[i]**。早期版本走"取
1384
- // volume 大者",会让 tool_useinput JSON 通常更长)抢占 text 位,
1385
- // 导致流式过程中已经渲染的文字突然消失,只剩工具卡片——直到 result
1386
- // event 给出最终 turnState.result,compactContentBlocks 的 fallback
1387
- // 才补回 text。用户反馈"文字消失,回复完成后又出现"就是这条路径。
1423
+ // 新规则:当类型不一致时,把新 block **追加**到 merged 末尾而非覆盖 prev。
1424
+ // 既兼容 a)(同位置同类型仍按累积取大),又兼容 b)(拼接的新类型 block
1425
+ // 进入末尾),还能挡住 b 早期版本里"短回退"的异常 frameblocks.length
1426
+ // < prev.length 时直接拒绝)。
1388
1427
  if (blocks.length < prev.length)
1389
1428
  return;
1390
1429
  const merged = [];
1430
+ const appendix = [];
1391
1431
  for (let i = 0; i < blocks.length; i++) {
1392
1432
  const a = prev[i];
1393
1433
  const b = blocks[i];
@@ -1405,11 +1445,14 @@ export class StructuredSessionManager {
1405
1445
  merged.push(blockVolume(b) >= blockVolume(a) ? b : a);
1406
1446
  }
1407
1447
  else {
1408
- // 类型变了:保留 prev,不让 tool_use 等抢占 text 位。
1448
+ // 类型变了:保留 prev[i],把 incoming block 追加到末尾。
1409
1449
  merged.push(a);
1450
+ appendix.push(b);
1410
1451
  }
1411
1452
  }
1412
1453
  }
1454
+ for (const b of appendix)
1455
+ merged.push(b);
1413
1456
  blocksByKey.set(key, merged);
1414
1457
  };
1415
1458
  const rebuildTurnBlocks = () => {
@@ -1503,8 +1546,19 @@ export class StructuredSessionManager {
1503
1546
  const msgId = typeof parsed.message.id === "string" && parsed.message.id
1504
1547
  ? `assistant:${parsed.message.id}`
1505
1548
  : `assistant:anon:${keyOrder.length}`;
1506
- if (extracted.content.length > 0) {
1507
- upsertBlocks(msgId, extracted.content);
1549
+ // parent_tool_use_id 决定父/子 agent。父 message 里的 Task tool_use 登记
1550
+ // 到 taskMetaRegistry;子 message 的每个 block 用 __subagent 盖章。
1551
+ const parentToolUseId = typeof parsed.parent_tool_use_id === "string" && parsed.parent_tool_use_id
1552
+ ? parsed.parent_tool_use_id
1553
+ : null;
1554
+ if (parentToolUseId === null) {
1555
+ captureTaskMeta(extracted.content, taskMetaRegistry);
1556
+ }
1557
+ const stamped = parentToolUseId === null
1558
+ ? extracted.content
1559
+ : tagSubagentBlocks(extracted.content, parentToolUseId, taskMetaRegistry);
1560
+ if (stamped.length > 0) {
1561
+ upsertBlocks(msgId, stamped);
1508
1562
  rebuildTurnBlocks();
1509
1563
  }
1510
1564
  // NOTE: usage from streaming "assistant" events contains partial/incremental
@@ -1541,8 +1595,14 @@ export class StructuredSessionManager {
1541
1595
  });
1542
1596
  }
1543
1597
  }
1544
- if (collected.length > 0) {
1545
- upsertBlocks(`tool_result:${toolResultSeq++}`, collected);
1598
+ const parentToolUseId = typeof parsed.parent_tool_use_id === "string" && parsed.parent_tool_use_id
1599
+ ? parsed.parent_tool_use_id
1600
+ : null;
1601
+ const stamped = parentToolUseId === null
1602
+ ? collected
1603
+ : tagSubagentBlocks(collected, parentToolUseId, taskMetaRegistry);
1604
+ if (stamped.length > 0) {
1605
+ upsertBlocks(`tool_result:${toolResultSeq++}`, stamped);
1546
1606
  rebuildTurnBlocks();
1547
1607
  }
1548
1608
  syncSnapshot();
@@ -1794,6 +1854,10 @@ export class StructuredSessionManager {
1794
1854
  ...(permPolicy.allowedTools ? { allowedTools: permPolicy.allowedTools } : {}),
1795
1855
  ...(isManaged ? { disallowedTools: ["AskUserQuestion"] } : {}),
1796
1856
  includePartialMessages: true,
1857
+ // 把子 agent 的 text/thinking 也转发回来,UI 才能把"被 Task 召唤来的协作者"
1858
+ // 渲染成独立角色的群聊消息。关掉这个开关时只会收到子 agent 的 tool_use/tool_result,
1859
+ // text/thinking 被 SDK 吞掉。
1860
+ forwardSubagentText: true,
1797
1861
  ...(systemPromptParts.length > 0 ? { appendSystemPrompt: systemPromptParts.join("\n\n") } : {}),
1798
1862
  ...(sdkClaudeBinary ? { pathToClaudeCodeExecutable: sdkClaudeBinary } : {}),
1799
1863
  };
@@ -1855,6 +1919,11 @@ export class StructuredSessionManager {
1855
1919
  // Tracks in-progress streaming blocks keyed by content_block index from stream_event.
1856
1920
  // The map is cleared whenever a complete `assistant` message arrives — its blocks
1857
1921
  // are then promoted into `finalizedBlocks` below.
1922
+ //
1923
+ // `parentToolUseId` carries through from SDKPartialAssistantMessage so we can
1924
+ // stamp streaming blocks with subagent persona *during* streaming, not only
1925
+ // after the completion event. Without it, subagent text shows up under the
1926
+ // parent's avatar for tens of ms then snaps to the subagent — visible flicker.
1858
1927
  const streamingBlockByIndex = new Map();
1859
1928
  // Blocks from messages that have already completed within this turn — including
1860
1929
  // the parent assistant's prior messages, every subagent assistant message, and
@@ -1862,6 +1931,9 @@ export class StructuredSessionManager {
1862
1931
  // back-to-back; without this list, each new streaming message would visually
1863
1932
  // erase everything that came before it in the same turn.
1864
1933
  const finalizedBlocks = [];
1934
+ // Per-turn Task tool_use_id → meta map; populated from the parent assistant's
1935
+ // Task tool_use blocks and consulted when subagent messages arrive.
1936
+ const taskMetaRegistry = new Map();
1865
1937
  let emitTimer = null;
1866
1938
  const flushEmit = () => {
1867
1939
  if (emitTimer) {
@@ -1884,11 +1956,12 @@ export class StructuredSessionManager {
1884
1956
  const sorted = [...streamingBlockByIndex.entries()].sort((a, b) => a[0] - b[0]);
1885
1957
  const streaming = [];
1886
1958
  for (const [, sb] of sorted) {
1959
+ let block = null;
1887
1960
  if (sb.type === "text") {
1888
- streaming.push({ type: "text", text: sb.text });
1961
+ block = { type: "text", text: sb.text };
1889
1962
  }
1890
1963
  else if (sb.type === "thinking") {
1891
- streaming.push({ type: "thinking", thinking: sb.thinking });
1964
+ block = { type: "thinking", thinking: sb.thinking };
1892
1965
  }
1893
1966
  else if (sb.type === "tool_use" && sb.id && sb.name) {
1894
1967
  let input = {};
@@ -1898,7 +1971,16 @@ export class StructuredSessionManager {
1898
1971
  }
1899
1972
  catch { /* partial json */ }
1900
1973
  }
1901
- streaming.push({ type: "tool_use", id: sb.id, name: sb.name, input });
1974
+ block = { type: "tool_use", id: sb.id, name: sb.name, input };
1975
+ }
1976
+ if (!block)
1977
+ continue;
1978
+ if (sb.parentToolUseId) {
1979
+ const [stamped] = tagSubagentBlocks([block], sb.parentToolUseId, taskMetaRegistry);
1980
+ streaming.push(stamped);
1981
+ }
1982
+ else {
1983
+ streaming.push(block);
1902
1984
  }
1903
1985
  }
1904
1986
  return [...finalizedBlocks, ...streaming];
@@ -1950,7 +2032,9 @@ export class StructuredSessionManager {
1950
2032
  break;
1951
2033
  // Incremental streaming events (opt-in via includePartialMessages: true)
1952
2034
  if (msg.type === "stream_event") {
1953
- const ev = msg.event;
2035
+ const partial = msg;
2036
+ const ev = partial.event;
2037
+ const partialParentId = partial.parent_tool_use_id ?? null;
1954
2038
  if (ev.type === "content_block_start") {
1955
2039
  const cb = ev.content_block;
1956
2040
  const blockType = cb.type;
@@ -1963,6 +2047,7 @@ export class StructuredSessionManager {
1963
2047
  thinking: typeof cb.thinking === "string" ? cb.thinking : "",
1964
2048
  partialInput: "",
1965
2049
  finalized: false,
2050
+ parentToolUseId: partialParentId,
1966
2051
  });
1967
2052
  turnState.blocks = rebuildStreamingBlocks();
1968
2053
  syncSnapshot();
@@ -2005,7 +2090,16 @@ export class StructuredSessionManager {
2005
2090
  if (msg.type === "assistant") {
2006
2091
  const assistantMsg = msg;
2007
2092
  const extracted = this.extractAssistantMessage(assistantMsg.message);
2008
- finalizedBlocks.push(...extracted.content);
2093
+ // 父 assistant 的 Task tool_use → 注册到本轮 taskMeta map;
2094
+ // 子 agent 的 message(parent_tool_use_id 非空)→ 给每个 block 盖章。
2095
+ const parentToolUseId = assistantMsg.parent_tool_use_id ?? null;
2096
+ if (parentToolUseId === null) {
2097
+ captureTaskMeta(extracted.content, taskMetaRegistry);
2098
+ finalizedBlocks.push(...extracted.content);
2099
+ }
2100
+ else {
2101
+ finalizedBlocks.push(...tagSubagentBlocks(extracted.content, parentToolUseId, taskMetaRegistry));
2102
+ }
2009
2103
  streamingBlockByIndex.clear();
2010
2104
  turnState.blocks = rebuildStreamingBlocks();
2011
2105
  if (assistantMsg.session_id)
@@ -2041,11 +2135,13 @@ export class StructuredSessionManager {
2041
2135
  // tool call, or a subagent's tool_result during Task execution).
2042
2136
  if (msg.type === "user") {
2043
2137
  const userMsg = msg;
2138
+ const parentToolUseId = userMsg.parent_tool_use_id ?? null;
2044
2139
  const content = Array.isArray(userMsg.message?.content) ? userMsg.message.content : [];
2140
+ const collected = [];
2045
2141
  for (const block of content) {
2046
2142
  const b = block;
2047
2143
  if (b?.type === "tool_result") {
2048
- finalizedBlocks.push({
2144
+ collected.push({
2049
2145
  type: "tool_result",
2050
2146
  tool_use_id: typeof b.tool_use_id === "string" ? b.tool_use_id : "",
2051
2147
  content: this.normalizeToolResultContent(b.content),
@@ -2053,6 +2149,12 @@ export class StructuredSessionManager {
2053
2149
  });
2054
2150
  }
2055
2151
  }
2152
+ if (parentToolUseId === null) {
2153
+ finalizedBlocks.push(...collected);
2154
+ }
2155
+ else {
2156
+ finalizedBlocks.push(...tagSubagentBlocks(collected, parentToolUseId, taskMetaRegistry));
2157
+ }
2056
2158
  turnState.blocks = rebuildStreamingBlocks();
2057
2159
  syncSnapshot();
2058
2160
  scheduleEmit();
@@ -2227,11 +2329,17 @@ export class StructuredSessionManager {
2227
2329
  const previous = compacted[compacted.length - 1];
2228
2330
  if (previous
2229
2331
  && previous.type === "text"
2230
- && block.type === "text") {
2332
+ && block.type === "text"
2333
+ // 子 agent 边界不合并:父 assistant 的 text 与子 agent 的 text 必须保持独立,
2334
+ // 渲染层才能切段并给子 agent 单独发头像。同一 subagent 内部允许合并。
2335
+ && (previous.__subagent?.taskId ?? null) === (block.__subagent?.taskId ?? null)) {
2231
2336
  // 用新对象替换 compacted 末尾,**不要**就地改 previous.text —— previous
2232
2337
  // 通常和调用方持有的 turnState.blocks 共享引用,原地 mutate 会让下次
2233
2338
  // syncSnapshot 把已合并的内容再合并一次,呈指数级复制。
2234
- compacted[compacted.length - 1] = { type: "text", text: `${previous.text}${block.text}` };
2339
+ const merged = { type: "text", text: `${previous.text}${block.text}` };
2340
+ if (previous.__subagent)
2341
+ merged.__subagent = previous.__subagent;
2342
+ compacted[compacted.length - 1] = merged;
2235
2343
  continue;
2236
2344
  }
2237
2345
  compacted.push(block);
package/dist/types.d.ts CHANGED
@@ -299,13 +299,30 @@ export interface ChatMessage {
299
299
  role: "user" | "assistant";
300
300
  content: string;
301
301
  }
302
+ /**
303
+ * Meta marker attached to blocks emitted by a Task-spawned subagent. Present
304
+ * on every block (text / thinking / tool_use / tool_result) whose origin is a
305
+ * subagent's stream rather than the main assistant. Drives the multi-persona
306
+ * chat rendering ("third cat joining the conversation").
307
+ *
308
+ * `taskId` is the parent Task tool_use id (= SDK's `parent_tool_use_id`).
309
+ * The parent Task tool_use block itself is NOT marked — it lives in the
310
+ * main assistant's stream.
311
+ */
312
+ export interface SubagentMeta {
313
+ taskId: string;
314
+ agentType?: string;
315
+ taskDescription?: string;
316
+ }
302
317
  export interface TextBlock {
303
318
  type: "text";
304
319
  text: string;
320
+ __subagent?: SubagentMeta;
305
321
  }
306
322
  export interface ThinkingBlock {
307
323
  type: "thinking";
308
324
  thinking: string;
325
+ __subagent?: SubagentMeta;
309
326
  }
310
327
  export interface ToolUseBlock {
311
328
  type: "tool_use";
@@ -313,6 +330,7 @@ export interface ToolUseBlock {
313
330
  name: string;
314
331
  description?: string;
315
332
  input: Record<string, unknown>;
333
+ __subagent?: SubagentMeta;
316
334
  }
317
335
  export interface ToolResultBlock {
318
336
  type: "tool_result";
@@ -324,6 +342,7 @@ export interface ToolResultBlock {
324
342
  is_error?: boolean;
325
343
  /** When true, content has been truncated for transport. Client should fetch full content via API. */
326
344
  _truncated?: boolean;
345
+ __subagent?: SubagentMeta;
327
346
  }
328
347
  export type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock;
329
348
  export interface ConversationTurn {