@co0ontty/wand 1.30.0 → 1.31.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.
@@ -117,6 +117,50 @@ function tagSubagentBlocks(blocks, parentToolUseId, registry) {
117
117
  };
118
118
  return blocks.map((block) => ({ ...block, __subagent: stamp }));
119
119
  }
120
+ /**
121
+ * 给已被 captureTaskMeta 识别为 Task/Agent 的 tool_use block 本身也盖 __subagent 章。
122
+ * taskId 用自己的 block.id —— 与子消息的 parent_tool_use_id(也等于这个 id)保持一致,
123
+ * 前端 splitTurnBySubagent 按 taskId 分组时父 Task tool_use 和 SDK 转发的子消息能合并到同一段。
124
+ */
125
+ function stampSelfTask(blocks, registry) {
126
+ return blocks.map((b) => {
127
+ if (b.type !== "tool_use")
128
+ return b;
129
+ if (b.__subagent)
130
+ return b; // 已盖章不重复(防止幂等问题)
131
+ const meta = registry.get(b.id);
132
+ if (!meta && b.name !== "Task" && b.name !== "Agent")
133
+ return b;
134
+ const stamp = {
135
+ taskId: b.id,
136
+ ...(meta?.agentType ? { agentType: meta.agentType } : {}),
137
+ ...(meta?.description ? { taskDescription: meta.description } : {}),
138
+ };
139
+ return { ...b, __subagent: stamp };
140
+ });
141
+ }
142
+ /**
143
+ * 当父 assistant 在 parentToolUseId === null 的 user turn 里收到 Task 工具的 tool_result 时,
144
+ * tagSubagentBlocks 不会被调用(它只在 parentToolUseId 非空时盖章)。这里按 tool_use_id
145
+ * 反查 registry,给这条 tool_result 单独盖章,让前端能把它归到同一个 subagent 段。
146
+ */
147
+ function stampParentTaskResults(blocks, registry) {
148
+ return blocks.map((b) => {
149
+ if (b.type !== "tool_result")
150
+ return b;
151
+ if (b.__subagent)
152
+ return b;
153
+ const meta = registry.get(b.tool_use_id);
154
+ if (!meta)
155
+ return b;
156
+ const stamp = {
157
+ taskId: b.tool_use_id,
158
+ ...(meta.agentType ? { agentType: meta.agentType } : {}),
159
+ ...(meta.description ? { taskDescription: meta.description } : {}),
160
+ };
161
+ return { ...b, __subagent: stamp };
162
+ });
163
+ }
120
164
  const STREAM_EMIT_DEBOUNCE_MS = 16;
