@co0ontty/wand 4.72.1 → 4.72.2-beta.g26c27fb
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.
- package/dist/build-info.json +4 -4
- package/dist/config.js +18 -0
- package/dist/message-truncator.js +6 -0
- package/dist/render-binary.d.ts +61 -0
- package/dist/render-binary.js +204 -0
- package/dist/render-daemon-client.d.ts +153 -0
- package/dist/render-daemon-client.js +902 -0
- package/dist/render-host.d.ts +59 -0
- package/dist/render-host.js +270 -0
- package/dist/render-protocol.d.ts +128 -0
- package/dist/render-protocol.js +99 -0
- package/dist/server-session-routes.js +42 -6
- package/dist/server.js +26 -3
- package/dist/session-transport.js +1 -1
- package/dist/structured-client-protocol.d.ts +3 -1
- package/dist/structured-client-protocol.js +46 -3
- package/dist/structured-content.d.ts +20 -3
- package/dist/structured-content.js +80 -1
- package/dist/structured-pi-adapter.js +18 -2
- package/dist/structured-session-manager.js +2 -2
- package/dist/types.d.ts +12 -0
- package/dist/web-ui/content/scripts.js +59 -59
- package/dist/web-ui/embedded-assets.js +1 -1
- package/dist/ws-broadcast.js +4 -4
- package/package.json +7 -3
- package/scripts/install-render-binary.js +499 -0
- package/scripts/stage-render-binaries.js +517 -0
package/dist/server.js
CHANGED
|
@@ -55,7 +55,8 @@ import { isLogBusActive, wandTuiLog } from "./tui/log-bus.js";
|
|
|
55
55
|
import { EMBEDDED_WEB_ASSETS } from "./web-ui/embedded-assets.js";
|
|
56
56
|
import { renderApp } from "./web-ui/index.js";
|
|
57
57
|
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
58
|
-
import {
|
|
58
|
+
import { TerminalDaemonClient } from "./terminal-daemon-client.js";
|
|
59
|
+
import { createUpgradeAwareTerminalHost, resolveRenderEngine } from "./render-host.js";
|
|
59
60
|
import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
|
|
60
61
|
import { updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
|
|
61
62
|
const SERVER_MODULE_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
@@ -317,12 +318,34 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
317
318
|
config,
|
|
318
319
|
repositoryUrl: PKG_REPO_URL,
|
|
319
320
|
});
|
|
320
|
-
|
|
321
|
+
// PTY 所有者可以是 legacy terminald 或常驻的 Rust Render:升级期由复合 host 按所有权
|
|
322
|
+
// 路由(旧会话留在 legacy,新会话进 Render),对客户端契约没有任何改变。
|
|
323
|
+
const ptyHosts = options.terminalHost
|
|
324
|
+
? { host: options.terminalHost, renderHost: null, legacyHost: null }
|
|
325
|
+
: await createUpgradeAwareTerminalHost(configPath, {
|
|
326
|
+
engine: config.render?.engine,
|
|
327
|
+
binaryPath: config.render?.binaryPath,
|
|
328
|
+
});
|
|
329
|
+
const terminalHost = ptyHosts.host;
|
|
321
330
|
const processes = new ProcessManager(config, storage, configDir, terminalHost);
|
|
322
331
|
const structuredLogger = new SessionLogger(configDir, config.shortcutLogMaxBytes);
|
|
323
332
|
// Production startup requires the daemon-backed host so CLI runs outlive web
|
|
324
333
|
// restarts. In-process hosts are retained only for explicit test injection.
|
|
325
|
-
const structuredExecHost =
|
|
334
|
+
const structuredExecHost = ptyHosts.legacyHost
|
|
335
|
+
?? (terminalHost instanceof TerminalDaemonClient ? terminalHost : undefined);
|
|
336
|
+
// Render 协议 v1 没有任何 structured* 方法,而 Render 模式对 legacy terminald 只
|
|
337
|
+
// adopt、不 spawn:本机没有旧 terminald 时这里就是 undefined,结构化 CLI run 会
|
|
338
|
+
// 退化成进程内运行(`recoverDetachedRuns()` 也会 no-op)。这相对「web 重启不丢任务」
|
|
339
|
+
// 是功能回退,绝不能静默发生 —— 见 docs/render-upgrade-path.md §10.4。
|
|
340
|
+
if (!structuredExecHost && process.env.WAND_TEST_MODE !== "1" && !process.env.NODE_TEST_CONTEXT) {
|
|
341
|
+
const renderEngine = resolveRenderEngine(process.env.WAND_RENDER_ENGINE, config.render?.engine);
|
|
342
|
+
process.stderr.write("[wand] WARNING: structured CLI runs will NOT survive a web restart. " +
|
|
343
|
+
`render.engine=${renderEngine} only adopts an existing legacy terminald (it never spawns one) and Render protocol v1 has no ` +
|
|
344
|
+
"structured methods, so structured runs stay inside this web process and recoverDetachedRuns() is a no-op. " +
|
|
345
|
+
`Workaround: set "render": { "engine": "legacy" } in ${configPath} so Wand starts/adopts the legacy terminald again ` +
|
|
346
|
+
"(PTYs already owned by Render keep running), or keep a legacy terminald alive on this machine. " +
|
|
347
|
+
"See docs/render-upgrade-path.md §10.4.\n");
|
|
348
|
+
}
|
|
326
349
|
const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger, undefined, {}, structuredExecHost);
|
|
327
350
|
const sessionRegistry = new SessionRegistry(processes, structuredSessions, storage);
|
|
328
351
|
const missions = new Missions(storage, structuredSessions, sessionRegistry);
|
|
@@ -87,7 +87,7 @@ export function toSessionDetailDTO(snapshot, options = {}) {
|
|
|
87
87
|
outputOffset,
|
|
88
88
|
outputTotal: rawOutput.length,
|
|
89
89
|
outputTruncated: outputOffset > 0,
|
|
90
|
-
...(options.messages !== undefined ? { messages: enrichStructuredMessages(options.messages) } : {}),
|
|
90
|
+
...(options.messages !== undefined ? { messages: enrichStructuredMessages(options.messages, snapshot.id) } : {}),
|
|
91
91
|
...(options.messageOffset !== undefined ? { messageOffset: options.messageOffset } : {}),
|
|
92
92
|
...(options.messageTotal !== undefined ? { messageTotal: options.messageTotal } : {}),
|
|
93
93
|
...(options.leadingBlockOffset !== undefined ? { leadingBlockOffset: options.leadingBlockOffset } : {}),
|
|
@@ -3,5 +3,7 @@ export declare const WAND_PROTOCOL_VERSION = 2;
|
|
|
3
3
|
/**
|
|
4
4
|
* Add Wand-owned semantics without mutating persisted provider blocks.
|
|
5
5
|
* This is the external interface consumed by every client.
|
|
6
|
+
*
|
|
7
|
+
* 传 sessionId 时额外把内联图片改写成取图 URL(见 rewriteInlineToolImageUrls)。
|
|
6
8
|
*/
|
|
7
|
-
export declare function enrichStructuredMessages(messages: ConversationTurn[]): ConversationTurn[];
|
|
9
|
+
export declare function enrichStructuredMessages(messages: ConversationTurn[], sessionId?: string): ConversationTurn[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asRecord } from "./structured-content.js";
|
|
1
|
+
import { asRecord, isStructuredImagePart } from "./structured-content.js";
|
|
2
2
|
export const WAND_PROTOCOL_VERSION = 2;
|
|
3
3
|
function text(value) {
|
|
4
4
|
return typeof value === "string" && value ? value : undefined;
|
|
@@ -190,11 +190,54 @@ function tasksFromSegment(messages, start, end) {
|
|
|
190
190
|
targetId,
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* 把 tool_result 里的内联 base64 图片换成「按会话取图」的 URL。
|
|
195
|
+
*
|
|
196
|
+
* 为什么不能把 base64 直接下发:读一张截图动辄 1–3MB,base64 后更大,会撑爆移动端
|
|
197
|
+
* 单帧上限(iOS 默认 1MiB)导致反复断连;而且同一张图在每条快照里重复传。换成 URL
|
|
198
|
+
* 后各端用同一套带鉴权的取图通道按需加载(Web 走 <img>,原生走各自 ImageLoader)。
|
|
199
|
+
* source 本来就是 url 的图片不动,保持幂等。
|
|
200
|
+
*/
|
|
201
|
+
function rewriteInlineToolImageUrls(messages, sessionId) {
|
|
202
|
+
return messages.map((turn) => {
|
|
203
|
+
let turnChanged = false;
|
|
204
|
+
const content = turn.content.map((block) => {
|
|
205
|
+
if (block.type !== "tool_result" || !Array.isArray(block.content))
|
|
206
|
+
return block;
|
|
207
|
+
let imageIndex = -1;
|
|
208
|
+
let blockChanged = false;
|
|
209
|
+
const parts = block.content.map((part) => {
|
|
210
|
+
if (!isStructuredImagePart(part))
|
|
211
|
+
return part;
|
|
212
|
+
imageIndex += 1;
|
|
213
|
+
const source = asRecord(part.source);
|
|
214
|
+
if (source?.type !== "base64" || typeof source.data !== "string")
|
|
215
|
+
return part;
|
|
216
|
+
blockChanged = true;
|
|
217
|
+
return {
|
|
218
|
+
type: "image",
|
|
219
|
+
source: {
|
|
220
|
+
type: "url",
|
|
221
|
+
url: `/api/sessions/${encodeURIComponent(sessionId)}/tool-images/${encodeURIComponent(block.tool_use_id)}/${imageIndex}`,
|
|
222
|
+
...(typeof source.media_type === "string" ? { media_type: source.media_type } : {}),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
if (!blockChanged)
|
|
227
|
+
return block;
|
|
228
|
+
turnChanged = true;
|
|
229
|
+
return { ...block, content: parts };
|
|
230
|
+
});
|
|
231
|
+
return turnChanged ? { ...turn, content } : turn;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
193
234
|
/**
|
|
194
235
|
* Add Wand-owned semantics without mutating persisted provider blocks.
|
|
195
236
|
* This is the external interface consumed by every client.
|
|
237
|
+
*
|
|
238
|
+
* 传 sessionId 时额外把内联图片改写成取图 URL(见 rewriteInlineToolImageUrls)。
|
|
196
239
|
*/
|
|
197
|
-
export function enrichStructuredMessages(messages) {
|
|
240
|
+
export function enrichStructuredMessages(messages, sessionId) {
|
|
198
241
|
const enriched = stampDerivedSubagents(messages).map((turn) => ({
|
|
199
242
|
...turn,
|
|
200
243
|
content: turn.content.map((block) => {
|
|
@@ -231,5 +274,5 @@ export function enrichStructuredMessages(messages) {
|
|
|
231
274
|
}
|
|
232
275
|
segmentStart = i;
|
|
233
276
|
}
|
|
234
|
-
return enriched;
|
|
277
|
+
return sessionId ? rewriteInlineToolImageUrls(enriched, sessionId) : enriched;
|
|
235
278
|
}
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
/** 把 unknown 收敛成普通对象;数组 / null / 原始值都返回 null。 */
|
|
2
2
|
export declare function asRecord(value: unknown): Record<string, unknown> | null;
|
|
3
|
-
|
|
4
|
-
export declare function normalizeStructuredToolResultContent(content: unknown): string | Array<{
|
|
3
|
+
export type StructuredContentPart = {
|
|
5
4
|
type: string;
|
|
6
5
|
[key: string]: unknown;
|
|
7
|
-
}
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* 各家 provider 的图片 part 形态不同:
|
|
9
|
+
* · Anthropic:`{ type:"image", source:{ type:"base64", media_type, data } }` / `source:{ type:"url", url }`
|
|
10
|
+
* · OpenAI Responses:`{ type:"image_url", image_url:{ url } }` / `image_url:"…"`
|
|
11
|
+
* · Pi:`{ type:"image", data, mimeType }`
|
|
12
|
+
* 只要带图片语义就算,具体归一化交给 canonicalizeStructuredImagePart。
|
|
13
|
+
*/
|
|
14
|
+
export declare function isStructuredImagePart(value: unknown): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* 统一成客户端认识的规范形态(Web / Android / iOS / macOS 都按
|
|
17
|
+
* `{ type:"image", source:{ type:"base64"|"url", … } }` 解析)。识别不了返回 null,
|
|
18
|
+
* 由调用方原样保留,避免丢信息。
|
|
19
|
+
*/
|
|
20
|
+
export declare function canonicalizeStructuredImagePart(value: unknown): StructuredContentPart | null;
|
|
21
|
+
/** content(字符串 / part 数组)里是否含图片 part。 */
|
|
22
|
+
export declare function contentHasStructuredImage(content: unknown): boolean;
|
|
23
|
+
/** Preserve both Responses content parts and arbitrary structured tool output. */
|
|
24
|
+
export declare function normalizeStructuredToolResultContent(content: unknown): string | StructuredContentPart[];
|
|
@@ -4,12 +4,91 @@ export function asRecord(value) {
|
|
|
4
4
|
? value
|
|
5
5
|
: null;
|
|
6
6
|
}
|
|
7
|
+
function stringValue(value) {
|
|
8
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 各家 provider 的图片 part 形态不同:
|
|
12
|
+
* · Anthropic:`{ type:"image", source:{ type:"base64", media_type, data } }` / `source:{ type:"url", url }`
|
|
13
|
+
* · OpenAI Responses:`{ type:"image_url", image_url:{ url } }` / `image_url:"…"`
|
|
14
|
+
* · Pi:`{ type:"image", data, mimeType }`
|
|
15
|
+
* 只要带图片语义就算,具体归一化交给 canonicalizeStructuredImagePart。
|
|
16
|
+
*/
|
|
17
|
+
export function isStructuredImagePart(value) {
|
|
18
|
+
const part = asRecord(value);
|
|
19
|
+
if (!part)
|
|
20
|
+
return false;
|
|
21
|
+
if (part.type === "image_url")
|
|
22
|
+
return !!imageUrlOf(part);
|
|
23
|
+
if (part.type !== "image")
|
|
24
|
+
return false;
|
|
25
|
+
const source = asRecord(part.source);
|
|
26
|
+
if (source && (stringValue(source.data) || stringValue(source.url)))
|
|
27
|
+
return true;
|
|
28
|
+
return !!(stringValue(part.data) || stringValue(part.url));
|
|
29
|
+
}
|
|
30
|
+
function imageUrlOf(part) {
|
|
31
|
+
const raw = part.image_url;
|
|
32
|
+
if (typeof raw === "string")
|
|
33
|
+
return stringValue(raw);
|
|
34
|
+
return stringValue(asRecord(raw)?.url);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 统一成客户端认识的规范形态(Web / Android / iOS / macOS 都按
|
|
38
|
+
* `{ type:"image", source:{ type:"base64"|"url", … } }` 解析)。识别不了返回 null,
|
|
39
|
+
* 由调用方原样保留,避免丢信息。
|
|
40
|
+
*/
|
|
41
|
+
export function canonicalizeStructuredImagePart(value) {
|
|
42
|
+
const part = asRecord(value);
|
|
43
|
+
if (!part)
|
|
44
|
+
return null;
|
|
45
|
+
if (part.type === "image_url") {
|
|
46
|
+
const url = imageUrlOf(part);
|
|
47
|
+
return url ? { type: "image", source: { type: "url", url } } : null;
|
|
48
|
+
}
|
|
49
|
+
if (part.type !== "image")
|
|
50
|
+
return null;
|
|
51
|
+
const source = asRecord(part.source);
|
|
52
|
+
if (source) {
|
|
53
|
+
const data = stringValue(source.data);
|
|
54
|
+
if (source.type === "base64" && data) {
|
|
55
|
+
return {
|
|
56
|
+
type: "image",
|
|
57
|
+
source: { type: "base64", media_type: stringValue(source.media_type) ?? "image/png", data },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const url = stringValue(source.url);
|
|
61
|
+
if (url)
|
|
62
|
+
return { type: "image", source: { type: "url", url } };
|
|
63
|
+
}
|
|
64
|
+
const data = stringValue(part.data);
|
|
65
|
+
if (data) {
|
|
66
|
+
const mediaType = stringValue(part.mimeType)
|
|
67
|
+
?? stringValue(part.mime_type)
|
|
68
|
+
?? stringValue(source?.media_type)
|
|
69
|
+
?? "image/png";
|
|
70
|
+
return { type: "image", source: { type: "base64", media_type: mediaType, data } };
|
|
71
|
+
}
|
|
72
|
+
const url = stringValue(part.url);
|
|
73
|
+
if (url)
|
|
74
|
+
return { type: "image", source: { type: "url", url } };
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
/** content(字符串 / part 数组)里是否含图片 part。 */
|
|
78
|
+
export function contentHasStructuredImage(content) {
|
|
79
|
+
if (!Array.isArray(content))
|
|
80
|
+
return false;
|
|
81
|
+
return content.some((part) => isStructuredImagePart(part));
|
|
82
|
+
}
|
|
7
83
|
/** Preserve both Responses content parts and arbitrary structured tool output. */
|
|
8
84
|
export function normalizeStructuredToolResultContent(content) {
|
|
9
85
|
if (typeof content === "string")
|
|
10
86
|
return content;
|
|
11
87
|
if (Array.isArray(content)) {
|
|
12
|
-
const parts = content
|
|
88
|
+
const parts = content
|
|
89
|
+
.filter((item) => !!item && typeof item === "object" && typeof item.type === "string")
|
|
90
|
+
// 图片 part 归一化后客户端才能跨端渲染;非图片 part 原样保留。
|
|
91
|
+
.map((part) => canonicalizeStructuredImagePart(part) ?? part);
|
|
13
92
|
if (parts.length === content.length)
|
|
14
93
|
return parts;
|
|
15
94
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { startStructuredCli } from "./structured-exec-pump.js";
|
|
3
|
-
import { asRecord } from "./structured-content.js";
|
|
3
|
+
import { asRecord, normalizeStructuredToolResultContent } from "./structured-content.js";
|
|
4
4
|
import { thinkingEffortToPiLevel } from "./structured-provider-common.js";
|
|
5
5
|
export function isMissingPiSession(error, sessionId) {
|
|
6
6
|
return !!sessionId && !!error?.includes(`No session found matching '${sessionId}'`);
|
|
@@ -17,6 +17,22 @@ function textContent(value) {
|
|
|
17
17
|
return item.text;
|
|
18
18
|
return textContent(item.content);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Pi 的工具结果是 `{ content: [ text…, image… ] }`。图片 part 不能像以前那样只抽文本
|
|
22
|
+
* 丢掉——读图 / 截图全靠它,统一交给 normalizeStructuredToolResultContent 归一化。
|
|
23
|
+
* 纯文本结果仍压成字符串,保持既有形态。
|
|
24
|
+
*/
|
|
25
|
+
function piToolResultContent(value) {
|
|
26
|
+
const record = asRecord(value);
|
|
27
|
+
const raw = record && "content" in record ? record.content : value;
|
|
28
|
+
const normalized = normalizeStructuredToolResultContent(raw);
|
|
29
|
+
if (typeof normalized === "string")
|
|
30
|
+
return normalized;
|
|
31
|
+
if (normalized.length > 0 && normalized.every((part) => part.type === "text" && typeof part.text === "string")) {
|
|
32
|
+
return normalized.map((part) => part.text).filter(Boolean).join("\n");
|
|
33
|
+
}
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
20
36
|
export function buildPiArgs(session, prompt) {
|
|
21
37
|
const args = ["--mode", "json", "--print"];
|
|
22
38
|
const model = session.selectedModel?.trim();
|
|
@@ -141,7 +157,7 @@ export function applyPiEvent(state, event) {
|
|
|
141
157
|
}
|
|
142
158
|
if (event.type === "tool_execution_end") {
|
|
143
159
|
const id = typeof event.toolCallId === "string" ? event.toolCallId : "unknown";
|
|
144
|
-
state.blocks.push({ type: "tool_result", tool_use_id: id, content:
|
|
160
|
+
state.blocks.push({ type: "tool_result", tool_use_id: id, content: piToolResultContent(event.result), is_error: event.isError === true });
|
|
145
161
|
}
|
|
146
162
|
if (event.type === "message_end" || event.type === "turn_end") {
|
|
147
163
|
const message = asRecord(event.message);
|
|
@@ -207,7 +207,7 @@ function buildStructuredOutputPayload(snapshot) {
|
|
|
207
207
|
return {
|
|
208
208
|
wandProtocolVersion: WAND_PROTOCOL_VERSION,
|
|
209
209
|
output: snapshot.output,
|
|
210
|
-
messages: snapshot.messages ? enrichStructuredMessages(snapshot.messages) : undefined,
|
|
210
|
+
messages: snapshot.messages ? enrichStructuredMessages(snapshot.messages, snapshot.id) : undefined,
|
|
211
211
|
queuedMessages: snapshot.queuedMessages,
|
|
212
212
|
queuedMessageSkills: snapshot.queuedMessageSkills,
|
|
213
213
|
sessionKind: "structured",
|
|
@@ -259,7 +259,7 @@ function buildIncrementalStructuredPayload(snapshot, cardDefaults) {
|
|
|
259
259
|
const messages = snapshot.messages ?? [];
|
|
260
260
|
// Derive semantics from the complete current turn before taking the
|
|
261
261
|
// incremental tail; TaskCreate results can live in an earlier provider frame.
|
|
262
|
-
const clientMessages = enrichStructuredMessages(messages);
|
|
262
|
+
const clientMessages = enrichStructuredMessages(messages, snapshot.id);
|
|
263
263
|
const lastTurn = clientMessages.length > 0 ? clientMessages[clientMessages.length - 1] : undefined;
|
|
264
264
|
// Streaming turn (index 0 here) is preserved verbatim; truncation only kicks
|
|
265
265
|
// in if the live response is already bigger than the transport threshold,
|
package/dist/types.d.ts
CHANGED
|
@@ -65,6 +65,16 @@ export interface AndroidApkConfig {
|
|
|
65
65
|
apkDir?: string;
|
|
66
66
|
currentApkFile?: string;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* PTY 归属引擎。`auto` 优先 Rust Render(找到了二进制才用),`rust` 强制 Rust,
|
|
70
|
+
* `legacy` 保留现有 terminald 行为。仅切换所有权,DB/客户端契约不变。
|
|
71
|
+
*/
|
|
72
|
+
export type RenderEngine = "auto" | "rust" | "legacy";
|
|
73
|
+
export interface RenderConfig {
|
|
74
|
+
engine: RenderEngine;
|
|
75
|
+
/** 显式指定 wand-render 可执行文件;留空则按约定顺序自动查找。 */
|
|
76
|
+
binaryPath?: string;
|
|
77
|
+
}
|
|
68
78
|
export interface MacosDmgConfig {
|
|
69
79
|
enabled?: boolean;
|
|
70
80
|
dmgDir?: string;
|
|
@@ -123,6 +133,8 @@ export interface WandConfig {
|
|
|
123
133
|
android?: AndroidApkConfig;
|
|
124
134
|
macos?: MacosDmgConfig;
|
|
125
135
|
ios?: IosIpaConfig;
|
|
136
|
+
/** Render(常驻 PTY 持有者)引擎选择。默认 auto。 */
|
|
137
|
+
render?: RenderConfig;
|
|
126
138
|
/** Default expand/collapse state for card types in structured chat view */
|
|
127
139
|
cardDefaults?: CardExpandDefaults;
|
|
128
140
|
/** 新建会话时默认使用的 Claude 模型(别名或完整 ID)。留空则不传 --model,由 claude 自行决定。 */
|