@co0ontty/wand 1.30.3 → 1.31.1
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/README.md +39 -2
- package/dist/claude-sdk-runner.d.ts +31 -0
- package/dist/claude-sdk-runner.js +142 -0
- package/dist/cli.js +104 -0
- package/dist/git-quick-commit.js +18 -26
- package/dist/process-manager.d.ts +7 -0
- package/dist/process-manager.js +26 -2
- package/dist/prompt-optimizer.js +17 -26
- package/dist/server-session-routes.js +72 -3
- package/dist/server.js +1 -0
- package/dist/structured-session-manager.d.ts +24 -0
- package/dist/structured-session-manager.js +106 -7
- package/dist/tui/attach.js +7 -8
- package/dist/tui/commands.d.ts +24 -7
- package/dist/tui/commands.js +200 -86
- package/dist/tui/index.js +8 -8
- package/dist/tui/service-panel.js +3 -4
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +927 -81
- package/dist/web-ui/content/styles.css +986 -141
- package/package.json +1 -1
|
@@ -144,7 +144,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
144
144
|
});
|
|
145
145
|
app.post("/api/structured-sessions", express.json(), async (req, res) => {
|
|
146
146
|
const body = req.body;
|
|
147
|
-
console.log("[WAND] POST /api/structured-sessions body:", JSON.stringify({ cwd: body.cwd, mode: body.mode, runner: body.runner, provider: body.provider, worktreeEnabled: body.worktreeEnabled === true, hasPrompt: !!body.prompt, model: body.model }));
|
|
147
|
+
console.log("[WAND] POST /api/structured-sessions body:", JSON.stringify({ cwd: body.cwd, mode: body.mode, runner: body.runner, provider: body.provider, worktreeEnabled: body.worktreeEnabled === true, hasPrompt: !!body.prompt, model: body.model, thinkingEffort: body.thinkingEffort }));
|
|
148
148
|
try {
|
|
149
149
|
if (body.provider && body.provider !== "claude" && body.provider !== "codex") {
|
|
150
150
|
res.status(400).json({ error: "结构化会话当前仅支持 Claude 或 Codex provider。" });
|
|
@@ -158,6 +158,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
158
158
|
runner: body.runner ?? (provider === "codex" ? "codex-cli-exec" : "claude-cli-print"),
|
|
159
159
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
160
160
|
model: typeof body.model === "string" ? body.model.trim() : undefined,
|
|
161
|
+
thinkingEffort: typeof body.thinkingEffort === "string"
|
|
162
|
+
? body.thinkingEffort
|
|
163
|
+
: undefined,
|
|
161
164
|
});
|
|
162
165
|
console.log("[WAND] structured session created:", JSON.stringify({ id: snapshot.id, sessionKind: snapshot.sessionKind, runner: snapshot.runner, status: snapshot.status }));
|
|
163
166
|
onSessionCreated?.(body.cwd ?? snapshot.cwd);
|
|
@@ -196,6 +199,29 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
196
199
|
res.status(400).json({ error: getErrorMessage(error, "切换模型失败。") });
|
|
197
200
|
}
|
|
198
201
|
});
|
|
202
|
+
// 思考深度切换:与 /model 路由对称。结构化会话立即影响下一条 prompt(CLI/SDK 各自接入
|
|
203
|
+
// applyThinkingEffortToPrompt / thinking budget),PTY 会话仅影响通过 chat 视图发送的输入。
|
|
204
|
+
app.post("/api/sessions/:id/thinking-effort", express.json(), (req, res) => {
|
|
205
|
+
const body = req.body;
|
|
206
|
+
const raw = typeof body?.thinkingEffort === "string" ? body.thinkingEffort : null;
|
|
207
|
+
const id = req.params.id;
|
|
208
|
+
try {
|
|
209
|
+
if (structured.get(id)) {
|
|
210
|
+
const updated = structured.setSessionThinkingEffort(id, raw);
|
|
211
|
+
res.json(updated);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!processes.get(id)) {
|
|
215
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const updated = processes.setSessionThinkingEffort(id, raw);
|
|
219
|
+
res.json(updated);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
res.status(400).json({ error: getErrorMessage(error, "切换思考深度失败。") });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
199
225
|
app.get("/api/structured-sessions/:id/messages", (req, res) => {
|
|
200
226
|
const snapshot = structured.get(req.params.id);
|
|
201
227
|
if (!snapshot) {
|
|
@@ -207,10 +233,13 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
207
233
|
app.post("/api/structured-sessions/:id/messages", express.json(), async (req, res) => {
|
|
208
234
|
const input = String(req.body?.input ?? "");
|
|
209
235
|
const interrupt = !!req.body?.interrupt;
|
|
236
|
+
// preserveQueue: 仅在 interrupt 路径有意义。排队条「立即」会带这个 flag,
|
|
237
|
+
// 让退出 handler 不要把剩余 queuedMessages 清空(默认行为是清空)。
|
|
238
|
+
const preserveQueue = !!req.body?.preserveQueue;
|
|
210
239
|
const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
|
|
211
|
-
console.log("[WAND] POST /api/structured-sessions/:id/messages id:", req.params.id, "input:", input.substring(0, 50), "interrupt:", interrupt, "idempotencyKey:", idempotencyKey);
|
|
240
|
+
console.log("[WAND] POST /api/structured-sessions/:id/messages id:", req.params.id, "input:", input.substring(0, 50), "interrupt:", interrupt, "preserveQueue:", preserveQueue, "idempotencyKey:", idempotencyKey);
|
|
212
241
|
try {
|
|
213
|
-
const snapshot = await structured.sendMessage(req.params.id, input, { interrupt, idempotencyKey });
|
|
242
|
+
const snapshot = await structured.sendMessage(req.params.id, input, { interrupt, preserveQueue, idempotencyKey });
|
|
214
243
|
res.json(snapshot);
|
|
215
244
|
}
|
|
216
245
|
catch (error) {
|
|
@@ -222,6 +251,46 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
222
251
|
});
|
|
223
252
|
}
|
|
224
253
|
});
|
|
254
|
+
// ── Structured queued-messages management ──
|
|
255
|
+
// 三个端点构成"排队消息条"的后端操作面:reorder(拖拽换序)、单条删除、全部清空。
|
|
256
|
+
// 全部走乐观更新模型,失败时前端会回滚到上一次 WS 推送的 queuedMessages。
|
|
257
|
+
app.patch("/api/structured-sessions/:id/queued", express.json(), (req, res) => {
|
|
258
|
+
const rawOrder = req.body?.order;
|
|
259
|
+
if (!Array.isArray(rawOrder)) {
|
|
260
|
+
res.status(400).json({ error: "缺少 order 数组。" });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const snapshot = structured.reorderQueuedMessages(req.params.id, rawOrder.map((v) => Number(v)));
|
|
265
|
+
res.json(snapshot);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
res.status(400).json({ error: getErrorMessage(error, "无法调整排队顺序。") });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
app.delete("/api/structured-sessions/:id/queued/:index", (req, res) => {
|
|
272
|
+
const index = Number(req.params.index);
|
|
273
|
+
if (!Number.isFinite(index)) {
|
|
274
|
+
res.status(400).json({ error: "下标无效。" });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const snapshot = structured.deleteQueuedMessage(req.params.id, index);
|
|
279
|
+
res.json(snapshot);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
res.status(400).json({ error: getErrorMessage(error, "无法删除排队消息。") });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
app.delete("/api/structured-sessions/:id/queued", (req, res) => {
|
|
286
|
+
try {
|
|
287
|
+
const snapshot = structured.clearQueuedMessages(req.params.id);
|
|
288
|
+
res.json(snapshot);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
res.status(400).json({ error: getErrorMessage(error, "无法清空排队消息。") });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
225
294
|
// ── Tool content lazy-load endpoint ──
|
|
226
295
|
app.get("/api/sessions/:id/tool-content/:toolUseId", (req, res) => {
|
|
227
296
|
const snapshot = getSessionById(processes, structured, req.params.id);
|
package/dist/server.js
CHANGED
|
@@ -1813,6 +1813,7 @@ export async function startServer(config, configPath) {
|
|
|
1813
1813
|
model: effectiveModel,
|
|
1814
1814
|
cols: reqCols,
|
|
1815
1815
|
rows: reqRows,
|
|
1816
|
+
thinkingEffort: body.thinkingEffort ?? undefined,
|
|
1816
1817
|
});
|
|
1817
1818
|
recordRecentPath(storage, body.cwd ?? snapshot.cwd);
|
|
1818
1819
|
res.status(201).json(snapshot);
|
|
@@ -10,6 +10,8 @@ interface CreateStructuredSessionOptions {
|
|
|
10
10
|
worktreeEnabled?: boolean;
|
|
11
11
|
/** 用户指定的 Claude 模型(别名或完整 ID)。留空则 spawn 时不加 --model。 */
|
|
12
12
|
model?: string;
|
|
13
|
+
/** 用户预设的思考深度。留空 / null 视为 off。 */
|
|
14
|
+
thinkingEffort?: SessionSnapshot["thinkingEffort"];
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* 把任意外部输入收敛到合法的 thinkingEffort 枚举值。`null` / 非法值都视为
|
|
@@ -39,6 +41,14 @@ export declare class StructuredSessionManager {
|
|
|
39
41
|
*/
|
|
40
42
|
private readonly pendingSdkQueries;
|
|
41
43
|
private readonly interruptedWith;
|
|
44
|
+
/**
|
|
45
|
+
* Sessions where the current interrupt is a "queue promote" (用户从排队条点了「立即」
|
|
46
|
+
* 把队首插队到 now)。退出处理三个分支默认会把 queuedMessages 清空——因为常规的
|
|
47
|
+
* interrupt 语义是"算了,做这个",把队列也作废。但 queue-promote 的语义是
|
|
48
|
+
* "先做这条,剩下的队列还要继续",所以这里打个标记,让退出 handler 保留 queue。
|
|
49
|
+
* 收到后必须 delete 掉,避免下一次普通 interrupt 误带 flag。
|
|
50
|
+
*/
|
|
51
|
+
private readonly preserveQueueOnInterrupt;
|
|
42
52
|
/** Last wall-clock time (ms) we did a full saveSession for a streaming session. */
|
|
43
53
|
private readonly lastStreamSaveAt;
|
|
44
54
|
/**
|
|
@@ -69,11 +79,25 @@ export declare class StructuredSessionManager {
|
|
|
69
79
|
sendMessage(id: string, input: string, opts?: {
|
|
70
80
|
interrupt?: boolean;
|
|
71
81
|
idempotencyKey?: string;
|
|
82
|
+
preserveQueue?: boolean;
|
|
72
83
|
}): Promise<SessionSnapshot>;
|
|
73
84
|
/** Approve a pending permission request. */
|
|
74
85
|
approvePermission(sessionId: string): SessionSnapshot;
|
|
75
86
|
/** Deny a pending permission request. */
|
|
76
87
|
denyPermission(sessionId: string): SessionSnapshot;
|
|
88
|
+
/**
|
|
89
|
+
* Reorder the pending queued messages. `order` is a permutation of the current
|
|
90
|
+
* indices, e.g. `[2, 0, 1]` means "move the third queued message to the front,
|
|
91
|
+
* push the original first to position #2". Throws if the permutation is
|
|
92
|
+
* malformed (length mismatch / duplicate / out-of-range). 不允许在 inFlight
|
|
93
|
+
* 期间改"已经被 flushNextQueuedMessage 拿走的队首",但本方法只动 queue 数组
|
|
94
|
+
* 本身,flushNext 在另一段时序里读 sessions.get(...) 当前快照,已经天然安全。
|
|
95
|
+
*/
|
|
96
|
+
reorderQueuedMessages(sessionId: string, order: number[]): SessionSnapshot;
|
|
97
|
+
/** Remove a single queued message by index. */
|
|
98
|
+
deleteQueuedMessage(sessionId: string, index: number): SessionSnapshot;
|
|
99
|
+
/** Clear all queued messages. No-op when queue is already empty. */
|
|
100
|
+
clearQueuedMessages(sessionId: string): SessionSnapshot;
|
|
77
101
|
/** Update the selected model for a structured session. Takes effect on the next spawn. */
|
|
78
102
|
setSessionModel(sessionId: string, model: string | null): SessionSnapshot;
|
|
79
103
|
/**
|
|
@@ -390,8 +390,8 @@ function buildAppendSystemPromptParts(language, mode) {
|
|
|
390
390
|
}
|
|
391
391
|
if (trimmedLanguage) {
|
|
392
392
|
parts.push(isChinese
|
|
393
|
-
? "
|
|
394
|
-
: `Please respond in ${trimmedLanguage}. Use ${trimmedLanguage} for all your explanations, comments, and conversational text.`);
|
|
393
|
+
? "请使用中文回复。所有解释、注释和对话文本都使用中文。当你通过 Task 工具派发子代理(subagent)时,必须在传给子代理的 prompt 里明确要求它也使用中文回复,确保所有子代理的输出同样遵循该语言偏好。"
|
|
394
|
+
: `Please respond in ${trimmedLanguage}. Use ${trimmedLanguage} for all your explanations, comments, and conversational text. When you dispatch a subagent via the Task tool, you MUST explicitly instruct the subagent in its prompt to also respond in ${trimmedLanguage}, so every subagent's output follows the same language preference.`);
|
|
395
395
|
}
|
|
396
396
|
return parts;
|
|
397
397
|
}
|
|
@@ -434,6 +434,14 @@ export class StructuredSessionManager {
|
|
|
434
434
|
*/
|
|
435
435
|
pendingSdkQueries = new Map();
|
|
436
436
|
interruptedWith = new Map();
|
|
437
|
+
/**
|
|
438
|
+
* Sessions where the current interrupt is a "queue promote" (用户从排队条点了「立即」
|
|
439
|
+
* 把队首插队到 now)。退出处理三个分支默认会把 queuedMessages 清空——因为常规的
|
|
440
|
+
* interrupt 语义是"算了,做这个",把队列也作废。但 queue-promote 的语义是
|
|
441
|
+
* "先做这条,剩下的队列还要继续",所以这里打个标记,让退出 handler 保留 queue。
|
|
442
|
+
* 收到后必须 delete 掉,避免下一次普通 interrupt 误带 flag。
|
|
443
|
+
*/
|
|
444
|
+
preserveQueueOnInterrupt = new Set();
|
|
437
445
|
/** Last wall-clock time (ms) we did a full saveSession for a streaming session. */
|
|
438
446
|
lastStreamSaveAt = new Map();
|
|
439
447
|
/**
|
|
@@ -549,6 +557,7 @@ export class StructuredSessionManager {
|
|
|
549
557
|
? prepareSessionWorktree({ cwd: options.cwd, sessionId: id })
|
|
550
558
|
: null;
|
|
551
559
|
const selectedModel = options.model?.trim() || null;
|
|
560
|
+
const initialThinkingEffort = normalizeThinkingEffort(options.thinkingEffort);
|
|
552
561
|
const snapshot = {
|
|
553
562
|
id,
|
|
554
563
|
sessionKind: "structured",
|
|
@@ -585,6 +594,7 @@ export class StructuredSessionManager {
|
|
|
585
594
|
autoApprovePermissions: shouldAutoApproveForMode(options.mode),
|
|
586
595
|
approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
|
|
587
596
|
selectedModel,
|
|
597
|
+
thinkingEffort: initialThinkingEffort,
|
|
588
598
|
};
|
|
589
599
|
this.sessions.set(id, snapshot);
|
|
590
600
|
this.storage.saveSession(snapshot);
|
|
@@ -635,6 +645,12 @@ export class StructuredSessionManager {
|
|
|
635
645
|
}
|
|
636
646
|
else if (opts?.interrupt) {
|
|
637
647
|
this.interruptedWith.set(id, prompt);
|
|
648
|
+
if (opts.preserveQueue) {
|
|
649
|
+
this.preserveQueueOnInterrupt.add(id);
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
this.preserveQueueOnInterrupt.delete(id);
|
|
653
|
+
}
|
|
638
654
|
try {
|
|
639
655
|
child.kill("SIGTERM");
|
|
640
656
|
}
|
|
@@ -767,6 +783,60 @@ export class StructuredSessionManager {
|
|
|
767
783
|
denyPermission(sessionId) {
|
|
768
784
|
return this.resolvePermission(sessionId, false);
|
|
769
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* Reorder the pending queued messages. `order` is a permutation of the current
|
|
788
|
+
* indices, e.g. `[2, 0, 1]` means "move the third queued message to the front,
|
|
789
|
+
* push the original first to position #2". Throws if the permutation is
|
|
790
|
+
* malformed (length mismatch / duplicate / out-of-range). 不允许在 inFlight
|
|
791
|
+
* 期间改"已经被 flushNextQueuedMessage 拿走的队首",但本方法只动 queue 数组
|
|
792
|
+
* 本身,flushNext 在另一段时序里读 sessions.get(...) 当前快照,已经天然安全。
|
|
793
|
+
*/
|
|
794
|
+
reorderQueuedMessages(sessionId, order) {
|
|
795
|
+
const session = this.requireSession(sessionId);
|
|
796
|
+
const queue = session.queuedMessages ?? [];
|
|
797
|
+
if (!Array.isArray(order) || order.length !== queue.length) {
|
|
798
|
+
throw new Error("排序长度与当前队列不一致,请刷新后重试。");
|
|
799
|
+
}
|
|
800
|
+
const seen = new Set();
|
|
801
|
+
for (const idx of order) {
|
|
802
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= queue.length || seen.has(idx)) {
|
|
803
|
+
throw new Error("排序参数无效。");
|
|
804
|
+
}
|
|
805
|
+
seen.add(idx);
|
|
806
|
+
}
|
|
807
|
+
const reordered = order.map((idx) => queue[idx]);
|
|
808
|
+
const updated = { ...session, queuedMessages: reordered };
|
|
809
|
+
this.sessions.set(sessionId, updated);
|
|
810
|
+
this.storage.saveSession(updated);
|
|
811
|
+
this.emitStructuredSnapshot(updated);
|
|
812
|
+
return updated;
|
|
813
|
+
}
|
|
814
|
+
/** Remove a single queued message by index. */
|
|
815
|
+
deleteQueuedMessage(sessionId, index) {
|
|
816
|
+
const session = this.requireSession(sessionId);
|
|
817
|
+
const queue = session.queuedMessages ?? [];
|
|
818
|
+
if (!Number.isInteger(index) || index < 0 || index >= queue.length) {
|
|
819
|
+
throw new Error("队列中没有该条消息(可能已被处理)。");
|
|
820
|
+
}
|
|
821
|
+
const next = queue.slice(0, index).concat(queue.slice(index + 1));
|
|
822
|
+
const updated = { ...session, queuedMessages: next };
|
|
823
|
+
this.sessions.set(sessionId, updated);
|
|
824
|
+
this.storage.saveSession(updated);
|
|
825
|
+
this.emitStructuredSnapshot(updated);
|
|
826
|
+
return updated;
|
|
827
|
+
}
|
|
828
|
+
/** Clear all queued messages. No-op when queue is already empty. */
|
|
829
|
+
clearQueuedMessages(sessionId) {
|
|
830
|
+
const session = this.requireSession(sessionId);
|
|
831
|
+
if (!session.queuedMessages || session.queuedMessages.length === 0) {
|
|
832
|
+
return session;
|
|
833
|
+
}
|
|
834
|
+
const updated = { ...session, queuedMessages: [] };
|
|
835
|
+
this.sessions.set(sessionId, updated);
|
|
836
|
+
this.storage.saveSession(updated);
|
|
837
|
+
this.emitStructuredSnapshot(updated);
|
|
838
|
+
return updated;
|
|
839
|
+
}
|
|
770
840
|
/** Update the selected model for a structured session. Takes effect on the next spawn. */
|
|
771
841
|
setSessionModel(sessionId, model) {
|
|
772
842
|
const session = this.requireSession(sessionId);
|
|
@@ -848,6 +918,7 @@ export class StructuredSessionManager {
|
|
|
848
918
|
stop(id) {
|
|
849
919
|
const session = this.requireSession(id);
|
|
850
920
|
this.interruptedWith.delete(id);
|
|
921
|
+
this.preserveQueueOnInterrupt.delete(id);
|
|
851
922
|
const child = this.pendingChildren.get(id);
|
|
852
923
|
if (child) {
|
|
853
924
|
child.kill();
|
|
@@ -900,6 +971,8 @@ export class StructuredSessionManager {
|
|
|
900
971
|
}
|
|
901
972
|
this.sessions.delete(id);
|
|
902
973
|
this.lastStreamSaveAt.delete(id);
|
|
974
|
+
this.interruptedWith.delete(id);
|
|
975
|
+
this.preserveQueueOnInterrupt.delete(id);
|
|
903
976
|
this.storage.deleteSession(id);
|
|
904
977
|
this.logger?.deleteSession(id);
|
|
905
978
|
}
|
|
@@ -1043,6 +1116,11 @@ export class StructuredSessionManager {
|
|
|
1043
1116
|
if (modelChoice && modelChoice !== "default") {
|
|
1044
1117
|
args.push("--model", modelChoice);
|
|
1045
1118
|
}
|
|
1119
|
+
// 思考深度 → --reasoning-effort(off → minimal,standard → low,deep → medium,max → high)
|
|
1120
|
+
const reasoningFlag = thinkingEffortToCodexFlag(session.thinkingEffort);
|
|
1121
|
+
if (reasoningFlag) {
|
|
1122
|
+
args.push("--reasoning-effort", reasoningFlag);
|
|
1123
|
+
}
|
|
1046
1124
|
if (session.claudeSessionId) {
|
|
1047
1125
|
args.push("resume", session.claudeSessionId, "-");
|
|
1048
1126
|
}
|
|
@@ -1285,7 +1363,7 @@ export class StructuredSessionManager {
|
|
|
1285
1363
|
output: turnState.result,
|
|
1286
1364
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
1287
1365
|
messages: msgs,
|
|
1288
|
-
queuedMessages: interruptPrompt ? [] : current.queuedMessages,
|
|
1366
|
+
queuedMessages: interruptPrompt && !this.preserveQueueOnInterrupt.has(sessionId) ? [] : current.queuedMessages,
|
|
1289
1367
|
pendingEscalation: null,
|
|
1290
1368
|
permissionBlocked: false,
|
|
1291
1369
|
structuredState: {
|
|
@@ -1304,6 +1382,11 @@ export class StructuredSessionManager {
|
|
|
1304
1382
|
}
|
|
1305
1383
|
if (interruptPrompt) {
|
|
1306
1384
|
this.interruptedWith.delete(sessionId);
|
|
1385
|
+
// 把"保留队列"标记一并清掉——不属于本次 interrupt 的后续轮次会按
|
|
1386
|
+
// 默认(清空 queue)行为走,避免 stale flag 影响下一次普通 interrupt。
|
|
1387
|
+
// 注意:被保留的 queuedMessages 不需要在这里主动 flush,重发的
|
|
1388
|
+
// interruptPrompt 跑完会自然触发 flushNextQueuedMessage。
|
|
1389
|
+
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
1307
1390
|
resolve();
|
|
1308
1391
|
setImmediate(() => {
|
|
1309
1392
|
this.sendMessage(sessionId, interruptPrompt).catch((err) => {
|
|
@@ -1384,6 +1467,10 @@ export class StructuredSessionManager {
|
|
|
1384
1467
|
// variadic 参数贪婪吞掉(commander 的 <tools...> 会一直吃 positional 直到
|
|
1385
1468
|
// 下一个 flag)。表现为 claude 报 "Input must be provided either through
|
|
1386
1469
|
// stdin or as a prompt argument when using --print"。
|
|
1470
|
+
//
|
|
1471
|
+
// 思考深度通过给 prompt 前置魔法词触发(think / think hard / ultrathink)。
|
|
1472
|
+
// applyThinkingEffortToPrompt 自身已经做了"用户已写过就不重复加"的保护。
|
|
1473
|
+
const effectivePrompt = applyThinkingEffortToPrompt(prompt, session.thinkingEffort);
|
|
1387
1474
|
const spawnedAt = new Date().toISOString();
|
|
1388
1475
|
const child = spawn("claude", args, {
|
|
1389
1476
|
cwd: session.cwd,
|
|
@@ -1396,13 +1483,13 @@ export class StructuredSessionManager {
|
|
|
1396
1483
|
pid: child.pid ?? null,
|
|
1397
1484
|
cwd: session.cwd,
|
|
1398
1485
|
args,
|
|
1399
|
-
prompt:
|
|
1400
|
-
promptLength:
|
|
1486
|
+
prompt: effectivePrompt.slice(0, 2048),
|
|
1487
|
+
promptLength: effectivePrompt.length,
|
|
1401
1488
|
claudeSessionId: session.claudeSessionId,
|
|
1402
1489
|
spawnedAt,
|
|
1403
1490
|
});
|
|
1404
1491
|
this.pendingChildren.set(sessionId, child);
|
|
1405
|
-
child.stdin?.end(
|
|
1492
|
+
child.stdin?.end(effectivePrompt);
|
|
1406
1493
|
const turnState = {
|
|
1407
1494
|
blocks: [],
|
|
1408
1495
|
result: "",
|
|
@@ -1792,7 +1879,7 @@ export class StructuredSessionManager {
|
|
|
1792
1879
|
output: turnState.result,
|
|
1793
1880
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
1794
1881
|
messages: msgs,
|
|
1795
|
-
queuedMessages: interruptPrompt ? [] : current.queuedMessages,
|
|
1882
|
+
queuedMessages: interruptPrompt && !this.preserveQueueOnInterrupt.has(sessionId) ? [] : current.queuedMessages,
|
|
1796
1883
|
pendingEscalation: null,
|
|
1797
1884
|
permissionBlocked: false,
|
|
1798
1885
|
structuredState: {
|
|
@@ -1817,6 +1904,11 @@ export class StructuredSessionManager {
|
|
|
1817
1904
|
// 用户中断当前回复:保存部分回复后立即发送新消息。
|
|
1818
1905
|
if (interruptPrompt) {
|
|
1819
1906
|
this.interruptedWith.delete(sessionId);
|
|
1907
|
+
// 把"保留队列"标记一并清掉——不属于本次 interrupt 的后续轮次会按
|
|
1908
|
+
// 默认(清空 queue)行为走,避免 stale flag 影响下一次普通 interrupt。
|
|
1909
|
+
// 注意:被保留的 queuedMessages 不需要在这里主动 flush,重发的
|
|
1910
|
+
// interruptPrompt 跑完会自然触发 flushNextQueuedMessage。
|
|
1911
|
+
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
1820
1912
|
resolve();
|
|
1821
1913
|
setImmediate(() => {
|
|
1822
1914
|
this.sendMessage(sessionId, interruptPrompt).catch((err) => {
|
|
@@ -1889,6 +1981,12 @@ export class StructuredSessionManager {
|
|
|
1889
1981
|
// SDK 默认会把整个 process.env 透传给 claude 子进程;这里显式按 inheritEnv 配置组装,
|
|
1890
1982
|
// 否则关闭"继承环境变量"开关时 SDK 路径会被静默忽略。
|
|
1891
1983
|
const sdkEnv = buildChildEnv(this.config.inheritEnv !== false);
|
|
1984
|
+
// 思考深度:off → 显式禁用 thinking,其他 → 给一个固定 budget。
|
|
1985
|
+
// SDK 类型用驼峰 budgetTokens(API 层是 budget_tokens,SDK 内部已做转换)。
|
|
1986
|
+
const sdkThinkingBudget = thinkingEffortToSdkBudget(session.thinkingEffort);
|
|
1987
|
+
const sdkThinking = sdkThinkingBudget > 0
|
|
1988
|
+
? { type: "enabled", budgetTokens: sdkThinkingBudget }
|
|
1989
|
+
: { type: "disabled" };
|
|
1892
1990
|
const sdkOptions = {
|
|
1893
1991
|
cwd: session.cwd,
|
|
1894
1992
|
abortController,
|
|
@@ -1897,6 +1995,7 @@ export class StructuredSessionManager {
|
|
|
1897
1995
|
...(permPolicy.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
|
|
1898
1996
|
...(permPolicy.allowedTools ? { allowedTools: permPolicy.allowedTools } : {}),
|
|
1899
1997
|
...(isManaged ? { disallowedTools: ["AskUserQuestion"] } : {}),
|
|
1998
|
+
thinking: sdkThinking,
|
|
1900
1999
|
includePartialMessages: true,
|
|
1901
2000
|
// 把子 agent 的 text/thinking 也转发回来,UI 才能把"被 Task 召唤来的协作者"
|
|
1902
2001
|
// 渲染成独立角色的群聊消息。关掉这个开关时只会收到子 agent 的 tool_use/tool_result,
|
package/dist/tui/attach.js
CHANGED
|
@@ -230,14 +230,13 @@ export function startAttachTui(deps) {
|
|
|
230
230
|
layout.showToast("服务已安装,按 Shift+S 卸载", "warn", 2500);
|
|
231
231
|
return;
|
|
232
232
|
}
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
});
|
|
233
|
+
const isRoot = typeof process.getuid === "function" ? process.getuid() === 0 : false;
|
|
234
|
+
const body = process.platform === "linux"
|
|
235
|
+
? `将写入 /etc/systemd/system/wand.service,systemctl enable --now,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,可以 Ctrl+C 退出 TUI 后跑 sudo wand service:install。"}`
|
|
236
|
+
: process.platform === "darwin"
|
|
237
|
+
? `将写入 /Library/LaunchDaemons/com.wand.web.plist,launchctl load,开机自启。\n${isRoot ? "当前是 root,可以直接装。" : "⚠ 需要 root,退出 TUI 跑 sudo wand service:install。"}`
|
|
238
|
+
: "当前平台暂不支持。";
|
|
239
|
+
const ok = await layout.confirm({ title: "注册为系统服务", body });
|
|
241
240
|
if (!ok)
|
|
242
241
|
return;
|
|
243
242
|
const r = await runOffMicrotask(() => installService({ configPath: deps.configPath }));
|
package/dist/tui/commands.d.ts
CHANGED
|
@@ -36,12 +36,29 @@ export declare function checkUpdate(currentVersion: string): UpdateInfo;
|
|
|
36
36
|
export declare function installUpdate(): CommandResult;
|
|
37
37
|
export declare function openInBrowser(url: string): CommandResult;
|
|
38
38
|
export declare function copyToClipboard(text: string): CommandResult;
|
|
39
|
+
/**
|
|
40
|
+
* 服务安装的作用域:
|
|
41
|
+
* - "system" = Linux 写 /etc/systemd/system/wand.service;macOS 写 /Library/LaunchDaemons/
|
|
42
|
+
* 需要 root,开机自启,所有用户可用,不依赖 login session。
|
|
43
|
+
* - "user" = Linux 写 ~/.config/systemd/user/wand.service;macOS 写 ~/Library/LaunchAgents/
|
|
44
|
+
* 不要 root,登出会被回收(除非 loginctl enable-linger)。
|
|
45
|
+
*
|
|
46
|
+
* 默认 system。
|
|
47
|
+
*/
|
|
48
|
+
export type ServiceScope = "system" | "user";
|
|
49
|
+
export declare const DEFAULT_SERVICE_SCOPE: ServiceScope;
|
|
39
50
|
export interface ServiceContext {
|
|
40
51
|
configPath: string;
|
|
41
52
|
/** wand 可执行文件路径。优先使用 process.argv[1],回退到 which wand。 */
|
|
42
53
|
wandBin?: string;
|
|
54
|
+
/** 显式指定作用域。不传走 DEFAULT_SERVICE_SCOPE。 */
|
|
55
|
+
scope?: ServiceScope;
|
|
56
|
+
}
|
|
57
|
+
export interface ServiceOpts {
|
|
58
|
+
/** 不传 = 自动检测已装的那个;都没装就用 default。 */
|
|
59
|
+
scope?: ServiceScope;
|
|
43
60
|
}
|
|
44
|
-
export declare function isServiceInstalled(): boolean;
|
|
61
|
+
export declare function isServiceInstalled(scope?: ServiceScope): boolean;
|
|
45
62
|
export interface ServiceStatus {
|
|
46
63
|
/** 是否已安装服务文件。 */
|
|
47
64
|
installed: boolean;
|
|
@@ -54,11 +71,11 @@ export interface ServiceStatus {
|
|
|
54
71
|
/** 平台。 */
|
|
55
72
|
platform: NodeJS.Platform;
|
|
56
73
|
}
|
|
57
|
-
export declare function serviceStatus(): ServiceStatus;
|
|
58
|
-
export declare function serviceStart(): CommandResult;
|
|
59
|
-
export declare function serviceStop(): CommandResult;
|
|
60
|
-
export declare function serviceRestart(): CommandResult;
|
|
74
|
+
export declare function serviceStatus(opts?: ServiceOpts): ServiceStatus;
|
|
75
|
+
export declare function serviceStart(opts?: ServiceOpts): CommandResult;
|
|
76
|
+
export declare function serviceStop(opts?: ServiceOpts): CommandResult;
|
|
77
|
+
export declare function serviceRestart(opts?: ServiceOpts): CommandResult;
|
|
61
78
|
/** 取最近 N 行服务日志。 */
|
|
62
|
-
export declare function serviceLogs(lines?: number): CommandResult;
|
|
79
|
+
export declare function serviceLogs(lines?: number, opts?: ServiceOpts): CommandResult;
|
|
63
80
|
export declare function installService(ctx: ServiceContext): CommandResult;
|
|
64
|
-
export declare function uninstallService(): CommandResult;
|
|
81
|
+
export declare function uninstallService(opts?: ServiceOpts): CommandResult;
|