@co0ontty/wand 4.73.0 → 4.75.0-beta.g1d477a2

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.
Files changed (62) hide show
  1. package/dist/build-info.json +4 -4
  2. package/dist/claude-sdk-runner.d.ts +2 -3
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +28 -0
  5. package/dist/message-truncator.js +3 -3
  6. package/dist/models.d.ts +39 -2
  7. package/dist/models.js +302 -9
  8. package/dist/native/darwin-arm64/wand-render +0 -0
  9. package/dist/native/darwin-arm64/wand-render.sha256 +1 -1
  10. package/dist/native/darwin-arm64/wand-render.version +1 -1
  11. package/dist/native/darwin-arm64/wand-structured-renderd +0 -0
  12. package/dist/native/darwin-arm64/wand-structured-renderd.sha256 +1 -0
  13. package/dist/native/darwin-arm64/wand-structured-renderd.version +1 -0
  14. package/dist/native/darwin-x64/wand-render +0 -0
  15. package/dist/native/darwin-x64/wand-render.sha256 +1 -1
  16. package/dist/native/darwin-x64/wand-render.version +1 -1
  17. package/dist/native/darwin-x64/wand-structured-renderd +0 -0
  18. package/dist/native/darwin-x64/wand-structured-renderd.sha256 +1 -0
  19. package/dist/native/darwin-x64/wand-structured-renderd.version +1 -0
  20. package/dist/native/linux-arm64/wand-render +0 -0
  21. package/dist/native/linux-arm64/wand-render.sha256 +1 -1
  22. package/dist/native/linux-arm64/wand-render.version +1 -1
  23. package/dist/native/linux-arm64/wand-structured-renderd +0 -0
  24. package/dist/native/linux-arm64/wand-structured-renderd.sha256 +1 -0
  25. package/dist/native/linux-arm64/wand-structured-renderd.version +1 -0
  26. package/dist/native/linux-x64/wand-render +0 -0
  27. package/dist/native/linux-x64/wand-render.sha256 +1 -1
  28. package/dist/native/linux-x64/wand-render.version +1 -1
  29. package/dist/native/linux-x64/wand-structured-renderd +0 -0
  30. package/dist/native/linux-x64/wand-structured-renderd.sha256 +1 -0
  31. package/dist/native/linux-x64/wand-structured-renderd.version +1 -0
  32. package/dist/process-manager.d.ts +2 -1
  33. package/dist/process-manager.js +15 -3
  34. package/dist/pty-terminal-state.d.ts +17 -0
  35. package/dist/pty-terminal-state.js +51 -1
  36. package/dist/render-daemon-client.d.ts +33 -2
  37. package/dist/render-daemon-client.js +66 -16
  38. package/dist/render-host.d.ts +27 -3
  39. package/dist/render-host.js +156 -17
  40. package/dist/server-session-routes.js +42 -3
  41. package/dist/server-task-routes.js +3 -3
  42. package/dist/server.js +12 -4
  43. package/dist/session-ai-context.d.ts +1 -1
  44. package/dist/session-ai-context.js +15 -4
  45. package/dist/session-logger.d.ts +2 -0
  46. package/dist/session-logger.js +44 -1
  47. package/dist/structured-grok-adapter.js +22 -0
  48. package/dist/structured-provider-common.d.ts +11 -9
  49. package/dist/structured-provider-common.js +60 -18
  50. package/dist/task-types.d.ts +2 -1
  51. package/dist/terminal-daemon-server.js +27 -4
  52. package/dist/tui/commands.js +183 -10
  53. package/dist/types.d.ts +11 -9
  54. package/dist/wand-task-sync.js +2 -1
  55. package/dist/web-ui/content/scripts.js +78 -72
  56. package/dist/web-ui/content/styles.css +1 -1
  57. package/dist/web-ui/content/tailwind.css +9 -0
  58. package/dist/web-ui/embedded-assets.js +2 -2
  59. package/dist/web-ui/thinking-efforts.d.ts +24 -0
  60. package/dist/web-ui/thinking-efforts.js +81 -0
  61. package/dist/ws-broadcast.js +11 -4
  62. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "7ff219242659cdf0ec264845fbba73b7bbf8561f",
3
- "builtAt": "2026-09-23T13:34:35.791Z",
4
- "version": "4.73.0",
5
- "channel": "stable"
2
+ "commit": "1d477a238a1e35f04bdd754c96a3bfff5e1738a3",
3
+ "builtAt": "2026-09-24T07:07:30.365Z",
4
+ "version": "4.75.0-beta.g1d477a2",
5
+ "channel": "beta"
6
6
  }
@@ -1,4 +1,3 @@
1
- import { type Options as SdkOptions } from "@anthropic-ai/claude-agent-sdk";
2
1
  export type ClaudeRunErrorCode = "CLAUDE_CLI_MISSING" | "CLAUDE_TIMEOUT" | "CLAUDE_CLI_FAILED" | "CLAUDE_EMPTY_RESULT";
