@co0ontty/wand 4.72.2 → 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 (101) 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 +48 -0
  5. package/dist/daemon-connection.d.ts +9 -0
  6. package/dist/daemon-connection.js +86 -0
  7. package/dist/distribution-manager.d.ts +5 -0
  8. package/dist/distribution-manager.js +79 -1
  9. package/dist/git-quick-commit.js +12 -0
  10. package/dist/message-truncator.js +9 -3
  11. package/dist/models.d.ts +39 -2
  12. package/dist/models.js +302 -9
  13. package/dist/native/darwin-arm64/wand-render +0 -0
  14. package/dist/native/darwin-arm64/wand-render.sha256 +1 -0
  15. package/dist/native/darwin-arm64/wand-render.version +1 -0
  16. package/dist/native/darwin-arm64/wand-structured-renderd +0 -0
  17. package/dist/native/darwin-arm64/wand-structured-renderd.sha256 +1 -0
  18. package/dist/native/darwin-arm64/wand-structured-renderd.version +1 -0
  19. package/dist/native/darwin-x64/wand-render +0 -0
  20. package/dist/native/darwin-x64/wand-render.sha256 +1 -0
  21. package/dist/native/darwin-x64/wand-render.version +1 -0
  22. package/dist/native/darwin-x64/wand-structured-renderd +0 -0
  23. package/dist/native/darwin-x64/wand-structured-renderd.sha256 +1 -0
  24. package/dist/native/darwin-x64/wand-structured-renderd.version +1 -0
  25. package/dist/native/linux-arm64/wand-render +0 -0
  26. package/dist/native/linux-arm64/wand-render.sha256 +1 -0
  27. package/dist/native/linux-arm64/wand-render.version +1 -0
  28. package/dist/native/linux-arm64/wand-structured-renderd +0 -0
  29. package/dist/native/linux-arm64/wand-structured-renderd.sha256 +1 -0
  30. package/dist/native/linux-arm64/wand-structured-renderd.version +1 -0
  31. package/dist/native/linux-x64/wand-render +0 -0
  32. package/dist/native/linux-x64/wand-render.sha256 +1 -0
  33. package/dist/native/linux-x64/wand-render.version +1 -0
  34. package/dist/native/linux-x64/wand-structured-renderd +0 -0
  35. package/dist/native/linux-x64/wand-structured-renderd.sha256 +1 -0
  36. package/dist/native/linux-x64/wand-structured-renderd.version +1 -0
  37. package/dist/process-manager.d.ts +12 -1
  38. package/dist/process-manager.js +88 -7
  39. package/dist/pty-terminal-state.d.ts +17 -0
  40. package/dist/pty-terminal-state.js +51 -1
  41. package/dist/render-binary.d.ts +61 -0
  42. package/dist/render-binary.js +204 -0
  43. package/dist/render-daemon-client.d.ts +202 -0
  44. package/dist/render-daemon-client.js +1164 -0
  45. package/dist/render-host.d.ts +88 -0
  46. package/dist/render-host.js +440 -0
  47. package/dist/render-protocol.d.ts +128 -0
  48. package/dist/render-protocol.js +99 -0
  49. package/dist/render-structured-client.d.ts +80 -0
  50. package/dist/render-structured-client.js +513 -0
  51. package/dist/render-structured-host.d.ts +31 -0
  52. package/dist/render-structured-host.js +218 -0
  53. package/dist/render-structured-protocol.d.ts +101 -0
  54. package/dist/render-structured-protocol.js +35 -0
  55. package/dist/server-session-routes.js +114 -12
  56. package/dist/server-task-routes.js +55 -6
  57. package/dist/server-update-routes.d.ts +2 -0
  58. package/dist/server-update-routes.js +44 -0
  59. package/dist/server.js +43 -13
  60. package/dist/session-ai-context.d.ts +1 -1
  61. package/dist/session-ai-context.js +15 -4
  62. package/dist/session-logger.d.ts +2 -0
  63. package/dist/session-logger.js +44 -1
  64. package/dist/session-transport.js +1 -1
  65. package/dist/storage.d.ts +2 -1
  66. package/dist/storage.js +14 -5
  67. package/dist/structured-client-protocol.d.ts +3 -1
  68. package/dist/structured-client-protocol.js +85 -25
  69. package/dist/structured-content.d.ts +20 -3
  70. package/dist/structured-content.js +80 -1
  71. package/dist/structured-exec-host.js +7 -1
  72. package/dist/structured-exec-pump.js +6 -2
  73. package/dist/structured-grok-adapter.js +22 -0
  74. package/dist/structured-pi-adapter.js +18 -2
  75. package/dist/structured-provider-common.d.ts +11 -9
  76. package/dist/structured-provider-common.js +60 -18
  77. package/dist/structured-session-manager.d.ts +2 -0
  78. package/dist/structured-session-manager.js +27 -3
  79. package/dist/task-types.d.ts +4 -1
  80. package/dist/terminal-daemon-client.d.ts +9 -2
  81. package/dist/terminal-daemon-client.js +119 -46
  82. package/dist/terminal-daemon-server.js +47 -8
  83. package/dist/terminal-host.d.ts +6 -0
  84. package/dist/terminal-host.js +3 -0
  85. package/dist/tui/commands.js +183 -10
  86. package/dist/tui/ipc-server.js +12 -0
  87. package/dist/types.d.ts +30 -10
  88. package/dist/unix-socket-keepalive.d.ts +3 -0
  89. package/dist/unix-socket-keepalive.js +94 -0
  90. package/dist/wand-task-sync.js +2 -1
  91. package/dist/web-ui/content/scripts.js +79 -73
  92. package/dist/web-ui/content/styles.css +1 -1
  93. package/dist/web-ui/content/tailwind.css +21 -0
  94. package/dist/web-ui/embedded-assets.js +2 -2
  95. package/dist/web-ui/thinking-efforts.d.ts +24 -0
  96. package/dist/web-ui/thinking-efforts.js +81 -0
  97. package/dist/ws-broadcast.d.ts +1 -1
  98. package/dist/ws-broadcast.js +28 -9
  99. package/package.json +8 -3
  100. package/scripts/install-render-binary.js +499 -0
  101. package/scripts/stage-render-binaries.js +626 -0
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
  /**
@@ -0,0 +1 @@
1
+ 27b881febe0177ec8c610a0e3b2884c425434adfc79ecc40e938fd8a1e375de5 wand-render
@@ -0,0 +1 @@
1
+ e0e940747fbe5af0da033763fdbb786c063443d3207aee4b2934e3fe98139776 wand-structured-renderd
@@ -0,0 +1 @@
1
+ 1481b8f4688e5d0bfa2e6c1e4b41da288820fcfb43a737b027401e55334bedaf wand-render
@@ -0,0 +1 @@
1
+ 0.1.3
@@ -0,0 +1 @@
1
+ f67206b4d171f26172787e8184e684d61170c30646ab7d72a1f4232b737b4738 wand-structured-renderd
@@ -0,0 +1 @@
1
+ e51aee7fde515b0356c1a6919b66812ae4ff679f693847f61e816166e99d8805 wand-render
@@ -0,0 +1 @@
1
+ 0.1.3
@@ -0,0 +1 @@
1
+ af342aa8e7dcde0ce2563629921c9080131ed666ba743901ecfa22c31f04bb98 wand-structured-renderd
@@ -0,0 +1 @@
1
+ 27c00887d03a24867f120a67852e5d0666cf7ddae18c3d708cbcca9c45a82427 wand-render
@@ -0,0 +1 @@
1
+ 0.1.3
@@ -0,0 +1 @@
1
+ 52e1ec6dc79695c6af248b4b3bce160b3fb7be2c5f786172c78e13685c89ce39 wand-structured-renderd
@@ -2,7 +2,7 @@ import { EventEmitter } from "node:events";
2
2
  import { WandStorage } from "./storage.js";
3
3
  import { ExecutionMode, ProcessEventHandler, SessionProvider, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
4
4
  import { type PermissionResolution } from "./claude-pty-bridge.js";
5
- import { type PtyTerminalSnapshot } from "./pty-terminal-state.js";
5
+ import { type PtyTerminalSnapshot, type PtyHistoryPage } from "./pty-terminal-state.js";
6
6
  import { type TerminalHost } from "./terminal-host.js";
7
7
  import { type ClaudeHistorySession, type CodexHistorySession, type OpenCodeHistorySession, type QoderHistorySession } from "./provider-history-scanner.js";
8
8
  export type { ClaudeHistorySession, CodexHistorySession, OpenCodeHistorySession, QoderHistorySession, } from "./provider-history-scanner.js";
@@ -15,6 +15,11 @@ export declare class SessionInputError extends Error {
15
15
  readonly sessionStatus?: SessionSnapshot["status"] | undefined;
16
16
  constructor(message: string, code: "SESSION_NOT_FOUND" | "SESSION_NOT_RUNNING" | "SESSION_NO_PTY", sessionId: string, sessionStatus?: SessionSnapshot["status"] | undefined);
17
17
  }
18
+ /** Write outcome is unknown to the caller; do not record this as accepted input. */
19
+ export declare class PtyInputDeliveryError extends Error {
20
+ readonly sessionId: string;
21
+ constructor(sessionId: string, cause: unknown);
22
+ }
18
23
  export declare class ProcessManager extends EventEmitter {
19
24
  private readonly config;
20
25
  private readonly storage;
@@ -39,6 +44,7 @@ export declare class ProcessManager extends EventEmitter {
39
44
  private bindRestoredTerminal;
40
45
  private initializeClaudeBridge;
41
46
  private bindTerminalProcess;
47
+ private handleTerminalResync;
42
48
  private handleTerminalExit;
43
49
  private handleTerminalData;
44
50
  on(_event: "process", listener: ProcessEventHandler): this;
@@ -107,8 +113,13 @@ export declare class ProcessManager extends EventEmitter {
107
113
  */
108
114
  setSessionMode(id: string, mode: ExecutionMode): SessionSnapshot;
109
115
  sendInput(id: string, input: string, view?: "chat" | "terminal", shortcutKey?: string, trackUserInput?: boolean): SessionSnapshot;
116
+ sendInputConfirmed(id: string, input: string, view?: "chat" | "terminal", shortcutKey?: string, trackUserInput?: boolean): Promise<SessionSnapshot>;
117
+ private preparePtyInput;
118
+ private validatePtyInput;
119
+ private trackPtyInput;
110
120
  resize(id: string, cols: number, rows: number): SessionSnapshot;
111
121
  getTerminalState(id: string): PtyTerminalSnapshot | null;
122
+ getTerminalHistoryPage(id: string, before: number, revision: number): Promise<PtyHistoryPage | null>;
112
123
  /**
113
124
  * Finalize provider-only state while leaving the owning PTY shell alive. The
114
125
  * session itself remains running so terminal input can continue normally.