@co0ontty/wand 1.60.4 → 1.61.0-beta.g5043a45

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "6c41ab7538027b4f0e82aaecef3adbace521ad95",
3
- "builtAt": "2026-06-13T07:32:37.969Z",
4
- "version": "1.60.4",
5
- "channel": "stable"
2
+ "commit": "5043a45750e7229c7f3567c971edfd10beccbce8",
3
+ "builtAt": "2026-06-13T23:53:44.046Z",
4
+ "version": "1.61.0-beta.g5043a45",
5
+ "channel": "beta"
6
6
  }
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 ["defaultMode", "defaultCwd", "defaultModel", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
10
+ export declare const PREFERENCE_KEYS: readonly ["defaultMode", "defaultCwd", "defaultModel", "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
  export declare const defaultConfig: () => WandConfig;
package/dist/config.js CHANGED
@@ -16,6 +16,7 @@ export const PREFERENCE_KEYS = [
16
16
  "defaultMode",
17
17
  "defaultCwd",
18
18
  "defaultModel",
19
+ "defaultThinkingEffort",
19
20
  "structuredRunner",
20
21
  "language",
21
22
  "cardDefaults",
@@ -44,6 +45,7 @@ export const defaultConfig = () => ({
44
45
  macos: defaultMacosDmgConfig(),
45
46
  cardDefaults: defaultCardExpandDefaults(),
46
47
  defaultModel: "",
48
+ defaultThinkingEffort: "off",
47
49
  structuredRunner: "cli",
48
50
  inheritEnv: true,
49
51
  commandPresets: [
@@ -242,6 +244,11 @@ export function applyStoragePreferences(config, storage) {
242
244
  if (typeof v === "string")
243
245
  config.defaultModel = v.trim();
244
246
  }
247
+ if (storage.hasPreference(preferenceStorageKey("defaultThinkingEffort"))) {
248
+ const v = storage.getPreference(preferenceStorageKey("defaultThinkingEffort"), defaults.defaultThinkingEffort ?? "off");
249
+ if (v === "off" || v === "standard" || v === "deep" || v === "max")
250
+ config.defaultThinkingEffort = v;
251
+ }
245
252
  if (storage.hasPreference(preferenceStorageKey("structuredRunner"))) {
246
253
  const v = storage.getPreference(preferenceStorageKey("structuredRunner"), defaults.structuredRunner ?? "cli");
247
254
  if (v === "cli" || v === "sdk")
@@ -285,6 +292,12 @@ export function writePreferenceToStorage(config, storage, key, value) {
285
292
  config.defaultModel = v;
286
293
  break;
287
294
  }
295
+ case "defaultThinkingEffort": {
296
+ const v = value === "standard" || value === "deep" || value === "max" ? value : "off";
297
+ storage.setPreference(dbKey, v);
298
+ config.defaultThinkingEffort = v;
299
+ break;
300
+ }
288
301
  case "structuredRunner": {
289
302
  const v = value === "cli" ? "cli" : "sdk";
290
303
  storage.setPreference(dbKey, v);
@@ -448,6 +461,11 @@ function mergeWithDefaults(input) {
448
461
  macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
449
462
  cardDefaults: normalizeCardDefaults(input.cardDefaults),
450
463
  defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
464
+ defaultThinkingEffort: input.defaultThinkingEffort === "standard"
465
+ || input.defaultThinkingEffort === "deep"
466
+ || input.defaultThinkingEffort === "max"
467
+ ? input.defaultThinkingEffort
468
+ : "off",
451
469
  structuredRunner: (input.structuredRunner === "sdk" || input.structuredRunner === "cli") ? input.structuredRunner : defaults.structuredRunner,
452
470
  inheritEnv: typeof input.inheritEnv === "boolean" ? input.inheritEnv : (defaults.inheritEnv ?? true),
453
471
  };
@@ -165,7 +165,10 @@ function isClaudeSessionFileAvailable(cwd, claudeSessionId) {
165
165
  * interpretations and validate with existsSync, falling back to naive replacement.
166
166
  */
167
167
  function invertNormalizedProjectDir(dirName) {
168
- // The normalization is: path.resolve(cwd).replace(/\//g, "-")
168
+ // The normalization replaces every non-alphanumeric char with "-", so this
169
+ // inversion is best-effort: "-" most often maps back to "/", but may also be
170
+ // a literal "-", ".", or "_". We try "/" vs "-" per position and validate with
171
+ // existsSync; dots/underscores in the original path can't be recovered here.
169
172
  const naive = dirName.replace(/-/g, "/");
170
173
  if (existsSync(naive))
171
174
  return naive;
@@ -494,7 +497,11 @@ function listClaudeTaskIds() {
494
497
  }
495
498
  }
496
499
  function getClaudeProjectDir(cwd) {
497
- const normalized = path.resolve(cwd).replace(/\//g, "-");
500
+ // Claude Code encodes the project dir by replacing every non-alphanumeric
501
+ // character (slash, dot, underscore, etc.) with "-", not just "/". Mirroring
502
+ // only "/" misses paths like ".../vibe_coding/wand" → the scan looks in a
503
+ // directory Claude never wrote to, so the session ID is never discovered.
504
+ const normalized = path.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
498
505
  return path.join(os.homedir(), ".claude", "projects", normalized);
499
506
  }
500
507
  function getLatestClaudeTaskId(excludeIds) {
@@ -159,10 +159,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
159
159
  provider,
160
160
  runner: body.runner ?? (provider === "codex" ? "codex-cli-exec" : "claude-cli-print"),
161
161
  worktreeEnabled: body.worktreeEnabled === true,
162
- model: typeof body.model === "string" ? body.model.trim() : undefined,
162
+ model: typeof body.model === "string" ? body.model.trim() : (config.defaultModel ?? "").trim() || undefined,
163
163
  thinkingEffort: typeof body.thinkingEffort === "string"
164
164
  ? body.thinkingEffort
165
- : undefined,
165
+ : config.defaultThinkingEffort,
166
166
  });
167
167
  onSessionCreated?.(body.cwd ?? snapshot.cwd);
168
168
  const prompt = body.prompt?.trim();
@@ -252,7 +252,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
252
252
  }
253
253
  });
254
254
  // ── Structured queued-messages management ──
255
- // 三个端点构成"排队消息条"的后端操作面:reorder(拖拽换序)、单条删除、全部清空。
255
+ // 这些端点构成"排队消息条"的后端操作面:reorder、立即发送、单条删除、全部清空。
256
256
  // 全部走乐观更新模型,失败时前端会回滚到上一次 WS 推送的 queuedMessages。
257
257
  app.patch("/api/structured-sessions/:id/queued", express.json(), (req, res) => {
258
258
  const rawOrder = req.body?.order;
@@ -282,6 +282,27 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
282
282
  res.status(400).json({ error: getErrorMessage(error, "无法删除排队消息。") });
283
283
  }
284
284
  });
285
+ app.post("/api/structured-sessions/:id/queued/:index/promote", express.json(), async (req, res) => {
286
+ const index = Number(req.params.index);
287
+ if (!Number.isInteger(index)) {
288
+ res.status(400).json({ error: "下标无效。" });
289
+ return;
290
+ }
291
+ const expectedText = typeof req.body?.expectedText === "string" ? req.body.expectedText : undefined;
292
+ const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
293
+ try {
294
+ const snapshot = await structured.promoteQueuedMessage(req.params.id, index, expectedText, idempotencyKey);
295
+ res.json(snapshot);
296
+ }
297
+ catch (error) {
298
+ const errorCode = error?.code;
299
+ const status = errorCode === "duplicate_idempotency_key" ? 409 : 400;
300
+ res.status(status).json({
301
+ error: getErrorMessage(error, "无法立即发送排队消息。"),
302
+ errorCode,
303
+ });
304
+ }
305
+ });
285
306
  app.delete("/api/structured-sessions/:id/queued", (req, res) => {
286
307
  try {
287
308
  const snapshot = structured.clearQueuedMessages(req.params.id);
package/dist/server.js CHANGED
@@ -1190,6 +1190,8 @@ export async function startServer(config, configPath) {
1190
1190
  port: config.port,
1191
1191
  defaultMode: config.defaultMode,
1192
1192
  defaultCwd: config.defaultCwd,
1193
+ defaultModel: config.defaultModel ?? "",
1194
+ defaultThinkingEffort: config.defaultThinkingEffort ?? "off",
1193
1195
  commandPresets: config.commandPresets,
1194
1196
  structuredRunner: config.structuredRunner ?? "cli",
1195
1197
  structuredRunners: [
@@ -2037,7 +2039,7 @@ export async function startServer(config, configPath) {
2037
2039
  model: effectiveModel,
2038
2040
  cols: reqCols,
2039
2041
  rows: reqRows,
2040
- thinkingEffort: body.thinkingEffort ?? undefined,
2042
+ thinkingEffort: body.thinkingEffort ?? config.defaultThinkingEffort,
2041
2043
  });
2042
2044
  recordRecentPath(storage, body.cwd ?? snapshot.cwd);
2043
2045
  res.status(201).json(snapshot);
@@ -90,6 +90,7 @@ export declare class StructuredSessionManager {
90
90
  interrupt?: boolean;
91
91
  idempotencyKey?: string;
92
92
  preserveQueue?: boolean;
93
+ queueAlreadyRemoved?: boolean;
93
94
  }): Promise<SessionSnapshot>;
94
95
  /**
95
96
  * Reorder the pending queued messages. `order` is a permutation of the current
@@ -102,6 +103,12 @@ export declare class StructuredSessionManager {
102
103
  reorderQueuedMessages(sessionId: string, order: number[]): SessionSnapshot;
103
104
  /** Remove a single queued message by index. */
104
105
  deleteQueuedMessage(sessionId: string, index: number): SessionSnapshot;
106
+ /**
107
+ * Remove one queued message by index before sending it. Keeping this operation
108
+ * on the server prevents clients from re-sending the text while the original
109
+ * queue entry remains available for the automatic flush path.
110
+ */
111
+ promoteQueuedMessage(sessionId: string, index: number, expectedText?: string, idempotencyKey?: string): Promise<SessionSnapshot>;
105
112
  /** Clear all queued messages. No-op when queue is already empty. */
106
113
  clearQueuedMessages(sessionId: string): SessionSnapshot;
107
114
  /** Update the selected model for a structured session. Takes effect on the next spawn. */
@@ -119,8 +126,6 @@ export declare class StructuredSessionManager {
119
126
  stop(id: string): SessionSnapshot;
120
127
  delete(id: string): void;
121
128
  private requireSession;
122
- private buildQueuedPlaceholderTurns;
123
- private buildRenderableMessages;
124
129
  private emitStructuredSnapshot;
125
130
  private flushNextQueuedMessage;
126
131
  private emit;
@@ -629,6 +629,22 @@ export class StructuredSessionManager {
629
629
  this.interruptedWith.set(id, prompt);
630
630
  if (opts.preserveQueue) {
631
631
  this.preserveQueueOnInterrupt.add(id);
632
+ // 「立即发送」排队条某一条:interrupt 把它作为新输入重发,但该条仍留在
633
+ // queuedMessages 里。必须在这里把它从队列摘掉一次,否则 preserveQueue 会
634
+ // 原样保留整条队列,待 interruptPrompt 跑完 flushNextQueuedMessage 会把它
635
+ // 当成普通排队再发一遍(重复发送)。旧客户端没有走 promote endpoint,
636
+ // 服务端只能按文本删第一处匹配;新客户端会带 queueAlreadyRemoved 跳过这里。
637
+ if (!opts.queueAlreadyRemoved) {
638
+ const queue = session.queuedMessages ?? [];
639
+ const removeAt = queue.indexOf(prompt);
640
+ if (removeAt !== -1) {
641
+ const trimmedQueue = queue.slice(0, removeAt).concat(queue.slice(removeAt + 1));
642
+ session = { ...session, queuedMessages: trimmedQueue };
643
+ this.sessions.set(id, session);
644
+ this.storage.saveSession(session);
645
+ this.emitStructuredSnapshot(session);
646
+ }
647
+ }
632
648
  }
633
649
  else {
634
650
  this.preserveQueueOnInterrupt.delete(id);
@@ -796,6 +812,44 @@ export class StructuredSessionManager {
796
812
  this.emitStructuredSnapshot(updated);
797
813
  return updated;
798
814
  }
815
+ /**
816
+ * Remove one queued message by index before sending it. Keeping this operation
817
+ * on the server prevents clients from re-sending the text while the original
818
+ * queue entry remains available for the automatic flush path.
819
+ */
820
+ async promoteQueuedMessage(sessionId, index, expectedText, idempotencyKey) {
821
+ const session = this.requireSession(sessionId);
822
+ if (idempotencyKey && this.seenIdempotencyKeys.has(`${sessionId}:${idempotencyKey}`)) {
823
+ return session;
824
+ }
825
+ const queue = session.queuedMessages ?? [];
826
+ if (!Number.isInteger(index) || index < 0 || index >= queue.length) {
827
+ throw new Error("队列中没有该条消息(可能已被处理)。");
828
+ }
829
+ if (expectedText !== undefined && queue[index] !== expectedText) {
830
+ throw new Error("排队消息已变化,请按最新顺序重试。");
831
+ }
832
+ const prompt = queue[index];
833
+ const remaining = queue.slice(0, index).concat(queue.slice(index + 1));
834
+ const inFlight = session.status === "running" && session.structuredState?.inFlight === true;
835
+ const updated = { ...session, queuedMessages: remaining };
836
+ this.sessions.set(sessionId, updated);
837
+ this.storage.saveSession(updated);
838
+ this.emitStructuredSnapshot(updated);
839
+ try {
840
+ return await this.sendMessage(sessionId, prompt, {
841
+ interrupt: inFlight,
842
+ preserveQueue: inFlight,
843
+ queueAlreadyRemoved: true,
844
+ idempotencyKey,
845
+ });
846
+ }
847
+ catch {
848
+ // Once the item has been promoted it must not return to the queue: the
849
+ // send path may have already persisted its user turn before a runner error.
850
+ return this.requireSession(sessionId);
851
+ }
852
+ }
799
853
  /** Clear all queued messages. No-op when queue is already empty. */
800
854
  clearQueuedMessages(sessionId) {
801
855
  const session = this.requireSession(sessionId);
@@ -957,23 +1011,13 @@ export class StructuredSessionManager {
957
1011
  }
958
1012
  return session;
959
1013
  }
960
- buildQueuedPlaceholderTurns(session) {
961
- return (session.queuedMessages ?? []).map((text) => ({
962
- role: "user",
963
- content: [{ type: "text", text, __queued: true }],
964
- }));
965
- }
966
- buildRenderableMessages(session) {
967
- return [
968
- ...(session.messages ?? []),
969
- ...this.buildQueuedPlaceholderTurns(session),
970
- ];
971
- }
972
1014
  emitStructuredSnapshot(session, eventType = "output") {
1015
+ // 排队消息只通过 payload.queuedMessages 单独下发,由各端在消息卡片外的「排队条」
1016
+ // 里纵向渲染——绝不再把它们当成 __queued 占位 turn 混进 messages 消息流里,否则会
1017
+ // 和排队条重复显示(旧的「显示异常」根因)。
973
1018
  const payload = buildStructuredOutputPayload(session);
974
1019
  const data = {
975
1020
  ...payload,
976
- messages: this.buildRenderableMessages(session),
977
1021
  status: session.status,
978
1022
  exitCode: session.exitCode,
979
1023
  };
package/dist/types.d.ts CHANGED
@@ -102,6 +102,8 @@ export interface WandConfig {
102
102
  cardDefaults?: CardExpandDefaults;
103
103
  /** 新建会话时默认使用的 Claude 模型(别名或完整 ID)。留空则不传 --model,由 claude 自行决定。 */
104
104
  defaultModel?: string;
105
+ /** 新建会话时默认使用的思考深度。 */
106
+ defaultThinkingEffort?: "off" | "standard" | "deep" | "max";
105
107
  /** 结构化会话使用的 runner: "cli"(默认,spawn claude -p)或 "sdk"(@anthropic-ai/claude-agent-sdk)。 */
106
108
  structuredRunner?: "cli" | "sdk";
107
109
  /**