@co0ontty/wand 1.21.12 → 1.21.13
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/server.js +254 -31
- package/dist/structured-session-manager.d.ts +6 -2
- package/dist/structured-session-manager.js +170 -97
- package/dist/types.d.ts +24 -0
- package/dist/web-ui/content/scripts.js +709 -138
- package/dist/web-ui/content/styles.css +313 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
|
+
import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
|
|
5
6
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
6
7
|
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
7
8
|
import { buildChildEnv } from "./env-utils.js";
|
|
@@ -123,6 +124,55 @@ function withSummary(snapshot) {
|
|
|
123
124
|
function shouldAutoApproveForMode(mode) {
|
|
124
125
|
return mode === "full-access" || mode === "managed" || mode === "auto-edit";
|
|
125
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Root 模式下绕过权限的工具白名单。Claude CLI 拒绝以 root 身份用 bypassPermissions,
|
|
129
|
+
* 退而求其次用 acceptEdits + 显式 allowedTools 覆盖 CWD 之外的路径。
|
|
130
|
+
*/
|
|
131
|
+
const ROOT_FALLBACK_ALLOWED_TOOLS = [
|
|
132
|
+
"Bash", "Edit", "Write", "Read", "Glob", "Grep", "NotebookEdit", "WebFetch", "WebSearch",
|
|
133
|
+
];
|
|
134
|
+
/**
|
|
135
|
+
* 把 (执行模式, 自动批准开关) 映射成 Claude CLI / SDK 的权限决策。
|
|
136
|
+
* CLI runner 把它转成 --permission-mode / --allowedTools flag,
|
|
137
|
+
* SDK runner 直接塞进 Options。两边的决策规则保持一字不差。
|
|
138
|
+
*/
|
|
139
|
+
function derivePermissionPolicy(mode, autoApprove) {
|
|
140
|
+
const shouldBypass = autoApprove || mode === "full-access" || mode === "managed";
|
|
141
|
+
const shouldAcceptEdits = mode === "auto-edit";
|
|
142
|
+
if (!isRunningAsRoot()) {
|
|
143
|
+
if (shouldBypass)
|
|
144
|
+
return { permissionMode: "bypassPermissions", allowedTools: undefined };
|
|
145
|
+
if (shouldAcceptEdits)
|
|
146
|
+
return { permissionMode: "acceptEdits", allowedTools: undefined };
|
|
147
|
+
return { permissionMode: "default", allowedTools: undefined };
|
|
148
|
+
}
|
|
149
|
+
if (shouldBypass || shouldAcceptEdits) {
|
|
150
|
+
return { permissionMode: "acceptEdits", allowedTools: ROOT_FALLBACK_ALLOWED_TOOLS };
|
|
151
|
+
}
|
|
152
|
+
return { permissionMode: "default", allowedTools: undefined };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 拼装要追加到系统提示词里的片段:托管模式的自主决策提示 + 用户配置的语言偏好。
|
|
156
|
+
* CLI runner 每段单独 push 一对 `--append-system-prompt <part>` flag,
|
|
157
|
+
* SDK runner 用 "\n\n" 串成一个 appendSystemPrompt 字符串塞 Options。
|
|
158
|
+
* 文本统一到这里维护,避免两个 runner 各抄一份导致漂移。
|
|
159
|
+
*/
|
|
160
|
+
function buildAppendSystemPromptParts(language, mode) {
|
|
161
|
+
const trimmedLanguage = language?.trim();
|
|
162
|
+
const isChinese = trimmedLanguage === "中文";
|
|
163
|
+
const parts = [];
|
|
164
|
+
if (mode === "managed") {
|
|
165
|
+
parts.push(isChinese
|
|
166
|
+
? "你正在完全托管的自主模式下运行。用户可能无法及时回复问题或确认。你必须独立做出所有决策——自行选择最佳方案,而不是向用户询问偏好、确认或澄清。如果有多种可行方案,选择你认为最合适的并继续执行。除非任务本身存在根本性的歧义且无法合理推断,否则不要等待用户输入。果断行动,自主决策。"
|
|
167
|
+
: "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.");
|
|
168
|
+
}
|
|
169
|
+
if (trimmedLanguage) {
|
|
170
|
+
parts.push(isChinese
|
|
171
|
+
? "请使用中文回复。所有解释、注释和对话文本都使用中文。"
|
|
172
|
+
: `Please respond in ${trimmedLanguage}. Use ${trimmedLanguage} for all your explanations, comments, and conversational text.`);
|
|
173
|
+
}
|
|
174
|
+
return parts;
|
|
175
|
+
}
|
|
126
176
|
function buildStructuredOutputPayload(snapshot) {
|
|
127
177
|
return {
|
|
128
178
|
output: snapshot.output,
|
|
@@ -155,6 +205,12 @@ export class StructuredSessionManager {
|
|
|
155
205
|
sessions = new Map();
|
|
156
206
|
pendingChildren = new Map();
|
|
157
207
|
pendingSdkAbort = new Map();
|
|
208
|
+
/**
|
|
209
|
+
* Active SDK Query handle per session, kept around so we can call
|
|
210
|
+
* `query.interrupt()` for a graceful stop instead of aborting via signal.
|
|
211
|
+
* Only populated while an SDK call is in flight.
|
|
212
|
+
*/
|
|
213
|
+
pendingSdkQueries = new Map();
|
|
158
214
|
interruptedWith = new Map();
|
|
159
215
|
/** Last wall-clock time (ms) we did a full saveSession for a streaming session. */
|
|
160
216
|
lastStreamSaveAt = new Map();
|
|
@@ -357,6 +413,10 @@ export class StructuredSessionManager {
|
|
|
357
413
|
child.kill("SIGTERM");
|
|
358
414
|
}
|
|
359
415
|
catch (_err) { /* ignore */ }
|
|
416
|
+
const sdkQueryHandle = this.pendingSdkQueries.get(id);
|
|
417
|
+
if (sdkQueryHandle) {
|
|
418
|
+
void sdkQueryHandle.interrupt().catch(() => { });
|
|
419
|
+
}
|
|
360
420
|
const sdkAbort = this.pendingSdkAbort.get(id);
|
|
361
421
|
if (sdkAbort)
|
|
362
422
|
sdkAbort.abort();
|
|
@@ -419,9 +479,14 @@ export class StructuredSessionManager {
|
|
|
419
479
|
sessionId: id,
|
|
420
480
|
data: { status: "running", sessionKind: "structured", queuedMessages: updated.queuedMessages, structuredState: updated.structuredState },
|
|
421
481
|
});
|
|
422
|
-
// 续接 AskUserQuestion
|
|
423
|
-
//
|
|
424
|
-
|
|
482
|
+
// 续接 AskUserQuestion 的两条不同路线:
|
|
483
|
+
// - CLI runner (`claude -p`):stdin 是 ignore,没有 tool_result 回传通道,
|
|
484
|
+
// 只能把答案当作普通文本塞回去,靠提示词让 Claude 自己脑补"这是工具回答"。
|
|
485
|
+
// - SDK runner:streaming input mode 下 prompt 是 AsyncIterable,可以把
|
|
486
|
+
// 用户答案直接 yield 成真正的 tool_result block,对 Claude 来说就是标准
|
|
487
|
+
// 的工具结果,不需要任何 hack。runner 自己从 session.messages 末尾读取
|
|
488
|
+
// 新加的 userTurn,所以传原始 prompt 即可。
|
|
489
|
+
const cliClaudePrompt = pendingAsk
|
|
425
490
|
? `[对刚才 AskUserQuestion 工具的回答 — 结构化模式不支持工具结果回传,下面是用户从选项中的选择]\n${prompt}`
|
|
426
491
|
: prompt;
|
|
427
492
|
try {
|
|
@@ -429,10 +494,10 @@ export class StructuredSessionManager {
|
|
|
429
494
|
await this.runCodexStreaming(id, updated, prompt);
|
|
430
495
|
}
|
|
431
496
|
else if (this.config.structuredRunner === "sdk") {
|
|
432
|
-
await this.runClaudeSdkStreaming(id, updated,
|
|
497
|
+
await this.runClaudeSdkStreaming(id, updated, prompt);
|
|
433
498
|
}
|
|
434
499
|
else {
|
|
435
|
-
await this.runClaudeStreaming(id, updated,
|
|
500
|
+
await this.runClaudeStreaming(id, updated, cliClaudePrompt);
|
|
436
501
|
}
|
|
437
502
|
const finished = this.requireSession(id);
|
|
438
503
|
return finished;
|
|
@@ -541,6 +606,13 @@ export class StructuredSessionManager {
|
|
|
541
606
|
child.kill();
|
|
542
607
|
this.pendingChildren.delete(id);
|
|
543
608
|
}
|
|
609
|
+
// SDK runner:先尝试 query.interrupt() 优雅停止,失败再走 abort。
|
|
610
|
+
// 两个都清掉避免后续重复操作。
|
|
611
|
+
const sdkQuery = this.pendingSdkQueries.get(id);
|
|
612
|
+
if (sdkQuery) {
|
|
613
|
+
void sdkQuery.interrupt().catch(() => { });
|
|
614
|
+
this.pendingSdkQueries.delete(id);
|
|
615
|
+
}
|
|
544
616
|
const sdkAbort = this.pendingSdkAbort.get(id);
|
|
545
617
|
if (sdkAbort) {
|
|
546
618
|
sdkAbort.abort();
|
|
@@ -569,6 +641,11 @@ export class StructuredSessionManager {
|
|
|
569
641
|
child.kill();
|
|
570
642
|
this.pendingChildren.delete(id);
|
|
571
643
|
}
|
|
644
|
+
const sdkQuery = this.pendingSdkQueries.get(id);
|
|
645
|
+
if (sdkQuery) {
|
|
646
|
+
void sdkQuery.interrupt().catch(() => { });
|
|
647
|
+
this.pendingSdkQueries.delete(id);
|
|
648
|
+
}
|
|
572
649
|
const sdkAbort = this.pendingSdkAbort.get(id);
|
|
573
650
|
if (sdkAbort) {
|
|
574
651
|
sdkAbort.abort();
|
|
@@ -700,29 +777,8 @@ export class StructuredSessionManager {
|
|
|
700
777
|
// ---------------------------------------------------------------------------
|
|
701
778
|
// CLI argument construction
|
|
702
779
|
// ---------------------------------------------------------------------------
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const shouldAcceptEdits = mode === "auto-edit";
|
|
706
|
-
if (!isRunningAsRoot()) {
|
|
707
|
-
if (shouldBypass) {
|
|
708
|
-
return ["--permission-mode", "bypassPermissions"];
|
|
709
|
-
}
|
|
710
|
-
if (shouldAcceptEdits) {
|
|
711
|
-
return ["--permission-mode", "acceptEdits"];
|
|
712
|
-
}
|
|
713
|
-
return [];
|
|
714
|
-
}
|
|
715
|
-
// Root: Claude CLI refuses bypassPermissions.
|
|
716
|
-
// acceptEdits auto-approves within CWD; --allowedTools extends to all paths.
|
|
717
|
-
if (shouldBypass || shouldAcceptEdits) {
|
|
718
|
-
return [
|
|
719
|
-
"--permission-mode", "acceptEdits",
|
|
720
|
-
"--allowedTools", "Bash", "Edit", "Write", "Read", "Glob", "Grep",
|
|
721
|
-
"NotebookEdit", "WebFetch", "WebSearch",
|
|
722
|
-
];
|
|
723
|
-
}
|
|
724
|
-
return [];
|
|
725
|
-
}
|
|
780
|
+
// claude CLI 的权限/系统提示 flag 由模块级 derivePermissionPolicy() +
|
|
781
|
+
// buildAppendSystemPromptParts() 派生,定义在文件顶部,与 SDK runner 共用。
|
|
726
782
|
buildCodexArgs(session) {
|
|
727
783
|
const args = ["exec", "--json", "--color", "never"];
|
|
728
784
|
const shouldBypass = session.autoApprovePermissions === true || session.mode === "full-access" || session.mode === "managed";
|
|
@@ -1048,23 +1104,21 @@ export class StructuredSessionManager {
|
|
|
1048
1104
|
runClaudeStreaming(sessionId, session, prompt) {
|
|
1049
1105
|
return new Promise((resolve, reject) => {
|
|
1050
1106
|
const args = ["-p", "--verbose", "--output-format", "stream-json"];
|
|
1051
|
-
//
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
//
|
|
1055
|
-
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1107
|
+
// 权限策略:决策规则与 SDK runner 共享 derivePermissionPolicy(),CLI 这边把
|
|
1108
|
+
// 结果转成对应的 flag。--allowedTools 是 commander 的 variadic(<tools...>),
|
|
1109
|
+
// 紧跟其后的所有非 flag 形 token 都会被吞进工具列表,因此后面任何位置参数
|
|
1110
|
+
// 都得是 -- 开头的 flag——下面追加 --append-system-prompt / --model / --resume
|
|
1111
|
+
// 都满足这个条件。
|
|
1112
|
+
const permPolicy = derivePermissionPolicy(session.mode, session.autoApprovePermissions ?? false);
|
|
1113
|
+
if (permPolicy.permissionMode !== "default") {
|
|
1114
|
+
args.push("--permission-mode", permPolicy.permissionMode);
|
|
1115
|
+
}
|
|
1116
|
+
if (permPolicy.allowedTools) {
|
|
1117
|
+
args.push("--allowedTools", ...permPolicy.allowedTools);
|
|
1062
1118
|
}
|
|
1063
|
-
//
|
|
1064
|
-
|
|
1065
|
-
args.push("--append-system-prompt",
|
|
1066
|
-
? "请使用中文回复。所有解释、注释和对话文本都使用中文。"
|
|
1067
|
-
: `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`);
|
|
1119
|
+
// 追加系统提示词(托管模式自主决策 + 语言偏好),文本与 SDK runner 共享。
|
|
1120
|
+
for (const part of buildAppendSystemPromptParts(this.config.language, session.mode)) {
|
|
1121
|
+
args.push("--append-system-prompt", part);
|
|
1068
1122
|
}
|
|
1069
1123
|
const modelChoice = session.selectedModel?.trim();
|
|
1070
1124
|
if (modelChoice && modelChoice !== "default") {
|
|
@@ -1485,56 +1539,14 @@ export class StructuredSessionManager {
|
|
|
1485
1539
|
* payloads for incremental text/thinking/tool_use updates, followed by a final
|
|
1486
1540
|
* SDKAssistantMessage with the authoritative complete content.
|
|
1487
1541
|
*/
|
|
1488
|
-
runClaudeSdkStreaming(sessionId, session, prompt) {
|
|
1489
|
-
return new Promise((resolve, reject) => {
|
|
1490
|
-
void this._runClaudeSdkStreamingAsync(sessionId, session, prompt).then(resolve, reject);
|
|
1491
|
-
});
|
|
1492
|
-
}
|
|
1493
|
-
async _runClaudeSdkStreamingAsync(sessionId, session, prompt) {
|
|
1494
|
-
let sdkQuery;
|
|
1495
|
-
try {
|
|
1496
|
-
const sdkMod = await import("@anthropic-ai/claude-agent-sdk");
|
|
1497
|
-
sdkQuery = sdkMod.query;
|
|
1498
|
-
}
|
|
1499
|
-
catch {
|
|
1500
|
-
throw new Error("@anthropic-ai/claude-agent-sdk 未安装,无法使用 SDK runner。");
|
|
1501
|
-
}
|
|
1542
|
+
async runClaudeSdkStreaming(sessionId, session, prompt) {
|
|
1502
1543
|
const abortController = new AbortController();
|
|
1503
1544
|
this.pendingSdkAbort.set(sessionId, abortController);
|
|
1504
1545
|
const isManaged = session.mode === "managed";
|
|
1505
1546
|
let killedForAskUserQuestion = false;
|
|
1506
|
-
//
|
|
1507
|
-
const
|
|
1508
|
-
const
|
|
1509
|
-
let permissionMode = "default";
|
|
1510
|
-
let allowedToolsForRoot;
|
|
1511
|
-
if (!isRunningAsRoot()) {
|
|
1512
|
-
if (shouldBypass)
|
|
1513
|
-
permissionMode = "bypassPermissions";
|
|
1514
|
-
else if (shouldAcceptEdits)
|
|
1515
|
-
permissionMode = "acceptEdits";
|
|
1516
|
-
}
|
|
1517
|
-
else {
|
|
1518
|
-
// Root: acceptEdits + allowedTools (same workaround as CLI runner)
|
|
1519
|
-
if (shouldBypass || shouldAcceptEdits) {
|
|
1520
|
-
permissionMode = "acceptEdits";
|
|
1521
|
-
allowedToolsForRoot = ["Bash", "Edit", "Write", "Read", "Glob", "Grep", "NotebookEdit", "WebFetch", "WebSearch"];
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
// System prompt additions
|
|
1525
|
-
const isChinese = this.config.language?.trim() === "中文";
|
|
1526
|
-
const systemPromptParts = [];
|
|
1527
|
-
if (isManaged) {
|
|
1528
|
-
systemPromptParts.push(isChinese
|
|
1529
|
-
? "你正在完全托管的自主模式下运行。用户可能无法及时回复问题或确认。你必须独立做出所有决策——自行选择最佳方案,而不是向用户询问偏好、确认或澄清。如果有多种可行方案,选择你认为最合适的并继续执行。除非任务本身存在根本性的歧义且无法合理推断,否则不要等待用户输入。果断行动,自主决策。"
|
|
1530
|
-
: "You are running in a fully managed, autonomous mode. The user may not be available to respond to questions or confirmations in a timely manner. You MUST make all decisions independently — choose the best approach yourself instead of asking the user for preferences, confirmations, or clarifications. If multiple approaches are viable, pick the one you judge most appropriate and proceed. Never block on user input unless the task is fundamentally ambiguous and cannot be reasonably inferred. Be decisive and self-directed.");
|
|
1531
|
-
}
|
|
1532
|
-
const language = this.config.language?.trim();
|
|
1533
|
-
if (language) {
|
|
1534
|
-
systemPromptParts.push(isChinese
|
|
1535
|
-
? "请使用中文回复。所有解释、注释和对话文本都使用中文。"
|
|
1536
|
-
: `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`);
|
|
1537
|
-
}
|
|
1547
|
+
// 权限策略 + 系统提示词都通过共享 helper 派生,与 CLI runner 一字不差。
|
|
1548
|
+
const permPolicy = derivePermissionPolicy(session.mode, session.autoApprovePermissions ?? false);
|
|
1549
|
+
const systemPromptParts = buildAppendSystemPromptParts(this.config.language, session.mode);
|
|
1538
1550
|
const sdkClaudeBinary = resolveSdkClaudeBinary();
|
|
1539
1551
|
// SDK 默认会把整个 process.env 透传给 claude 子进程;这里显式按 inheritEnv 配置组装,
|
|
1540
1552
|
// 否则关闭"继承环境变量"开关时 SDK 路径会被静默忽略。
|
|
@@ -1543,9 +1555,9 @@ export class StructuredSessionManager {
|
|
|
1543
1555
|
cwd: session.cwd,
|
|
1544
1556
|
abortController,
|
|
1545
1557
|
env: sdkEnv,
|
|
1546
|
-
permissionMode,
|
|
1547
|
-
...(permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
|
|
1548
|
-
...(
|
|
1558
|
+
permissionMode: permPolicy.permissionMode,
|
|
1559
|
+
...(permPolicy.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
|
|
1560
|
+
...(permPolicy.allowedTools ? { allowedTools: permPolicy.allowedTools } : {}),
|
|
1549
1561
|
...(isManaged ? { disallowedTools: ["AskUserQuestion"] } : {}),
|
|
1550
1562
|
includePartialMessages: true,
|
|
1551
1563
|
...(systemPromptParts.length > 0 ? { appendSystemPrompt: systemPromptParts.join("\n\n") } : {}),
|
|
@@ -1556,6 +1568,49 @@ export class StructuredSessionManager {
|
|
|
1556
1568
|
const modelChoice = session.selectedModel?.trim();
|
|
1557
1569
|
if (modelChoice && modelChoice !== "default")
|
|
1558
1570
|
sdkOptions.model = modelChoice;
|
|
1571
|
+
// Streaming input mode:把这一轮的 user turn 重建成一条 SDKUserMessage 喂给 SDK。
|
|
1572
|
+
// 上层 sendMessage 已经把 userTurn 写进 session.messages 末尾——如果它的内容是
|
|
1573
|
+
// tool_result,说明本次是用户在回答上一轮 AskUserQuestion,否则就是普通文本。
|
|
1574
|
+
// 走 streaming input 而非 string prompt 的好处:tool_result 是真的 tool_result
|
|
1575
|
+
// block,对 Claude 来说就是标准工具回传,不需要 "[对刚才工具的回答…]" 这种文本
|
|
1576
|
+
// 提示让模型脑补语义。
|
|
1577
|
+
const lastUserTurn = (session.messages ?? []).slice().reverse().find((m) => m.role === "user");
|
|
1578
|
+
const lastUserBlock = lastUserTurn?.content?.[0];
|
|
1579
|
+
let sdkInitialMessage;
|
|
1580
|
+
if (lastUserBlock?.type === "tool_result") {
|
|
1581
|
+
// Anthropic 的 tool_result.content 原生支持 string 或 content-block 数组(text/image
|
|
1582
|
+
// 等)。wand 内部 ToolResultBlock 的 array 形态是 `{type: string; ...}` 比官方 union
|
|
1583
|
+
// 宽,但实际取值都是 `{type: "text", text}`,结构上兼容;用 `as` 把宽类型缩到 SDK
|
|
1584
|
+
// 接受的形态即可,比 JSON.stringify 把数组拍成一坨 JSON 文本更忠实。
|
|
1585
|
+
sdkInitialMessage = {
|
|
1586
|
+
type: "user",
|
|
1587
|
+
message: {
|
|
1588
|
+
role: "user",
|
|
1589
|
+
content: [
|
|
1590
|
+
{
|
|
1591
|
+
type: "tool_result",
|
|
1592
|
+
tool_use_id: lastUserBlock.tool_use_id,
|
|
1593
|
+
content: lastUserBlock.content,
|
|
1594
|
+
is_error: lastUserBlock.is_error === true,
|
|
1595
|
+
},
|
|
1596
|
+
],
|
|
1597
|
+
},
|
|
1598
|
+
parent_tool_use_id: null,
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
else {
|
|
1602
|
+
sdkInitialMessage = {
|
|
1603
|
+
type: "user",
|
|
1604
|
+
message: {
|
|
1605
|
+
role: "user",
|
|
1606
|
+
content: [{ type: "text", text: prompt }],
|
|
1607
|
+
},
|
|
1608
|
+
parent_tool_use_id: null,
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
async function* singleShotPrompt() {
|
|
1612
|
+
yield sdkInitialMessage;
|
|
1613
|
+
}
|
|
1559
1614
|
const turnState = {
|
|
1560
1615
|
blocks: [],
|
|
1561
1616
|
result: "",
|
|
@@ -1647,14 +1702,16 @@ export class StructuredSessionManager {
|
|
|
1647
1702
|
kind: "claude-sdk",
|
|
1648
1703
|
provider: "claude",
|
|
1649
1704
|
cwd: session.cwd,
|
|
1650
|
-
permissionMode,
|
|
1705
|
+
permissionMode: permPolicy.permissionMode,
|
|
1651
1706
|
prompt: prompt.slice(0, 2048),
|
|
1652
1707
|
promptLength: prompt.length,
|
|
1653
1708
|
claudeSessionId: session.claudeSessionId,
|
|
1654
1709
|
spawnedAt,
|
|
1655
1710
|
});
|
|
1711
|
+
const queryHandle = sdkQuery({ prompt: singleShotPrompt(), options: sdkOptions });
|
|
1712
|
+
this.pendingSdkQueries.set(sessionId, queryHandle);
|
|
1656
1713
|
try {
|
|
1657
|
-
for await (const msg of
|
|
1714
|
+
for await (const msg of queryHandle) {
|
|
1658
1715
|
if (abortController.signal.aborted)
|
|
1659
1716
|
break;
|
|
1660
1717
|
// Incremental streaming events (opt-in via includePartialMessages: true)
|
|
@@ -1721,13 +1778,27 @@ export class StructuredSessionManager {
|
|
|
1721
1778
|
turnState.sessionId = assistantMsg.session_id;
|
|
1722
1779
|
syncSnapshot();
|
|
1723
1780
|
scheduleEmit();
|
|
1724
|
-
// Non-managed mode: detect AskUserQuestion
|
|
1781
|
+
// Non-managed mode: detect AskUserQuestion. Prefer query.interrupt()
|
|
1782
|
+
// (streaming input mode 的 control message,让 SDK 优雅地停掉当前 turn)
|
|
1783
|
+
// 而不是 abortController.abort()——abort 会让 SDK throw AbortError,整段
|
|
1784
|
+
// try/catch 走异常路径;interrupt 让 for-await 自然结束,行为更干净。
|
|
1785
|
+
// 失败时 fallback 到 abort,保证一定能跳出。
|
|
1786
|
+
//
|
|
1787
|
+
// 注意:interrupt 之后下一次 sendMessage 会重新 spawn 一次 SDK 调用并通过
|
|
1788
|
+
// resume 续接 + tool_result block 回答,不用文本伪造。
|
|
1725
1789
|
if (!isManaged && !killedForAskUserQuestion) {
|
|
1726
1790
|
const askBlock = extracted.content.find((b) => b.type === "tool_use" && b.name === "AskUserQuestion");
|
|
1727
1791
|
if (askBlock) {
|
|
1728
1792
|
killedForAskUserQuestion = true;
|
|
1729
1793
|
flushEmit();
|
|
1730
|
-
|
|
1794
|
+
try {
|
|
1795
|
+
await queryHandle.interrupt();
|
|
1796
|
+
}
|
|
1797
|
+
catch (_err) {
|
|
1798
|
+
// interrupt 在某些情况下(已经结束 / SDK 版本不支持)会 reject,
|
|
1799
|
+
// 兜底用 abort 强制退出。
|
|
1800
|
+
abortController.abort();
|
|
1801
|
+
}
|
|
1731
1802
|
}
|
|
1732
1803
|
}
|
|
1733
1804
|
continue;
|
|
@@ -1773,6 +1844,7 @@ export class StructuredSessionManager {
|
|
|
1773
1844
|
const isAbort = abortController.signal.aborted || (err instanceof Error && err.name === "AbortError");
|
|
1774
1845
|
if (!isAbort) {
|
|
1775
1846
|
this.pendingSdkAbort.delete(sessionId);
|
|
1847
|
+
this.pendingSdkQueries.delete(sessionId);
|
|
1776
1848
|
this.lastStreamSaveAt.delete(sessionId);
|
|
1777
1849
|
if (emitTimer)
|
|
1778
1850
|
clearTimeout(emitTimer);
|
|
@@ -1787,6 +1859,7 @@ export class StructuredSessionManager {
|
|
|
1787
1859
|
}
|
|
1788
1860
|
// Cleanup
|
|
1789
1861
|
this.pendingSdkAbort.delete(sessionId);
|
|
1862
|
+
this.pendingSdkQueries.delete(sessionId);
|
|
1790
1863
|
this.lastStreamSaveAt.delete(sessionId);
|
|
1791
1864
|
if (emitTimer)
|
|
1792
1865
|
clearTimeout(emitTimer);
|
package/dist/types.d.ts
CHANGED
|
@@ -232,6 +232,30 @@ export interface FileEntry {
|
|
|
232
232
|
name: string;
|
|
233
233
|
type: 'dir' | 'file';
|
|
234
234
|
gitStatus?: GitFileStatus;
|
|
235
|
+
/** File size in bytes; absent for directories. */
|
|
236
|
+
size?: number;
|
|
237
|
+
/** ISO timestamp of the last modification. */
|
|
238
|
+
mtime?: string;
|
|
239
|
+
}
|
|
240
|
+
export interface DirectoryListing {
|
|
241
|
+
items: FileEntry[];
|
|
242
|
+
/** True when the result was capped before all entries were returned. */
|
|
243
|
+
truncated: boolean;
|
|
244
|
+
/** Total number of entries in the directory (before truncation). */
|
|
245
|
+
total: number;
|
|
246
|
+
}
|
|
247
|
+
export type FilePreviewKind = "text" | "image" | "pdf" | "video" | "audio" | "binary";
|
|
248
|
+
export interface FilePreviewResponse {
|
|
249
|
+
kind: FilePreviewKind;
|
|
250
|
+
path: string;
|
|
251
|
+
name: string;
|
|
252
|
+
ext: string;
|
|
253
|
+
size: number;
|
|
254
|
+
mime?: string;
|
|
255
|
+
/** Detected language for text/code; only present when kind === "text". */
|
|
256
|
+
lang?: string;
|
|
257
|
+
/** File content; only present when kind === "text". */
|
|
258
|
+
content?: string;
|
|
235
259
|
}
|
|
236
260
|
export interface ChatMessage {
|
|
237
261
|
role: "user" | "assistant";
|