121
165
  /** Min interval between full saveSession() calls for an in-progress streaming turn.
122
166
  * saveSession serializes the entire messages array, so doing it on every NDJSON
@@ -505,6 +549,7 @@ export class StructuredSessionManager {
505
549
  ? prepareSessionWorktree({ cwd: options.cwd, sessionId: id })
506
550
  : null;
507
551
  const selectedModel = options.model?.trim() || null;
552
+ const initialThinkingEffort = normalizeThinkingEffort(options.thinkingEffort);
508
553
  const snapshot = {
509
554
  id,
510
555
  sessionKind: "structured",
@@ -541,6 +586,7 @@ export class StructuredSessionManager {
541
586
  autoApprovePermissions: shouldAutoApproveForMode(options.mode),
542
587
  approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
543
588
  selectedModel,
589
+ thinkingEffort: initialThinkingEffort,
544
590
  };
545
591
  this.sessions.set(id, snapshot);
546
592
  this.storage.saveSession(snapshot);
@@ -999,6 +1045,11 @@ export class StructuredSessionManager {
999
1045
  if (modelChoice && modelChoice !== "default") {
1000
1046
  args.push("--model", modelChoice);
1001
1047
  }
1048
+ // 思考深度 → --reasoning-effort(off → minimal,standard → low,deep → medium,max → high)
1049
+ const reasoningFlag = thinkingEffortToCodexFlag(session.thinkingEffort);
1050
+ if (reasoningFlag) {
1051
+ args.push("--reasoning-effort", reasoningFlag);
1052
+ }
1002
1053
  if (session.claudeSessionId) {
1003
1054
  args.push("resume", session.claudeSessionId, "-");
1004
1055
  }
@@ -1340,6 +1391,10 @@ export class StructuredSessionManager {
1340
1391
  // variadic 参数贪婪吞掉(commander 的 <tools...> 会一直吃 positional 直到
1341
1392
  // 下一个 flag)。表现为 claude 报 "Input must be provided either through
1342
1393
  // stdin or as a prompt argument when using --print"。
1394
+ //
1395
+ // 思考深度通过给 prompt 前置魔法词触发(think / think hard / ultrathink)。
1396
+ // applyThinkingEffortToPrompt 自身已经做了"用户已写过就不重复加"的保护。
1397
+ const effectivePrompt = applyThinkingEffortToPrompt(prompt, session.thinkingEffort);
1343
1398
  const spawnedAt = new Date().toISOString();
1344
1399
  const child = spawn("claude", args, {
1345
1400
  cwd: session.cwd,
@@ -1352,13 +1407,13 @@ export class StructuredSessionManager {
1352
1407
  pid: child.pid ?? null,
1353
1408
  cwd: session.cwd,
1354
1409
  args,
1355
- prompt: prompt.slice(0, 2048),
1356
- promptLength: prompt.length,
1410
+ prompt: effectivePrompt.slice(0, 2048),
1411
+ promptLength: effectivePrompt.length,
1357
1412
  claudeSessionId: session.claudeSessionId,
1358
1413
  spawnedAt,
1359
1414
  });
1360
1415
  this.pendingChildren.set(sessionId, child);
1361
- child.stdin?.end(prompt);
1416
+ child.stdin?.end(effectivePrompt);
1362
1417
  const turnState = {
1363
1418
  blocks: [],
1364
1419
  result: "",
@@ -1555,7 +1610,7 @@ export class StructuredSessionManager {
1555
1610
  captureTaskMeta(extracted.content, taskMetaRegistry);
1556
1611
  }
1557
1612
  const stamped = parentToolUseId === null
1558
- ? extracted.content
1613
+ ? stampSelfTask(extracted.content, taskMetaRegistry)
1559
1614
  : tagSubagentBlocks(extracted.content, parentToolUseId, taskMetaRegistry);
1560
1615
  if (stamped.length > 0) {
1561
1616
  upsertBlocks(msgId, stamped);
@@ -1599,7 +1654,7 @@ export class StructuredSessionManager {
1599
1654
  ? parsed.parent_tool_use_id
1600
1655
  : null;
1601
1656
  const stamped = parentToolUseId === null
1602
- ? collected
1657
+ ? stampParentTaskResults(collected, taskMetaRegistry)
1603
1658
  : tagSubagentBlocks(collected, parentToolUseId, taskMetaRegistry);
1604
1659
  if (stamped.length > 0) {
1605
1660
  upsertBlocks(`tool_result:${toolResultSeq++}`, stamped);
@@ -1845,6 +1900,12 @@ export class StructuredSessionManager {
1845
1900
  // SDK 默认会把整个 process.env 透传给 claude 子进程;这里显式按 inheritEnv 配置组装,
1846
1901
  // 否则关闭"继承环境变量"开关时 SDK 路径会被静默忽略。
1847
1902
  const sdkEnv = buildChildEnv(this.config.inheritEnv !== false);
1903
+ // 思考深度:off → 显式禁用 thinking,其他 → 给一个固定 budget。
1904
+ // SDK 类型用驼峰 budgetTokens(API 层是 budget_tokens,SDK 内部已做转换)。
1905
+ const sdkThinkingBudget = thinkingEffortToSdkBudget(session.thinkingEffort);
1906
+ const sdkThinking = sdkThinkingBudget > 0
1907
+ ? { type: "enabled", budgetTokens: sdkThinkingBudget }
1908
+ : { type: "disabled" };
1848
1909
  const sdkOptions = {
1849
1910
  cwd: session.cwd,
1850
1911
  abortController,
@@ -1853,6 +1914,7 @@ export class StructuredSessionManager {
1853
1914
  ...(permPolicy.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
1854
1915
  ...(permPolicy.allowedTools ? { allowedTools: permPolicy.allowedTools } : {}),
1855
1916
  ...(isManaged ? { disallowedTools: ["AskUserQuestion"] } : {}),
1917
+ thinking: sdkThinking,
1856
1918
  includePartialMessages: true,
1857
1919
  // 把子 agent 的 text/thinking 也转发回来,UI 才能把"被 Task 召唤来的协作者"
1858
1920
  // 渲染成独立角色的群聊消息。关掉这个开关时只会收到子 agent 的 tool_use/tool_result,
@@ -1983,7 +2045,13 @@ export class StructuredSessionManager {
1983
2045
  streaming.push(block);
1984
2046
  }
1985
2047
  }
1986
- return [...finalizedBlocks, ...streaming];
2048
+ // 流式阶段就给 Task/Agent tool_use 本身盖章,防止"先显示工具卡片几秒再跳为
2049
+ // handoff 行"的闪烁。content_block_start 阶段就有 name=Task/Agent,
2050
+ // stampSelfTask 据此即可命中;agentType 字段藏在 input 里,delta 累计后再由
2051
+ // 后续 captureTaskMeta 回填 registry,下次 rebuild 自动补上更完整的 stamp。
2052
+ captureTaskMeta(streaming, taskMetaRegistry);
2053
+ const stampedStreaming = stampSelfTask(streaming, taskMetaRegistry);
2054
+ return [...finalizedBlocks, ...stampedStreaming];
1987
2055
  };
1988
2056
  const syncSnapshot = () => {
1989
2057
  const current = this.sessions.get(sessionId);
@@ -2095,7 +2163,7 @@ export class StructuredSessionManager {
2095
2163
  const parentToolUseId = assistantMsg.parent_tool_use_id ?? null;
2096
2164
  if (parentToolUseId === null) {
2097
2165
  captureTaskMeta(extracted.content, taskMetaRegistry);
2098
- finalizedBlocks.push(...extracted.content);
2166
+ finalizedBlocks.push(...stampSelfTask(extracted.content, taskMetaRegistry));
2099
2167
  }
2100
2168
  else {
2101
2169
  finalizedBlocks.push(...tagSubagentBlocks(extracted.content, parentToolUseId, taskMetaRegistry));
@@ -2150,7 +2218,7 @@ export class StructuredSessionManager {
2150
2218
  }
2151
2219
  }
2152
2220
  if (parentToolUseId === null) {
2153
- finalizedBlocks.push(...collected);
2221
+ finalizedBlocks.push(...stampParentTaskResults(collected, taskMetaRegistry));
2154
2222
  }
2155
2223
  else {
2156
2224
  finalizedBlocks.push(...tagSubagentBlocks(collected, parentToolUseId, taskMetaRegistry));
@@ -230,14 +230,13 @@ export function startAttachTui(deps) {
230
230
  layout.showToast("服务已安装,按 Shift+S 卸载", "warn", 2500);
231
231
  return;
232
232
  }
233
- const ok = await layout.confirm({
234
- title: "注册为系统服务",
235
- body: process.platform === "linux"
236
- ? "将写入 ~/.config/systemd/user/wand.service。"
237
- : process.platform === "darwin"
238
- ? "将写入 ~/Library/LaunchAgents/com.wand.web.plist。"
239
- : "当前平台暂不支持。",
240
- });
233
+ const isRoot = typeof process.getuid === "function" ? process.getuid() === 0 : false;
234
+ const body = process.platform === "linux"
235
+ ? `将写入 /etc/systemd/system/wand.service,systemctl enable --now,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,可以 Ctrl+C 退出 TUI 后跑 sudo wand service:install。"}`
236
+ : process.platform === "darwin"
237
+ ? `将写入 /Library/LaunchDaemons/com.wand.web.plist,launchctl load,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,退出 TUI 跑 sudo wand service:install。"}`
238
+ : "当前平台暂不支持。";
239
+ const ok = await layout.confirm({ title: "注册为系统服务", body });
241
240
  if (!ok)
242
241
  return;
243
242
  const r = await runOffMicrotask(() => installService({ configPath: deps.configPath }));
@@ -36,12 +36,29 @@ export declare function checkUpdate(currentVersion: string): UpdateInfo;
36
36
  export declare function installUpdate(): CommandResult;
37
37
  export declare function openInBrowser(url: string): CommandResult;
38
38
  export declare function copyToClipboard(text: string): CommandResult;
39
+ /**
40
+ * 服务安装的作用域:
41
+ * - "system" = Linux 写 /etc/systemd/system/wand.service;macOS 写 /Library/LaunchDaemons/
42
+ * 需要 root,开机自启,所有用户可用,不依赖 login session。
43
+ * - "user" = Linux 写 ~/.config/systemd/user/wand.service;macOS 写 ~/Library/LaunchAgents/
44
+ * 不要 root,登出会被回收(除非 loginctl enable-linger)。
45
+ *
46
+ * 默认 system。
47
+ */
48
+ export type ServiceScope = "system" | "user";
49
+ export declare const DEFAULT_SERVICE_SCOPE: ServiceScope;
39
50
  export interface ServiceContext {
40
51
  configPath: string;
41
52
  /** wand 可执行文件路径。优先使用 process.argv[1],回退到 which wand。 */
42
53
  wandBin?: string;
54
+ /** 显式指定作用域。不传走 DEFAULT_SERVICE_SCOPE。 */
55
+ scope?: ServiceScope;
56
+ }
57
+ export interface ServiceOpts {
58
+ /** 不传 = 自动检测已装的那个;都没装就用 default。 */
59
+ scope?: ServiceScope;
43
60
  }
