@co0ontty/wand 2.6.0 → 2.7.0

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": "c212a14a9564a6f3cf23c019f76d82f623b8fc25",
3
- "builtAt": "2026-07-10T23:50:15.693Z",
4
- "version": "2.6.0",
2
+ "commit": "fe6482dc2c6a1a02fbe00800836b78af034d851a",
3
+ "builtAt": "2026-07-11T12:18:50.530Z",
4
+ "version": "2.7.0",
5
5
  "channel": "stable"
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", "defaultCodexModel", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
10
+ export declare const PREFERENCE_KEYS: readonly ["defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "commitCli", "commitModel", "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
@@ -18,6 +18,8 @@ export const PREFERENCE_KEYS = [
18
18
  "defaultCwd",
19
19
  "defaultModel",
20
20
  "defaultCodexModel",
21
+ "commitCli",
22
+ "commitModel",
21
23
  "defaultThinkingEffort",
22
24
  "structuredRunner",
23
25
  "language",
@@ -53,6 +55,8 @@ export const defaultConfig = () => ({
53
55
  cardDefaults: defaultCardExpandDefaults(),
54
56
  defaultModel: "",
55
57
  defaultCodexModel: "",
58
+ commitCli: "claude",
59
+ commitModel: "",
56
60
  defaultThinkingEffort: "off",
57
61
  structuredRunner: "cli",
58
62
  inheritEnv: true,
@@ -257,6 +261,16 @@ export function applyStoragePreferences(config, storage) {
257
261
  if (typeof v === "string")
258
262
  config.defaultCodexModel = v.trim();
259
263
  }
264
+ if (storage.hasPreference(preferenceStorageKey("commitCli"))) {
265
+ const v = storage.getPreference(preferenceStorageKey("commitCli"), defaults.commitCli ?? "claude");
266
+ if (v === "claude" || v === "codex")
267
+ config.commitCli = v;
268
+ }
269
+ if (storage.hasPreference(preferenceStorageKey("commitModel"))) {
270
+ const v = storage.getPreference(preferenceStorageKey("commitModel"), defaults.commitModel ?? "");
271
+ if (typeof v === "string")
272
+ config.commitModel = v.trim();
273
+ }
260
274
  if (storage.hasPreference(preferenceStorageKey("defaultThinkingEffort"))) {
261
275
  const v = storage.getPreference(preferenceStorageKey("defaultThinkingEffort"), defaults.defaultThinkingEffort ?? "off");
262
276
  if (v === "off" || v === "standard" || v === "deep" || v === "max")
@@ -311,6 +325,19 @@ export function writePreferenceToStorage(config, storage, key, value) {
311
325
  config.defaultCodexModel = v;
312
326
  break;
313
327
  }
328
+ case "commitCli": {
329
+ if (value !== "claude" && value !== "codex")
330
+ throw new Error(`无效 commit CLI: ${value}`);
331
+ storage.setPreference(dbKey, value);
332
+ config.commitCli = value;
333
+ break;
334
+ }
335
+ case "commitModel": {
336
+ const v = typeof value === "string" ? value.trim() : "";
337
+ storage.setPreference(dbKey, v);
338
+ config.commitModel = v;
339
+ break;
340
+ }
314
341
  case "defaultThinkingEffort": {
315
342
  const v = value === "standard" || value === "deep" || value === "max" ? value : "off";
316
343
  storage.setPreference(dbKey, v);
@@ -481,6 +508,8 @@ function mergeWithDefaults(input) {
481
508
  cardDefaults: normalizeCardDefaults(input.cardDefaults),
482
509
  defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
483
510
  defaultCodexModel: typeof input.defaultCodexModel === "string" ? input.defaultCodexModel.trim() : defaults.defaultCodexModel,
511
+ commitCli: input.commitCli === "codex" ? "codex" : "claude",
512
+ commitModel: typeof input.commitModel === "string" ? input.commitModel.trim() : defaults.commitModel,
484
513
  defaultThinkingEffort: input.defaultThinkingEffort === "standard"
485
514
  || input.defaultThinkingEffort === "deep"
486
515
  || input.defaultThinkingEffort === "max"
@@ -4,7 +4,7 @@ import { getDefaultModelForProvider, normalizeMode } from "./config.js";
4
4
  import { blockWindowMessagesForTransport, sliceTurnBlocksForTransport, truncateMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
5
5
  import { checkSessionWorktreeMergeability, cleanupSessionWorktree, getWorktreeMergeErrorCode, mergeSessionWorktree, WorktreeMergeError } from "./git-worktree.js";
6
6
  import { resolveSessionCwd } from "./session-cwd.js";
7
- import { resolveSessionAiContext } from "./session-ai-context.js";
7
+ import { resolveCommitAiContext } from "./session-ai-context.js";
8
8
  import { getGitStatus, QuickCommitError, runQuickCommitWithFallback, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
9
9
  import { getErrorMessage } from "./error-utils.js";
10
10
  export { getErrorMessage };
@@ -21,6 +21,18 @@ function getInputErrorResponse(error, sessionId) {
21
21
  },
22
22
  };
23
23
  }
24
+ const errorCode = error?.code;
25
+ if (errorCode === "duplicate_queued_message" || errorCode === "duplicate_idempotency_key") {
26
+ return {
27
+ statusCode: 409,
28
+ payload: {
29
+ error: getErrorMessage(error, "检测到重复发送,已拦截。"),
30
+ errorCode,
31
+ sessionId,
32
+ sessionStatus: null,
33
+ },
34
+ };
35
+ }
24
36
  return {
25
37
  statusCode: 400,
26
38
  payload: {
@@ -346,7 +358,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
346
358
  }
347
359
  catch (error) {
348
360
  const errorCode = error?.code;
349
- const status = errorCode === "duplicate_idempotency_key" ? 409 : 400;
361
+ const status = errorCode === "duplicate_idempotency_key" || errorCode === "duplicate_queued_message" ? 409 : 400;
350
362
  res.status(status).json({
351
363
  error: getErrorMessage(error, "无法发送结构化消息。"),
352
364
  errorCode,
@@ -538,7 +550,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
538
550
  }
539
551
  const body = (req.body ?? {});
540
552
  try {
541
- const ai = resolveSessionAiContext(snapshot, config);
553
+ const ai = resolveCommitAiContext(snapshot, config);
542
554
  const result = await runQuickCommitWithFallback({
543
555
  cwd: snapshot.cwd,
544
556
  language: config.language ?? "",
@@ -572,7 +584,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
572
584
  return;
573
585
  }
574
586
  try {
575
- const ai = resolveSessionAiContext(snapshot, config);
587
+ const ai = resolveCommitAiContext(snapshot, config);
576
588
  const result = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "", {
577
589
  ...ai,
578
590
  });
@@ -598,7 +610,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
598
610
  }
599
611
  const body = (req.body ?? {});
600
612
  try {
601
- const ai = resolveSessionAiContext(snapshot, config);
613
+ const ai = resolveCommitAiContext(snapshot, config);
602
614
  const result = await runTagHead({
603
615
  cwd: snapshot.cwd,
604
616
  language: config.language ?? "",
@@ -13,3 +13,5 @@ export interface SessionAiContext {
13
13
  export declare function resolveSessionProvider(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command">): SessionProvider;
14
14
  /** Build the provider-specific settings used by session-adjacent AI actions. */
15
15
  export declare function resolveSessionAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultThinkingEffort" | "inheritEnv">): SessionAiContext;
16
+ /** Build the AI context for quick-commit actions from their global preferences. */
17
+ export declare function resolveCommitAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultThinkingEffort" | "inheritEnv" | "commitCli" | "commitModel">): SessionAiContext;
@@ -34,3 +34,12 @@ export function resolveSessionAiContext(snapshot, config) {
34
34
  inheritEnv: config.inheritEnv,
35
35
  };
36
36
  }
37
+ /** Build the AI context for quick-commit actions from their global preferences. */
38
+ export function resolveCommitAiContext(snapshot, config) {
39
+ const sessionContext = resolveSessionAiContext(snapshot, config);
40
+ return {
41
+ ...sessionContext,
42
+ provider: config.commitCli === "codex" ? "codex" : "claude",
43
+ model: normalizeModel(config.commitModel),
44
+ };
45
+ }
@@ -56,6 +56,16 @@ export declare function buildCodexFileChangeBlocks(item: Record<string, unknown>
56
56
  * model-produced text/tool arguments; the final provider value replaces this.
57
57
  */
58
58
  export declare function estimateCodexOutputTokens(blocks: ContentBlock[]): number;
59
+ /**
60
+ * 返回最近一次真正提交给结构化会话的用户输入。
61
+ *
62
+ * 排队非空时,队尾才是“上一条提交”;否则回看当前正在处理的最后一个 user turn。
63
+ * 这里只接受可无损还原成字符串的 text / tool_result,避免把图片等结构化内容误判
64
+ * 成更早的纯文本输入。
65
+ */
66
+ export declare function getLastSubmittedStructuredInput(snapshot: Pick<SessionSnapshot, "messages" | "queuedMessages">): string | null;
67
+ /** 仅用于 in-flight 排队分支:连续两次内容相同则把后一次视为输入重放。 */
68
+ export declare function isDuplicateStructuredQueueInput(snapshot: Pick<SessionSnapshot, "messages" | "queuedMessages">, input: string): boolean;
59
69
  export declare class StructuredSessionManager {
60
70
  private readonly storage;
61
71
  private readonly config;
@@ -694,6 +694,44 @@ function buildStructuredOutputPayload(snapshot) {
694
694
  summary: snapshot.description ?? snapshot.summary,
695
695
  };
696
696
  }
697
+ /**
698
+ * 返回最近一次真正提交给结构化会话的用户输入。
699
+ *
700
+ * 排队非空时,队尾才是“上一条提交”;否则回看当前正在处理的最后一个 user turn。
701
+ * 这里只接受可无损还原成字符串的 text / tool_result,避免把图片等结构化内容误判
702
+ * 成更早的纯文本输入。
703
+ */
704
+ export function getLastSubmittedStructuredInput(snapshot) {
705
+ const queue = snapshot.queuedMessages ?? [];
706
+ for (let i = queue.length - 1; i >= 0; i--) {
707
+ const queued = queue[i]?.trim();
708
+ if (queued)
709
+ return queued;
710
+ }
711
+ const messages = snapshot.messages ?? [];
712
+ for (let i = messages.length - 1; i >= 0; i--) {
713
+ const turn = messages[i];
714
+ if (turn.role !== "user")
715
+ continue;
716
+ const textParts = turn.content
717
+ .filter((block) => block.type === "text")
718
+ .map((block) => block.text);
719
+ if (textParts.length > 0) {
720
+ const text = textParts.join("\n").trim();
721
+ return text || null;
722
+ }
723
+ const toolResult = turn.content.find((block) => block.type === "tool_result" && typeof block.content === "string");
724
+ return toolResult && typeof toolResult.content === "string" ? toolResult.content.trim() || null : null;
725
+ }
726
+ return null;
727
+ }
728
+ /** 仅用于 in-flight 排队分支:连续两次内容相同则把后一次视为输入重放。 */
729
+ export function isDuplicateStructuredQueueInput(snapshot, input) {
730
+ const prompt = input.trim();
731
+ if (!prompt)
732
+ return false;
733
+ return getLastSubmittedStructuredInput(snapshot) === prompt;
734
+ }
697
735
  function buildIncrementalStructuredPayload(snapshot, cardDefaults) {
698
736
  const messages = snapshot.messages ?? [];
699
737
  const lastTurn = messages.length > 0 ? messages[messages.length - 1] : undefined;
@@ -1001,6 +1039,11 @@ export class StructuredSessionManager {
1001
1039
  }
1002
1040
  else {
1003
1041
  const queue = [...(session.queuedMessages ?? [])];
1042
+ if (isDuplicateStructuredQueueInput(session, prompt)) {
1043
+ const err = new Error("与上一条消息相同,已忽略,不会加入排队。");
1044
+ err.code = "duplicate_queued_message";
1045
+ throw err;
1046
+ }
1004
1047
  if (queue.length >= 10) {
1005
1048
  throw new Error("排队消息已满(最多 10 条),请等待当前消息处理完成。");
1006
1049
  }
package/dist/types.d.ts CHANGED
@@ -104,6 +104,10 @@ export interface WandConfig {
104
104
  defaultModel?: string;
105
105
  /** 新建 Codex 会话时默认使用的模型。留空则不传 --model,由 codex 自行决定。 */
106
106
  defaultCodexModel?: string;
107
+ /** 快捷提交生成 commit message / tag 时使用的 CLI。 */
108
+ commitCli?: SessionProvider;
109
+ /** 快捷提交专用模型。留空则跟随所选 CLI 的默认模型。 */
110
+ commitModel?: string;
107
111
  /** 新建会话时默认使用的思考深度。 */
108
112
  defaultThinkingEffort?: "off" | "standard" | "deep" | "max";
109
113
  /** 结构化会话使用的 runner: "cli"(默认,spawn claude -p)或 "sdk"(@anthropic-ai/claude-agent-sdk)。 */