@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
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "cc5d57ee24c38039f381a1bbda3b8d4a95fc26b4",
3
- "builtAt": "2026-09-22T23:09:41.272Z",
4
- "version": "4.72.2",
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",
@@ -91,6 +93,8 @@ export const defaultConfig = () => ({
91
93
  android: defaultAndroidApkConfig(),
92
94
  macos: defaultMacosDmgConfig(),
93
95
  ios: defaultIosIpaConfig(),
96
+ render: defaultRenderConfig(),
97
+ structured: { processHost: "legacy" },
94
98
  cardDefaults: defaultCardExpandDefaults(),
95
99
  defaultModel: "",
96
100
  defaultCodexModel: "",
@@ -111,6 +115,7 @@ export const defaultConfig = () => ({
111
115
  authHeader: "bearer",
112
116
  source: "custom",
113
117
  },
118
+ systemAiModel: "",
114
119
  defaultThinkingEffort: "off",
115
120
  structuredRunner: "cli",
116
121
  inheritEnv: true,
@@ -380,6 +385,16 @@ export function applyStoragePreferences(config, storage) {
380
385
  const v = storage.getPreference(preferenceStorageKey("systemAi"), defaults.systemAi);
381
386
  config.systemAi = normalizeSystemAiConfig(v, defaults.systemAi);
382
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
+ }
383
398
  if (storage.hasPreference(preferenceStorageKey("defaultThinkingEffort"))) {
384
399
  const v = storage.getPreference(preferenceStorageKey("defaultThinkingEffort"), defaults.defaultThinkingEffort ?? "off");
385
400
  if (isThinkingEffort(v))
@@ -518,6 +533,19 @@ export function writePreferenceToStorage(config, storage, key, value, options =
518
533
  config.systemAi = normalized;
519
534
  break;
520
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
+ }
521
549
  case "defaultThinkingEffort": {
522
550
  if (!isThinkingEffort(value))
523
551
  throw new Error(`无效思考深度: ${String(value)}`);
@@ -634,6 +662,22 @@ function defaultIosIpaConfig() {
634
662
  currentIpaFile: "",
635
663
  };
636
664
  }
665
+ /**
666
+ * 默认 auto:存在 wand-render 就用它,否则保持 legacy terminald。
667
+ * 放在 config.json(部署项)而非 SQLite 偏好里——它决定进程所有权,属于部署决策。
668
+ */
669
+ function defaultRenderConfig() {
670
+ return { engine: "auto" };
671
+ }
672
+ /** 旧 config.json 没有 render 字段时回落到默认值,不能因为新字段让启动失败。 */
673
+ function normalizeRenderConfig(input) {
674
+ if (!input || typeof input !== "object" || Array.isArray(input))
675
+ return undefined;
676
+ const raw = input;
677
+ const engine = raw.engine === "rust" || raw.engine === "legacy" ? raw.engine : "auto";
678
+ const binaryPath = typeof raw.binaryPath === "string" ? raw.binaryPath.trim() : "";
679
+ return { engine, ...(binaryPath ? { binaryPath } : {}) };
680
+ }
637
681
  function normalizeIosIpaConfig(input) {
638
682
  if (!input || typeof input !== "object")
639
683
  return undefined;
@@ -737,6 +781,8 @@ function mergeWithDefaults(input) {
737
781
  android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
738
782
  macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
739
783
  ios: normalizeIosIpaConfig(input.ios) ?? defaults.ios,
784
+ render: normalizeRenderConfig(input.render) ?? defaults.render,
785
+ structured: { processHost: input.structured?.processHost === "rust" ? "rust" : "legacy" },
740
786
  cardDefaults: normalizeCardDefaults(input.cardDefaults),
741
787
  defaultProvider: isSessionProvider(input.defaultProvider) ? input.defaultProvider : "claude",
742
788
  defaultSessionKind: input.defaultSessionKind === "pty" ? "pty" : "structured",
@@ -750,6 +796,8 @@ function mergeWithDefaults(input) {
750
796
  commitCli: input.commitCli === "codex" || input.commitCli === "opencode" ? input.commitCli : "claude",
751
797
  commitModel: typeof input.commitModel === "string" ? input.commitModel.trim() : defaults.commitModel,
752
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,
753
801
  defaultThinkingEffort: isThinkingEffort(input.defaultThinkingEffort) ? input.defaultThinkingEffort : "off",
754
802
  structuredRunner: (input.structuredRunner === "sdk" || input.structuredRunner === "cli") ? input.structuredRunner : defaults.structuredRunner,
755
803
  inheritEnv: typeof input.inheritEnv === "boolean" ? input.inheritEnv : (defaults.inheritEnv ?? true),
@@ -0,0 +1,9 @@
1
+ import type net from "node:net";
2
+ /** Bound connect attempts, including sockets that close before the handshake. */
3
+ export declare function waitForDaemonSocket(socket: net.Socket): Promise<void>;
4
+ /** Probe the application, not just the fd: a half-open socket may still be writable. */
5
+ export declare function startDaemonHeartbeat(socket: net.Socket, probe: () => Promise<unknown>, options?: {
6
+ socketPath?: string;
7
+ intervalMs?: number;
8
+ timeoutMs?: number;
9
+ }): () => void;
@@ -0,0 +1,86 @@
1
+ import { lstatSync, utimesSync } from "node:fs";
2
+ const CONNECT_TIMEOUT_MS = 10_000;
3
+ const HEARTBEAT_INTERVAL_MS = 15_000;
4
+ const HEARTBEAT_TIMEOUT_MS = 5_000;
5
+ /** Bound connect attempts, including sockets that close before the handshake. */
6
+ export function waitForDaemonSocket(socket) {
7
+ return new Promise((resolve, reject) => {
8
+ const cleanup = () => {
9
+ clearTimeout(timer);
10
+ socket.off("connect", onConnect);
11
+ socket.off("error", onError);
12
+ socket.off("close", onClose);
13
+ };
14
+ const onConnect = () => { cleanup(); resolve(); };
15
+ const onError = (error) => { cleanup(); socket.destroy(); reject(error); };
16
+ const onClose = () => onError(new Error("Daemon socket closed while connecting"));
17
+ const timer = setTimeout(() => onError(new Error("Daemon socket connect timed out")), CONNECT_TIMEOUT_MS);
18
+ socket.once("connect", onConnect);
19
+ socket.once("error", onError);
20
+ socket.once("close", onClose);
21
+ });
22
+ }
23
+ /** Probe the application, not just the fd: a half-open socket may still be writable. */
24
+ export function startDaemonHeartbeat(socket, probe, options = {}) {
25
+ const { socketPath, intervalMs = HEARTBEAT_INTERVAL_MS, timeoutMs = HEARTBEAT_TIMEOUT_MS } = options;
26
+ // Old daemons have no listener watchdog. Refresh their existing endpoint's
27
+ // timestamps after a successful probe so idle /tmp cleanup won't remove it.
28
+ const identity = (() => {
29
+ if (!socketPath || process.platform === "win32")
30
+ return null;
31
+ try {
32
+ const stat = lstatSync(socketPath);
33
+ return stat.isSocket() && (typeof process.getuid !== "function" || stat.uid === process.getuid())
34
+ ? stat : null;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ })();
40
+ let lastTouch = 0;
41
+ let stopped = false;
42
+ let inFlight = false;
43
+ let deadline = null;
44
+ const stop = () => {
45
+ stopped = true;
46
+ clearInterval(timer);
47
+ if (deadline)
48
+ clearTimeout(deadline);
49
+ socket.off("close", stop);
50
+ };
51
+ const fail = () => {
52
+ if (stopped)
53
+ return;
54
+ stop();
55
+ // Destroy this generation only; its close handler performs normal replay
56
+ // and reconnect. Never retry mutating RPCs or terminate the daemon.
57
+ socket.destroy(new Error("Daemon heartbeat failed; reconnecting"));
58
+ };
59
+ const timer = setInterval(() => {
60
+ if (stopped || inFlight || socket.destroyed)
61
+ return;
62
+ inFlight = true;
63
+ deadline = setTimeout(fail, timeoutMs);
64
+ deadline.unref();
65
+ void Promise.resolve().then(() => stopped ? undefined : probe()).then(() => {
66
+ if (deadline)
67
+ clearTimeout(deadline);
68
+ deadline = null;
69
+ inFlight = false;
70
+ if (stopped || !identity || !socketPath || Date.now() - lastTouch < 60_000)
71
+ return;
72
+ try {
73
+ const stat = lstatSync(socketPath);
74
+ if (stat.dev === identity.dev && stat.ino === identity.ino) {
75
+ const now = new Date();
76
+ utimesSync(socketPath, now, now);
77
+ lastTouch = Date.now();
78
+ }
79
+ }
80
+ catch { /* The owning daemon repairs a missing path; never replace it here. */ }
81
+ }, fail);
82
+ }, intervalMs);
83
+ timer.unref();
84
+ socket.once("close", stop);
85
+ return stop;
86
+ }
@@ -82,6 +82,11 @@ export declare class DistributionManager {
82
82
  deleted: string[];
83
83
  freedBytes: number;
84
84
  }>;
85
+ /** Beta 仅从当前服务端的 macOS 分发目录选择,绝不回退 GitHub。 */
86
+ resolveLatestMacosBeta(): Promise<ResolvedDistributionAsset | null>;
87
+ /** 固定下载文件名,避免检查与下载之间新增包后下载到另一个版本。 */
88
+ resolveMacosBetaDownload(fileName: string): Promise<LocalDistributionAsset | null>;
89
+ private resolveMacosBetaAsset;
85
90
  resolveLatestDmg(): Promise<ResolvedDistributionAsset | null>;
86
91
  resolveMacosDownload(): Promise<LocalDistributionAsset | null>;
87
92
  resolveLatestIpa(): Promise<ResolvedDistributionAsset | null>;
@@ -1,9 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { createReadStream } from "node:fs";
3
- import { mkdir, readdir, readFile, stat, unlink } from "node:fs/promises";
3
+ import { lstat, mkdir, readdir, readFile, stat, unlink } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { compareApkInstallOrder, compareSemver, compareWandInstallOrder, extractSemver } from "./version-utils.js";
6
6
  const GITHUB_CACHE_TTL_MS = 10 * 60 * 1000;
7
+ const MACOS_UPDATE_FILE_PATTERN = /^wand-v\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?(?:\+[A-Za-z0-9.-]+)?\.(?:zip|dmg)$/i;
7
8
  /**
8
9
  * GitHub Release 正文还包含 Android/macOS/iOS 的安装指引;它们属于发布页,
9
10
  * 不该出现在 Android 的更新弹窗。保留分隔线前的变更摘要,并兼容旧版正文。
@@ -211,6 +212,83 @@ export class DistributionManager {
211
212
  }
212
213
  return { deleted, freedBytes };
213
214
  }
215
+ /** Beta 仅从当前服务端的 macOS 分发目录选择,绝不回退 GitHub。 */
216
+ async resolveLatestMacosBeta() {
217
+ const asset = await this.resolveMacosBetaAsset();
218
+ if (!asset?.version || asset.size <= 0)
219
+ return null;
220
+ const sha256 = await this.computeAssetSha256(asset);
221
+ if (!sha256)
222
+ return null;
223
+ return {
224
+ version: asset.version.split("+")[0],
225
+ downloadUrl: `/macos/update-download?fileName=${encodeURIComponent(asset.fileName)}`,
226
+ fileName: asset.fileName,
227
+ size: asset.size,
228
+ source: "local",
229
+ sha256,
230
+ };
231
+ }
232
+ /** 固定下载文件名,避免检查与下载之间新增包后下载到另一个版本。 */
233
+ async resolveMacosBetaDownload(fileName) {
234
+ if (!MACOS_UPDATE_FILE_PATTERN.test(fileName))
235
+ return null;
236
+ await this.refreshConfig();
237
+ const { config, configDir } = this.options;
238
+ if (config.macos?.enabled !== true)
239
+ return null;
240
+ const directory = resolveConfiguredDir(configDir, config.macos.dmgDir, "macos");
241
+ const filePath = path.join(directory, fileName);
242
+ try {
243
+ if (!(await lstat(filePath)).isFile())
244
+ return null;
245
+ }
246
+ catch {
247
+ return null;
248
+ }
249
+ return this.readLocalAsset(filePath, {
250
+ extension: fileName.toLowerCase().endsWith(".zip") ? ".zip" : ".dmg",
251
+ downloadUrl: `/macos/update-download?fileName=${encodeURIComponent(fileName)}`,
252
+ });
253
+ }
254
+ async resolveMacosBetaAsset() {
255
+ await this.refreshConfig();
256
+ const { config, configDir } = this.options;
257
+ if (config.macos?.enabled !== true)
258
+ return null;
259
+ const directory = resolveConfiguredDir(configDir, config.macos.dmgDir, "macos");
260
+ await mkdir(directory, { recursive: true });
261
+ const entries = await readdir(directory, { withFileTypes: true });
262
+ const candidates = await Promise.all(entries
263
+ .filter((entry) => entry.isFile() && MACOS_UPDATE_FILE_PATTERN.test(entry.name))
264
+ .map(async (entry) => ({
265
+ name: entry.name,
266
+ filePath: path.join(directory, entry.name),
267
+ fileStat: await stat(path.join(directory, entry.name)),
268
+ })));
269
+ candidates.sort((a, b) => {
270
+ const aVersion = extractArtifactVersion(a.name, path.extname(a.name));
271
+ const bVersion = extractArtifactVersion(b.name, path.extname(b.name));
272
+ const order = compareWandInstallOrder(aVersion ?? "", bVersion ?? "");
273
+ if (order !== 0)
274
+ return -order;
275
+ // 同版本优先 ZIP,避免 DMG 挂载;同格式多构建按最新修改时间挑选。
276
+ const format = Number(b.name.toLowerCase().endsWith(".zip")) - Number(a.name.toLowerCase().endsWith(".zip"));
277
+ return format || b.fileStat.mtimeMs - a.fileStat.mtimeMs;
278
+ });
279
+ const selected = candidates[0];
280
+ if (!selected)
281
+ return null;
282
+ return {
283
+ fileName: selected.name,
284
+ filePath: selected.filePath,
285
+ size: selected.fileStat.size,
286
+ updatedAt: selected.fileStat.mtime.toISOString(),
287
+ version: extractArtifactVersion(selected.name, path.extname(selected.name)),
288
+ downloadUrl: `/macos/update-download?fileName=${encodeURIComponent(selected.name)}`,
289
+ source: "local",
290
+ };
291
+ }
214
292
  async resolveLatestDmg() {
215
293
  const localDmg = await this.resolveMacosDownload();
216
294
  if (localDmg?.version) {
@@ -494,6 +494,18 @@ function runCliText(command, args, prompt, opts) {
494
494
  }
495
495
  reject(new QuickCommitError(`${command} CLI 失败:${(stderr || stdout).trim() || `exit ${code}`}`, "CLAUDE_CLI_FAILED"));
496
496
  });
497
+ // A CLI may exit before reading stdin (e.g. invalid credentials/argv).
498
+ // The writable stream emits EPIPE independently of ChildProcess.error;
499
+ // without a listener that becomes an uncaught exception after callers
500
+ // have already finished their turn or closed storage.
501
+ child.stdin?.on("error", (error) => {
502
+ if (settled)
503
+ return;
504
+ settled = true;
505
+ clearTimeout(timeout);
506
+ child.kill("SIGTERM");
507
+ reject(new QuickCommitError(`${command} CLI stdin failed: ${error.code ?? "write error"}`, "CLAUDE_CLI_FAILED"));
508
+ });
497
509
  child.stdin?.end(prompt);
498
510
  });
499
511
  }
@@ -3,6 +3,7 @@
3
3
  * Cards that are collapsed by default have their large results replaced
4
4
  * with a summary, and clients fetch full content on-demand via API.
5
5
  */
6
+ import { contentHasStructuredImage } from "./structured-content.js";
6
7
  const TRUNCATION_THRESHOLD = 200;
7
8
  const SUMMARY_LENGTH = 100;
8
9
  /**
@@ -12,12 +13,12 @@ const SUMMARY_LENGTH = 100;
12
13
  */
13
14
  const MESSAGE_WINDOW_SIZE = 40;
14
15
  /**
15
- * 块级窗口的默认预算:init/REST 默认只下发最近这么多个「内容块」(跨 turn 累计,
16
+ * 块级窗口的默认预算:显式启用时 init/REST 只下发最近这么多个「内容块」(跨 turn 累计,
16
17
  * 必要时切掉最旧那条 turn 的头部),更早的块由客户端滚动到顶时按需分页拉取。
17
18
  * turn 级窗口(MESSAGE_WINDOW_SIZE)对「单条 turn 携带上百块」的长任务无能为力——
18
19
  * 一条流式 assistant turn 可膨胀到 1MB+,整条下发会撑爆移动端 WS 帧、拖慢打开。
19
- * 块级窗口是对这种会话的根治手段。仅在客户端显式带 blockBudget 时启用(iOS),
20
- * Web/Android 走原有 turn 级路径不受影响。
20
+ * 块级窗口是对这种会话的根治手段。客户端显式带 blockBudget 时启用(Web/iOS),
21
+ * 未携带的原生客户端仍走 turn 级路径。
21
22
  */
22
23
  const MESSAGE_BLOCK_WINDOW = 60;
23
24
  /**
@@ -162,6 +163,11 @@ export function truncateMessagesForTransport(messages, cardDefaults, streamingTu
162
163
  const toolName = toolNameMap.get(result.tool_use_id) || "";
163
164
  if (!isToolDefaultCollapsed(toolName, cardDefaults))
164
165
  return block;
166
+ // 带图片的 tool_result 永不截断:图片 part 被 JSON.stringify 截半后客户端再也
167
+ // 抽不出 data URI(线上表现就是「读图不显示,只剩一串 base64 残文」)。图片要么
168
+ // 整块送达(客户端会强制展开渲染),要么由 _truncated + 懒加载单独取回。
169
+ if (contentHasStructuredImage(result.content))
170
+ return block;
165
171
  const contentStr = getContentString(result.content);
166
172
  if (contentStr.length <= TRUNCATION_THRESHOLD)
167
173
  return block;
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
  }