44
- export declare function isServiceInstalled(): boolean;
61
+ export declare function isServiceInstalled(scope?: ServiceScope): boolean;
45
62
  export interface ServiceStatus {
46
63
  /** 是否已安装服务文件。 */
47
64
  installed: boolean;
@@ -54,11 +71,11 @@ export interface ServiceStatus {
54
71
  /** 平台。 */
55
72
  platform: NodeJS.Platform;
56
73
  }
57
- export declare function serviceStatus(): ServiceStatus;
58
- export declare function serviceStart(): CommandResult;
59
- export declare function serviceStop(): CommandResult;
60
- export declare function serviceRestart(): CommandResult;
74
+ export declare function serviceStatus(opts?: ServiceOpts): ServiceStatus;
75
+ export declare function serviceStart(opts?: ServiceOpts): CommandResult;
76
+ export declare function serviceStop(opts?: ServiceOpts): CommandResult;
77
+ export declare function serviceRestart(opts?: ServiceOpts): CommandResult;
61
78
  /** 取最近 N 行服务日志。 */
62
- export declare function serviceLogs(lines?: number): CommandResult;
79
+ export declare function serviceLogs(lines?: number, opts?: ServiceOpts): CommandResult;
63
80
  export declare function installService(ctx: ServiceContext): CommandResult;
64
- export declare function uninstallService(): CommandResult;
81
+ export declare function uninstallService(opts?: ServiceOpts): CommandResult;