3
2
  export declare class ClaudeRunError extends Error {
4
3
  readonly code: ClaudeRunErrorCode;
@@ -9,8 +8,8 @@ export interface RunClaudePrintOptions {
9
8
  timeoutMs: number;
10
9
  /** Optional Claude model id / alias. Empty or "default" keeps Claude Code's own default. */
11
10
  model?: string;
12
- /** Optional adaptive-thinking effort, aligned with the active Claude session. */
13
- effort?: SdkOptions["effort"];
11
+ /** Optional adaptive-thinking effort. CLI 动态档位(high / xhigh 等)原样传给 SDK。 */
12
+ effort?: string;
14
13
  /**
15
14
  * 用户偏好的回复语言(取自 config.language)。传进来时会以
16
15
  * `appendSystemPrompt` 形式灌给 Claude,保证 quick-commit / prompt-optimizer
package/dist/config.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { WandStorage } from "./storage.js";
7
7
  * 升级路径:老 JSON 里仍存有这些字段时,首次启动会被搬到 DB(见 migrateLegacyPreferencesToDb),
8
8
  * 然后下一次 saveConfig 写回 JSON 时它们会被剥离(见 stripPreferenceFields)。
9
9
  */
10
- export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultTaskWorktree", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "defaultPiModel", "commitCli", "commitModel", "commitAiSource", "systemAi", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
10
+ export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultTaskWorktree", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "defaultPiModel", "commitCli", "commitModel", "commitAiSource", "systemAi", "systemAiCli", "systemAiModel", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
11
11
  export type PreferenceKey = (typeof PREFERENCE_KEYS)[number];
12
12
  export declare function isPreferenceKey(key: string): key is PreferenceKey;
13
13
  /**
package/dist/config.js CHANGED
@@ -33,6 +33,8 @@ export const PREFERENCE_KEYS = [
33
33
  "commitModel",
34
34
  "commitAiSource",
35
35
  "systemAi",
36
+ "systemAiCli",
37
+ "systemAiModel",
36
38
  "defaultThinkingEffort",
37
39
  "structuredRunner",
38
40
  "language",
@@ -113,6 +115,7 @@ export const defaultConfig = () => ({
113
115
  authHeader: "bearer",
114
116
  source: "custom",
115
117
  },
118
+ systemAiModel: "",
116
119
  defaultThinkingEffort: "off",
117
120
  structuredRunner: "cli",
118
121
  inheritEnv: true,
@@ -382,6 +385,16 @@ export function applyStoragePreferences(config, storage) {
382
385
  const v = storage.getPreference(preferenceStorageKey("systemAi"), defaults.systemAi);
383
386
  config.systemAi = normalizeSystemAiConfig(v, defaults.systemAi);
384
387
  }
388
+ if (storage.hasPreference(preferenceStorageKey("systemAiCli"))) {
389
+ const v = storage.getPreference(preferenceStorageKey("systemAiCli"), defaults.systemAiCli);
390
+ if (isSessionProvider(v))
391
+ config.systemAiCli = v;
392
+ }
393
+ if (storage.hasPreference(preferenceStorageKey("systemAiModel"))) {
394
+ const v = storage.getPreference(preferenceStorageKey("systemAiModel"), defaults.systemAiModel);
395
+ if (typeof v === "string")
396
+ config.systemAiModel = v.trim();
397
+ }
385
398
  if (storage.hasPreference(preferenceStorageKey("defaultThinkingEffort"))) {
386
399
  const v = storage.getPreference(preferenceStorageKey("defaultThinkingEffort"), defaults.defaultThinkingEffort ?? "off");
387
400
  if (isThinkingEffort(v))
@@ -520,6 +533,19 @@ export function writePreferenceToStorage(config, storage, key, value, options =
520
533
  config.systemAi = normalized;
521
534
  break;
522
535
  }
536
+ case "systemAiCli": {
537
+ if (!isSessionProvider(value))
538
+ throw new Error(`无效系统 AI CLI: ${String(value)}`);
539
+ storage.setPreference(dbKey, value);
540
+ config.systemAiCli = value;
541
+ break;
542
+ }
543
+ case "systemAiModel": {
544
+ const v = typeof value === "string" ? value.trim() : "";
545
+ storage.setPreference(dbKey, v);
546
+ config.systemAiModel = v;
547
+ break;
548
+ }
523
549
  case "defaultThinkingEffort": {
524
550
  if (!isThinkingEffort(value))
525
551
  throw new Error(`无效思考深度: ${String(value)}`);
@@ -770,6 +796,8 @@ function mergeWithDefaults(input) {
770
796
  commitCli: input.commitCli === "codex" || input.commitCli === "opencode" ? input.commitCli : "claude",
771
797
  commitModel: typeof input.commitModel === "string" ? input.commitModel.trim() : defaults.commitModel,
772
798
  commitAiSource: input.commitAiSource === "api" ? "api" : "cli",
799
+ systemAiCli: isSessionProvider(input.systemAiCli) ? input.systemAiCli : undefined,
800
+ systemAiModel: typeof input.systemAiModel === "string" ? input.systemAiModel.trim() : defaults.systemAiModel,
773
801
  defaultThinkingEffort: isThinkingEffort(input.defaultThinkingEffort) ? input.defaultThinkingEffort : "off",
774
802
  structuredRunner: (input.structuredRunner === "sdk" || input.structuredRunner === "cli") ? input.structuredRunner : defaults.structuredRunner,
775
803
  inheritEnv: typeof input.inheritEnv === "boolean" ? input.inheritEnv : (defaults.inheritEnv ?? true),
@@ -13,12 +13,12 @@ const SUMMARY_LENGTH = 100;
13
13
  */
14
14
  const MESSAGE_WINDOW_SIZE = 40;
15
15
  /**
16
- * 块级窗口的默认预算:init/REST 默认只下发最近这么多个「内容块」(跨 turn 累计,
16
+ * 块级窗口的默认预算:显式启用时 init/REST 只下发最近这么多个「内容块」(跨 turn 累计,
17
17
  * 必要时切掉最旧那条 turn 的头部),更早的块由客户端滚动到顶时按需分页拉取。
18
18
  * turn 级窗口(MESSAGE_WINDOW_SIZE)对「单条 turn 携带上百块」的长任务无能为力——
19
19
  * 一条流式 assistant turn 可膨胀到 1MB+,整条下发会撑爆移动端 WS 帧、拖慢打开。
20
- * 块级窗口是对这种会话的根治手段。仅在客户端显式带 blockBudget 时启用(iOS),
21
- * Web/Android 走原有 turn 级路径不受影响。
20
+ * 块级窗口是对这种会话的根治手段。客户端显式带 blockBudget 时启用(Web/iOS),
21
+ * 未携带的原生客户端仍走 turn 级路径。
22
22
  */
23
23
  const MESSAGE_BLOCK_WINDOW = 60;
24
24
  /**
package/dist/models.d.ts CHANGED
@@ -12,6 +12,8 @@ interface ModelCacheStorage {
12
12
  interface ModelCommandOptions {
13
13
  env: NodeJS.ProcessEnv;
14
14
  timeout: number;
15
+ /** Written to stdin, then closed. Pi RPC needs this. */
16
+ input?: string;
15
17
  }
16
18
  interface ModelCommandResult {
17
19
  stdout: string;
@@ -36,6 +38,18 @@ export interface ModelRefreshOptions {
36
38
  verifyClaudeCandidates?: boolean;
37
39
  now?: () => Date;
38
40
  }
41
+ export interface ThinkingEffortLevel {
42
+ effort: string;
43
+ description?: string;
44
+ }
45
+ /** 各 CLI 自己报出来的思考档位。Codex / 部分 OpenCode 模型另有 per-model 列表。 */
46
+ export interface ProviderThinkingEfforts {
47
+ claude: ThinkingEffortLevel[];
48
+ opencode: ThinkingEffortLevel[];
49
+ grok: ThinkingEffortLevel[];
50
+ qoder: ThinkingEffortLevel[];
51
+ pi: ThinkingEffortLevel[];
52
+ }
39
53
  export interface ModelCache {
40
54
  models: ClaudeModelInfo[];
41
55
  codexModels: ClaudeModelInfo[];
@@ -43,10 +57,13 @@ export interface ModelCache {
43
57
  grokModels: ClaudeModelInfo[];
44
58
  qoderModels: ClaudeModelInfo[];
45
59
  piModels: ClaudeModelInfo[];
60
+ thinkingEfforts: ProviderThinkingEfforts;
46
61
  claudeVersion: string | null;
47
62
  opencodeVersion: string | null;
48
63
  refreshedAt: string;
49
64
  }
65
+ /** 探针失败时的已知档位。下一次 CLI 成功应答会把它换掉。 */
66
+ export declare const FALLBACK_PROVIDER_THINKING: ProviderThinkingEfforts;
50
67
  /** Immutable-looking snapshot returned to API clients. */
51
68
  export interface ModelCatalogSnapshot extends ModelCache {
52
69
  /** SHA-256 of the catalog excluding `refreshedAt`. Changes only with content. */
@@ -62,6 +79,13 @@ export interface ModelCatalogRefreshRequest {
62
79
  /** Administrator-triggered refreshes may also validate Claude candidates. */
63
80
  verifyClaudeCandidates?: boolean;
64
81
  }
82
+ /** 从 CLI 的 help / 非法档位报错里抽出 `low, medium, high` 这种列表。 */
83
+ export declare function parseCliEffortList(text: string): string[];
84
+ /**
85
+ * `opencode models --verbose` 是「一行 id + 一段 JSON」。
86
+ * variants 是这个模型自己的推理档位;没有 JSON 时退回一行一个 id。
87
+ */
88
+ export declare function parseOpenCodeModelCatalog(stdout: string): ClaudeModelInfo[];
65
89
  /**
66
90
  * Parse `grok models` human-readable output:
67
91
  *
@@ -79,12 +103,22 @@ export declare function parseGrokModels(stdout: string): ClaudeModelInfo[];
79
103
  * safe ID below the `MODEL` header.
80
104
  */
81
105
  export declare function parseQoderModels(stdout: string): ClaudeModelInfo[];
106
+ /**
107
+ * Same rule as Pi's `getSupportedThinkingLevels`: no reasoning means only off;
108
+ * `xhigh` / `max` exist only when `thinkingLevelMap` defines them; `null` drops a level.
109
+ */
110
+ export declare function piThinkingLevelsForModel(model: {
111
+ reasoning?: boolean;
112
+ thinkingLevelMap?: Record<string, unknown> | null;
113
+ }): string[];
114
+ /** `pi --mode rpc` + `get_available_models`. One JSON response holds every model's thinking map. */
115
+ export declare function parsePiRpcModels(stdout: string): ClaudeModelInfo[];
82
116
  /**
83
117
  * Parse `pi --list-models`.
84
118
  *
85
119
  * Pi prints a padEnd-aligned table whose selectable `--model` value is
86
- * `provider/id` (for example `xai/grok-4.6`). The first two columns are the
87
- * only ones we keep; context / max-out / thinking / images are display-only.
120
+ * `provider/id` (for example `xai/grok-4.6`). The table only says thinking
121
+ * yes/no, so per-model levels come from `parsePiRpcModels` instead.
88
122
  */
89
123
  export declare function parsePiModels(stdout: string): ClaudeModelInfo[];
90
124
  /** Parse `opencode models`, whose stable machine-friendly output is one provider/model id per line. */
@@ -106,8 +140,11 @@ export declare class ModelCatalogService {
106
140
  private hasPersistedSnapshot;
107
141
  private refreshPromise;
108
142
  private inFlightIncludesVerification;
143
+ private readonly changeListeners;
109
144
  constructor(getOptions: () => ModelRefreshOptions);
110
145
  snapshot(): ModelCatalogSnapshot;
146
+ /** 目录内容真正变化时通知。客户端靠它刷新下拉,不用自己再跑一遍 CLI。 */
147
+ onChanged(listener: (result: ModelCatalogRefreshResult) => void): () => void;
111
148
  refresh(request?: ModelCatalogRefreshRequest): Promise<ModelCatalogRefreshResult>;
112
149
  private performRefresh;
113
150
  }
package/dist/models.js CHANGED
@@ -57,6 +57,28 @@ const QODER_FALLBACK_MODELS = [
57
57
  const PI_FALLBACK_MODELS = [
58
58
  { id: "default", label: "跟随 Pi 默认", alias: true },
59
59
  ];
60
+ const EFFORT_ID_PATTERN = /^[a-z][a-z0-9_-]{0,31}$/;
61
+ function effortLevels(ids) {
62
+ return ids.map((effort) => ({ effort }));
63
+ }
64
+ /** 探针失败时的已知档位。下一次 CLI 成功应答会把它换掉。 */
65
+ export const FALLBACK_PROVIDER_THINKING = {
66
+ claude: effortLevels(["low", "medium", "high", "xhigh", "max"]),
67
+ opencode: effortLevels(["low", "medium", "high", "max"]),
68
+ grok: effortLevels(["low", "medium", "high", "xhigh"]),
69
+ qoder: effortLevels(["auto", "none", "low", "medium", "high", "xhigh", "max", "ultracode"]),
70
+ pi: effortLevels(["minimal", "low", "medium", "high", "xhigh", "max"]),
71
+ };
72
+ function cloneThinkingEfforts(efforts) {
73
+ const copy = (levels) => levels.map((level) => ({ ...level }));
74
+ return {
75
+ claude: copy(efforts.claude),
76
+ opencode: copy(efforts.opencode),
77
+ grok: copy(efforts.grok),
78
+ qoder: copy(efforts.qoder),
79
+ pi: copy(efforts.pi),
80
+ };
81
+ }
60
82
  function cloneModels(models) {
61
83
  return models.map((model) => ({
62
84
  ...model,
@@ -73,6 +95,7 @@ function cloneCache(cache) {
73
95
  grokModels: cloneModels(cache.grokModels),
74
96
  qoderModels: cloneModels(cache.qoderModels),
75
97
  piModels: cloneModels(cache.piModels),
98
+ thinkingEfforts: cloneThinkingEfforts(cache.thinkingEfforts),
76
99
  claudeVersion: cache.claudeVersion,
77
100
  opencodeVersion: cache.opencodeVersion,
78
101
  refreshedAt: cache.refreshedAt,
@@ -82,7 +105,8 @@ function defaultCommandRunner(file, args, options) {
82
105
  return execFileAsync(file, args, {
83
106
  env: options.env,
84
107
  timeout: options.timeout,
85
- maxBuffer: 1024 * 1024,
108
+ maxBuffer: 4 * 1024 * 1024,
109
+ ...(options.input !== undefined ? { input: options.input } : {}),
86
110
  }).then(({ stdout, stderr }) => ({ stdout: String(stdout), stderr: String(stderr) }));
87
111
  }
88
112
  function normalizeClaudeModelId(value) {
@@ -238,6 +262,7 @@ function createInitialCache(options) {
238
262
  grokModels: cloneModels(GROK_FALLBACK_MODELS),
239
263
  qoderModels: cloneModels(QODER_FALLBACK_MODELS),
240
264
  piModels: cloneModels(PI_FALLBACK_MODELS),
265
+ thinkingEfforts: cloneThinkingEfforts(FALLBACK_PROVIDER_THINKING),
241
266
  claudeVersion: null,
242
267
  opencodeVersion: null,
243
268
  refreshedAt: now.toISOString(),
@@ -275,11 +300,11 @@ async function probeCodexModels(runner, env) {
275
300
  }
276
301
  async function probeOpenCode(runner, env) {
277
302
  const [modelsResult, versionResult] = await Promise.allSettled([
278
- runner("opencode", ["models"], { env, timeout: 8000 }),
303
+ runner("opencode", ["models", "--verbose"], { env, timeout: 8000 }),
279
304
  runner("opencode", ["--version"], { env, timeout: 5000 }),
280
305
  ]);
281
306
  const models = modelsResult.status === "fulfilled"
282
- ? { ok: true, value: parseOpenCodeModels(modelsResult.value.stdout) }
307
+ ? { ok: true, value: parseOpenCodeModelCatalog(modelsResult.value.stdout) }
283
308
  : { ok: false };
284
309
  const version = versionResult.status === "fulfilled"
285
310
  ? {
@@ -308,14 +333,143 @@ async function probeQoderModels(runner, env) {
308
333
  }
309
334
  }
310
335
  async function probePiModels(runner, env) {
336
+ const rpcText = await commandText(runner, "pi", ["--mode", "rpc", "--no-session"], env, 12_000, `${JSON.stringify({ id: "wand-models", type: "get_available_models" })}\n`);
337
+ const fromRpc = parsePiRpcModels(rpcText);
338
+ if (fromRpc.length > 1 || (fromRpc.length === 1 && fromRpc[0]?.id !== "default")) {
339
+ return { ok: true, value: fromRpc };
340
+ }
311
341
  try {
312
342
  const { stdout } = await runner("pi", ["--list-models"], { env, timeout: 8000 });
313
- return { ok: true, value: parsePiModels(stdout) };
343
+ const parsed = parsePiModels(stdout);
344
+ return parsed.some((model) => model.id !== "default") ? { ok: true, value: parsed } : { ok: false };
314
345
  }
315
346
  catch {
316
347
  return { ok: false };
317
348
  }
318
349
  }
350
+ /** 从 CLI 的 help / 非法档位报错里抽出 `low, medium, high` 这种列表。 */
351
+ export function parseCliEffortList(text) {
352
+ const patterns = [
353
+ /valid values(?:\s+are)?\s*:\s*([^.`\n]+)/i,
354
+ /use one of:\s*([^.`\n]+)/i,
355
+ /set thinking level:\s*([^.`\n]+)/i,
356
+ ];
357
+ for (const pattern of patterns) {
358
+ const match = text.match(pattern);
359
+ if (!match?.[1])
360
+ continue;
361
+ const ids = match[1]
362
+ .split(/[,/]/)
363
+ .map((part) => part.trim().toLowerCase())
364
+ .filter((part) => EFFORT_ID_PATTERN.test(part));
365
+ if (ids.length)
366
+ return Array.from(new Set(ids));
367
+ }
368
+ return [];
369
+ }
370
+ function effortLevelsFromIds(ids) {
371
+ return ids.map((effort) => ({ effort }));
372
+ }
373
+ async function commandText(runner, file, args, env, timeout, input) {
374
+ try {
375
+ const result = await runner(file, args, { env, timeout, ...(input !== undefined ? { input } : {}) });
376
+ return `${result.stderr}\n${result.stdout}`;
377
+ }
378
+ catch (error) {
379
+ const record = error;
380
+ return `${String(record.stderr ?? "")}\n${String(record.stdout ?? "")}\n${String(record.message ?? "")}`;
381
+ }
382
+ }
383
+ async function probeEffortList(runner, env, file, args) {
384
+ const text = await commandText(runner, file, args, env, 8000);
385
+ const ids = parseCliEffortList(text);
386
+ return ids.length ? { ok: true, value: effortLevelsFromIds(ids) } : { ok: false };
387
+ }
388
+ function variantsToEfforts(value) {
389
+ const ids = [];
390
+ if (Array.isArray(value)) {
391
+ for (const item of value) {
392
+ if (typeof item === "string")
393
+ ids.push(item);
394
+ else if (isRecord(item) && typeof item.effort === "string")
395
+ ids.push(item.effort);
396
+ }
397
+ }
398
+ else if (isRecord(value)) {
399
+ ids.push(...Object.keys(value));
400
+ }
401
+ const unique = Array.from(new Set(ids.map((id) => id.trim().toLowerCase()).filter((id) => EFFORT_ID_PATTERN.test(id))));
402
+ return effortLevelsFromIds(unique);
403
+ }
404
+ /**
405
+ * `opencode models --verbose` 是「一行 id + 一段 JSON」。
406
+ * variants 是这个模型自己的推理档位;没有 JSON 时退回一行一个 id。
407
+ */
408
+ export function parseOpenCodeModelCatalog(stdout) {
409
+ const verbose = parseOpenCodeVerboseModels(stdout);
410
+ if (verbose.length)
411
+ return verbose;
412
+ return parseOpenCodeModels(stdout);
413
+ }
414
+ function parseOpenCodeVerboseModels(stdout) {
415
+ const lines = stdout.split(/\r?\n/);
416
+ const found = [];
417
+ for (let index = 0; index < lines.length; index += 1) {
418
+ const header = lines[index]?.trim() ?? "";
419
+ if (!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:/-]*$/i.test(header))
420
+ continue;
421
+ if ((lines[index + 1] ?? "").trim() !== "{")
422
+ continue;
423
+ let depth = 0;
424
+ let end = index + 1;
425
+ for (; end < lines.length; end += 1) {
426
+ for (const character of lines[end] ?? "") {
427
+ if (character === "{")
428
+ depth += 1;
429
+ else if (character === "}")
430
+ depth -= 1;
431
+ }
432
+ if (depth === 0)
433
+ break;
434
+ }
435
+ if (depth !== 0)
436
+ continue;
437
+ let record = null;
438
+ try {
439
+ record = JSON.parse(lines.slice(index + 1, end + 1).join("\n"));
440
+ }
441
+ catch {
442
+ record = null;
443
+ }
444
+ const reasoningEfforts = record ? variantsToEfforts(record.variants) : [];
445
+ found.push({
446
+ id: header,
447
+ label: header,
448
+ ...(reasoningEfforts.length ? { reasoningEfforts } : {}),
449
+ });
450
+ index = end;
451
+ }
452
+ if (!found.length)
453
+ return [];
454
+ return [
455
+ { id: "default", label: "跟随 OpenCode 默认", alias: true },
456
+ ...found,
457
+ ];
458
+ }
459
+ function unionModelEfforts(models) {
460
+ const seen = new Set();
461
+ const levels = [];
462
+ for (const model of models) {
463
+ for (const level of model.reasoningEfforts ?? []) {
464
+ const effort = level.effort.trim().toLowerCase();
465
+ if (!EFFORT_ID_PATTERN.test(effort) || seen.has(effort))
466
+ continue;
467
+ seen.add(effort);
468
+ levels.push({ effort, ...(level.description ? { description: level.description } : {}) });
469
+ }
470
+ }
471
+ return levels;
472
+ }
319
473
  function createOfficialModelsApi(apiKey) {
320
474
  const client = new Anthropic({ apiKey });
321
475
  return {
@@ -400,7 +554,7 @@ export function parseGrokModels(stdout) {
400
554
  defaultModel = defaultMatch[1];
401
555
  continue;
402
556
  }
403
- const bulletMatch = line.match(/^\*+\s*([A-Za-z0-9][A-Za-z0-9._:-]{0,127})(?:\s*\(.*\))?\s*$/);
557
+ const bulletMatch = line.match(/^[-*•]+\s+([A-Za-z0-9][A-Za-z0-9._:-]{0,127})(?:\s*\(.*\))?\s*$/);
404
558
  if (bulletMatch) {
405
559
  ids.push(bulletMatch[1]);
406
560
  continue;
@@ -460,12 +614,88 @@ export function parseQoderModels(stdout) {
460
614
  }
461
615
  return [...cloneModels(QODER_FALLBACK_MODELS), ...discovered];
462
616
  }
617
+ const PI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
618
+ /**
619
+ * Same rule as Pi's `getSupportedThinkingLevels`: no reasoning means only off;
620
+ * `xhigh` / `max` exist only when `thinkingLevelMap` defines them; `null` drops a level.
621
+ */
622
+ export function piThinkingLevelsForModel(model) {
623
+ if (!model.reasoning)
624
+ return ["off"];
625
+ const map = model.thinkingLevelMap ?? {};
626
+ return PI_THINKING_LEVELS.filter((level) => {
627
+ if (!Object.prototype.hasOwnProperty.call(map, level)) {
628
+ return level !== "xhigh" && level !== "max";
629
+ }
630
+ return map[level] !== null;
631
+ });
632
+ }
633
+ /** `pi --mode rpc` + `get_available_models`. One JSON response holds every model's thinking map. */
634
+ export function parsePiRpcModels(stdout) {
635
+ let models = null;
636
+ for (const rawLine of stdout.split(/\r?\n/)) {
637
+ const line = rawLine.trim();
638
+ if (!line.startsWith("{"))
639
+ continue;
640
+ let record = null;
641
+ try {
642
+ record = JSON.parse(line);
643
+ }
644
+ catch {
645
+ record = null;
646
+ }
647
+ if (!record || record.type !== "response" || record.command !== "get_available_models" || record.success !== true) {
648
+ continue;
649
+ }
650
+ const data = isRecord(record.data) ? record.data : null;
651
+ if (data && Array.isArray(data.models))
652
+ models = data.models;
653
+ }
654
+ if (!models)
655
+ return [];
656
+ const discovered = [];
657
+ const seen = new Set();
658
+ for (const entry of models) {
659
+ if (!isRecord(entry))
660
+ continue;
661
+ const provider = typeof entry.provider === "string" ? entry.provider.trim() : "";
662
+ const modelId = typeof entry.id === "string" ? entry.id.trim() : "";
663
+ const id = provider && modelId ? `${provider}/${modelId}` : "";
664
+ if (!id || !PI_MODEL_ID_PATTERN.test(id) || id.length > 128 || seen.has(id))
665
+ continue;
666
+ seen.add(id);
667
+ const name = typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : id;
668
+ const reasoningEfforts = effortLevelsFromIds(piThinkingLevelsForModel({
669
+ reasoning: entry.reasoning === true,
670
+ thinkingLevelMap: isRecord(entry.thinkingLevelMap) || entry.thinkingLevelMap === null
671
+ ? entry.thinkingLevelMap
672
+ : undefined,
673
+ }));
674
+ discovered.push({
675
+ id,
676
+ label: name === id ? id : `${name} · ${id}`,
677
+ ...(reasoningEfforts.length ? { reasoningEfforts } : {}),
678
+ });
679
+ }
680
+ if (!discovered.length)
681
+ return [];
682
+ const union = unionModelEfforts(discovered);
683
+ return [
684
+ {
685
+ id: "default",
686
+ label: "跟随 Pi 默认",
687
+ alias: true,
688
+ ...(union.length ? { reasoningEfforts: union } : {}),
689
+ },
690
+ ...discovered,
691
+ ];
692
+ }
463
693
  /**
464
694
  * Parse `pi --list-models`.
465
695
  *
466
696
  * Pi prints a padEnd-aligned table whose selectable `--model` value is
467
- * `provider/id` (for example `xai/grok-4.6`). The first two columns are the
468
- * only ones we keep; context / max-out / thinking / images are display-only.
697
+ * `provider/id` (for example `xai/grok-4.6`). The table only says thinking
698
+ * yes/no, so per-model levels come from `parsePiRpcModels` instead.
469
699
  */
470
700
  export function parsePiModels(stdout) {
471
701
  const discovered = [];
@@ -566,6 +796,7 @@ function catalogRevision(cache) {
566
796
  grokModels: cache.grokModels,
567
797
  qoderModels: cache.qoderModels,
568
798
  piModels: cache.piModels,
799
+ thinkingEfforts: cache.thinkingEfforts,
569
800
  claudeVersion: cache.claudeVersion,
570
801
  opencodeVersion: cache.opencodeVersion,
571
802
  });
@@ -637,6 +868,35 @@ function parsePersistedModelList(value) {
637
868
  }
638
869
  return result;
639
870
  }
871
+ function parsePersistedThinkingEfforts(value) {
872
+ const fallback = cloneThinkingEfforts(FALLBACK_PROVIDER_THINKING);
873
+ if (!isRecord(value))
874
+ return fallback;
875
+ const providers = ["claude", "opencode", "grok", "qoder", "pi"];
876
+ for (const provider of providers) {
877
+ const levels = parsePersistedEffortLevels(value[provider]);
878
+ if (levels)
879
+ fallback[provider] = levels;
880
+ }
881
+ return fallback;
882
+ }
883
+ function parsePersistedEffortLevels(value) {
884
+ if (!Array.isArray(value))
885
+ return null;
886
+ const levels = [];
887
+ const seen = new Set();
888
+ for (const entry of value) {
889
+ if (!isRecord(entry))
890
+ return null;
891
+ const effort = safePersistedString(entry.effort, 32)?.toLowerCase() ?? "";
892
+ if (!EFFORT_ID_PATTERN.test(effort) || seen.has(effort))
893
+ return null;
894
+ seen.add(effort);
895
+ const description = safePersistedString(entry.description);
896
+ levels.push({ effort, ...(description ? { description } : {}) });
897
+ }
898
+ return levels.length ? levels : null;
899
+ }
640
900
  function parsePersistedModelCatalog(value) {
641
901
  if (!isRecord(value) || value.version !== MODEL_CATALOG_CACHE_VERSION || !isRecord(value.catalog))
642
902
  return null;
@@ -664,6 +924,7 @@ function parsePersistedModelCatalog(value) {
664
924
  grokModels,
665
925
  qoderModels,
666
926
  piModels,
927
+ thinkingEfforts: parsePersistedThinkingEfforts(catalog.thinkingEfforts),
667
928
  claudeVersion,
668
929
  opencodeVersion,
669
930
  refreshedAt,
@@ -703,7 +964,7 @@ async function discoverModelCache(options, previous) {
703
964
  const now = options.now?.() ?? new Date();
704
965
  const env = resolveChildEnv(options);
705
966
  const runner = options.commandRunner ?? defaultCommandRunner;
706
- const [claudeVersionProbe, codexProbe, opencodeProbe, grokProbe, qoderProbe, piProbe, apiProbe] = await Promise.all([
967
+ const [claudeVersionProbe, codexProbe, opencodeProbe, grokProbe, qoderProbe, piProbe, apiProbe, claudeEffortProbe, grokEffortProbe, qoderEffortProbe, piEffortProbe,] = await Promise.all([
707
968
  probeClaudeVersion(runner, env),
708
969
  probeCodexModels(runner, env),
709
970
  probeOpenCode(runner, env),
@@ -711,6 +972,10 @@ async function discoverModelCache(options, previous) {
711
972
  probeQoderModels(runner, env),
712
973
  probePiModels(runner, env),
713
974
  listClaudeModelsFromApi(options, env),
975
+ probeEffortList(runner, env, "claude", ["--effort", "__wand_probe__", "--help"]),
976
+ probeEffortList(runner, env, "grok", ["--effort", "__wand_probe__", "-p", "x", "--output-format", "streaming-json", "--max-turns", "1"]),
977
+ probeEffortList(runner, env, "qodercli", ["-p", "x", "--reasoning-effort", "__wand_probe__"]),
978
+ probeEffortList(runner, env, "pi", ["--help"]),
714
979
  ]);
715
980
  const claudeVersion = claudeVersionProbe.ok ? claudeVersionProbe.value : previous.claudeVersion;
716
981
  const priorVerifications = loadClaudeVerifications(options.storage);
@@ -743,6 +1008,19 @@ async function discoverModelCache(options, previous) {
743
1008
  grokModels: grokProbe.ok ? grokProbe.value : cloneModels(previous.grokModels),
744
1009
  qoderModels: qoderProbe.ok ? qoderProbe.value : cloneModels(previous.qoderModels),
745
1010
  piModels: piProbe.ok ? piProbe.value : cloneModels(previous.piModels),
1011
+ thinkingEfforts: {
1012
+ claude: claudeEffortProbe.ok ? claudeEffortProbe.value : cloneThinkingEfforts(previous.thinkingEfforts).claude,
1013
+ opencode: opencodeProbe.models.ok && unionModelEfforts(opencodeProbe.models.value).length
1014
+ ? unionModelEfforts(opencodeProbe.models.value)
1015
+ : cloneThinkingEfforts(previous.thinkingEfforts).opencode,
1016
+ grok: grokEffortProbe.ok ? grokEffortProbe.value : cloneThinkingEfforts(previous.thinkingEfforts).grok,
1017
+ qoder: qoderEffortProbe.ok ? qoderEffortProbe.value : cloneThinkingEfforts(previous.thinkingEfforts).qoder,
1018
+ pi: piProbe.ok && unionModelEfforts(piProbe.value).length
1019
+ ? unionModelEfforts(piProbe.value)
1020
+ : piEffortProbe.ok
1021
+ ? piEffortProbe.value
1022
+ : cloneThinkingEfforts(previous.thinkingEfforts).pi,
1023
+ },
746
1024
  claudeVersion,
747
1025
  opencodeVersion: opencodeProbe.version.ok ? opencodeProbe.version.value : previous.opencodeVersion,
748
1026
  refreshedAt: now.toISOString(),
@@ -763,6 +1041,7 @@ export class ModelCatalogService {
763
1041
  hasPersistedSnapshot;
764
1042
  refreshPromise = null;
765
1043
  inFlightIncludesVerification = false;
1044
+ changeListeners = new Set();
766
1045
  constructor(getOptions) {
767
1046
  this.getOptions = getOptions;
768
1047
  const initialOptions = getOptions();
@@ -774,6 +1053,11 @@ export class ModelCatalogService {
774
1053
  snapshot() {
775
1054
  return { ...cloneCache(this.cache), revision: this.revision };
776
1055
  }
1056
+ /** 目录内容真正变化时通知。客户端靠它刷新下拉,不用自己再跑一遍 CLI。 */
1057
+ onChanged(listener) {
1058
+ this.changeListeners.add(listener);
1059
+ return () => { this.changeListeners.delete(listener); };
1060
+ }
777
1061
  refresh(request = {}) {
778
1062
  const verifyClaudeCandidates = request.verifyClaudeCandidates === true;
779
1063
  if (this.refreshPromise) {
@@ -811,7 +1095,16 @@ export class ModelCatalogService {
811
1095
  savePersistedModelCatalog(options.storage, this.cache, this.revision);
812
1096
  this.hasPersistedSnapshot = Boolean(options.storage);
813
1097
  }
814
- return { ...this.snapshot(), changed, checkedAt };
1098
+ const result = { ...this.snapshot(), changed, checkedAt };
1099
+ if (changed) {
1100
+ for (const listener of this.changeListeners) {
1101
+ try {
1102
+ listener(result);
1103
+ }
1104
+ catch { /* 通知失败不影响目录本身 */ }
1105
+ }
1106
+ }
1107
+ return result;
815
1108
  }
816
1109
  }
817
1110
  /**
@@ -1 +1 @@
1
- 6855db8c0423d995dbd57567076cdf019a4e3306a465d0828cca80ee2f85f34c wand-render
1
+ 27b881febe0177ec8c610a0e3b2884c425434adfc79ecc40e938fd8a1e375de5 wand-render
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.3
@@ -0,0 +1 @@
1
+ e0e940747fbe5af0da033763fdbb786c063443d3207aee4b2934e3fe98139776 wand-structured-renderd
Binary file