@co0ontty/wand 4.13.0 → 4.14.0-beta.ga6331de

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": "95fe372be4bebb2e489b287ddc8de993291d390c",
3
- "builtAt": "2026-07-17T21:08:20.623Z",
4
- "version": "4.13.0",
5
- "channel": "stable"
2
+ "commit": "a6331de5ad360ff5cd861a4ffe8bc5dc37f56562",
3
+ "builtAt": "2026-07-18T00:02:04.661Z",
4
+ "version": "4.14.0-beta.ga6331de",
5
+ "channel": "beta"
6
6
  }
@@ -25,7 +25,7 @@ interface QuickCommitOptions {
25
25
  */
26
26
  submodule?: boolean;
27
27
  }
28
- interface QuickCommitAiOptions {
28
+ export interface QuickCommitAiOptions {
29
29
  provider?: SessionProvider;
30
30
  model?: string | null;
31
31
  thinkingEffort?: SessionSnapshot["thinkingEffort"];
@@ -37,6 +37,11 @@ export declare class QuickCommitError extends Error {
37
37
  readonly code: QuickCommitErrorCode;
38
38
  constructor(message: string, code: QuickCommitErrorCode);
39
39
  }
40
+ /**
41
+ * Run a lightweight AI request through the same source ordering used by quick
42
+ * commit: the user's selected source first, then the reciprocal source once.
43
+ */
44
+ export declare function callConfiguredAiText(prompt: string, cwd: string, language: string, opts: QuickCommitAiOptions): Promise<string>;
40
45
  export interface GenerateCommitMessageResult {
41
46
  message: string;
42
47
  /** AI-suggested next tag derived from the staged diff and the latest existing tag. */
@@ -625,7 +625,11 @@ function aiFallbackFailed(primary, primaryError, fallbackError) {
625
625
  const fallback = primary === "直连 API" ? "CLI" : "直连 API";
626
626
  return new QuickCommitError(`${primary} 与 ${fallback} 均失败:${getGitErrorMessage(primaryError)};${getGitErrorMessage(fallbackError)}`, "AI_FALLBACK_FAILED");
627
627
  }
628
- async function callAiText(prompt, cwd, language, opts) {
628
+ /**
629
+ * Run a lightweight AI request through the same source ordering used by quick
630
+ * commit: the user's selected source first, then the reciprocal source once.
631
+ */
632
+ export async function callConfiguredAiText(prompt, cwd, language, opts) {
629
633
  if (opts.systemAi?.enabled) {
630
634
  try {
631
635
  return await callDirectApiText(prompt, opts.systemAi);
@@ -682,7 +686,7 @@ async function generateCommitMessage(cwd, language, ai = {}) {
682
686
  const diff = await collectStagedDiff(cwd);
683
687
  const lang = language.trim() || "中文";
684
688
  const prompt = `阅读以下 git diff,用${lang}写一条简洁的 commit message。要求:祈使句,不超过 50 字,描述「做了什么」。只输出 message 本身,不要引号、不要 Markdown 格式、不要任何额外说明。\n\n${diff}`;
685
- const raw = await callAiText(prompt, cwd, language, ai);
689
+ const raw = await callConfiguredAiText(prompt, cwd, language, ai);
686
690
  const message = normalizeAiText(raw);
687
691
  if (!message) {
688
692
  throw new QuickCommitError("AI 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
@@ -738,7 +742,7 @@ async function generateCommitMessageWithTag(cwd, language, ai = {}) {
738
742
 
739
743
  git diff:
740
744
  ${diff}`;
741
- const raw = await callAiText(prompt, cwd, language, ai);
745
+ const raw = await callConfiguredAiText(prompt, cwd, language, ai);
742
746
  const parsed = tryParseJson(raw);
743
747
  let message;
744
748
  let suggestedTag;
@@ -812,7 +816,7 @@ commit message:${commitMessage}
812
816
 
813
817
  git diff:
814
818
  ${diff}`;
815
- const raw = await callAiText(prompt, cwd, language, ai);
819
+ const raw = await callConfiguredAiText(prompt, cwd, language, ai);
816
820
  const parsed = tryParseJson(raw);
817
821
  let suggested;
818
822
  if (parsed && typeof parsed.tag === "string") {
@@ -29,7 +29,7 @@ export declare class ProcessManager extends EventEmitter {
29
29
  private readonly dirtySessions;
30
30
  /** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数(旧服务器进程已死) */
31
31
  private orphanRecoveredCount;
32
- private readonly topicRequests;
32
+ private readonly topicCoordinator;
33
33
  private disposed;
34
34
  constructor(config: WandConfig, storage: WandStorage, configDir?: string);
35
35
  on(_event: "process", listener: ProcessEventHandler): this;
@@ -102,6 +102,7 @@ export declare class ProcessManager extends EventEmitter {
102
102
  private snapshotSlim;
103
103
  private isPermissionBlocked;
104
104
  setSessionTopic(id: string, title: string, description: string): SessionSnapshot;
105
+ private setSessionTopicGenerating;
105
106
  /**
106
107
  * Persist worktree merge progress through the manager that owns the live
107
108
  * session record. Returning null lets callers fall back to another owner (or
@@ -16,8 +16,9 @@ import { buildLanguageDirective, buildManagedAutonomyDirective } from "./languag
16
16
  import { prepareSessionWorktree } from "./git-worktree.js";
17
17
  import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
18
18
  import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
19
- import { generateSessionTopic } from "./session-topic.js";
19
+ import { SessionTopicCoordinator } from "./session-topic.js";
20
20
  import { getErrorMessage } from "./error-utils.js";
21
+ import { resolveCommitAiContext } from "./session-ai-context.js";
21
22
  import { resolveSessionCwd } from "./session-cwd.js";
22
23
  import { ProviderHistoryScanner, } from "./provider-history-scanner.js";
23
24
  function resolveProviderFromCommand(command) {
@@ -529,7 +530,7 @@ export class ProcessManager extends EventEmitter {
529
530
  dirtySessions = new Map();
530
531
  /** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数(旧服务器进程已死) */
531
532
  orphanRecoveredCount = 0;
532
- topicRequests = new Set();
533
+ topicCoordinator = new SessionTopicCoordinator();
533
534
  disposed = false;
534
535
  constructor(config, storage, configDir) {
535
536
  super();
@@ -624,6 +625,7 @@ export class ProcessManager extends EventEmitter {
624
625
  openCodeSessionDiscoveryTimer: null,
625
626
  claudeSessionId: restoredSessionId ?? updated.claudeSessionId,
626
627
  approvalStats: snapshot.approvalStats ?? { tool: 0, command: 0, file: 0, total: 0 },
628
+ titleGenerating: false,
627
629
  ptyCols: snapshot.ptyCols ?? 120,
628
630
  ptyRows: snapshot.ptyRows ?? 36,
629
631
  });
@@ -669,6 +671,7 @@ export class ProcessManager extends EventEmitter {
669
671
  openCodeSessionDiscoveryTimer: null,
670
672
  claudeSessionId: restoredSessionId ?? updated.claudeSessionId,
671
673
  approvalStats: snapshot.approvalStats ?? { tool: 0, command: 0, file: 0, total: 0 },
674
+ titleGenerating: false,
672
675
  ptyCols: snapshot.ptyCols ?? 120,
673
676
  ptyRows: snapshot.ptyRows ?? 36,
674
677
  });
@@ -726,7 +729,7 @@ export class ProcessManager extends EventEmitter {
726
729
  catch { /* best-effort shutdown flush */ }
727
730
  }
728
731
  }
729
- this.topicRequests.clear();
732
+ this.topicCoordinator.clear();
730
733
  this.removeAllListeners("process");
731
734
  this.logger.dispose();
732
735
  }
@@ -1672,10 +1675,17 @@ export class ProcessManager extends EventEmitter {
1672
1675
  record.title = title;
1673
1676
  record.description = description;
1674
1677
  const snapshot = this.snapshot(record);
1675
- this.storage.updateSessionRuntimeMetadata(snapshot);
1678
+ this.storage.updateSessionRuntimeMetadata({ ...snapshot, titleGenerating: undefined });
1676
1679
  this.emitEvent({ type: "output", sessionId: id, data: { title, description, summary: description } });
1677
1680
  return snapshot;
1678
1681
  }
1682
+ setSessionTopicGenerating(id, titleGenerating) {
1683
+ const record = this.sessions.get(id);
1684
+ if (!record || record.titleGenerating === titleGenerating)
1685
+ return;
1686
+ record.titleGenerating = titleGenerating;
1687
+ this.emitEvent({ type: "output", sessionId: id, data: { titleGenerating } });
1688
+ }
1679
1689
  /**
1680
1690
  * Persist worktree merge progress through the manager that owns the live
1681
1691
  * session record. Returning null lets callers fall back to another owner (or
@@ -1703,16 +1713,26 @@ export class ProcessManager extends EventEmitter {
1703
1713
  maybeGenerateSessionTopic(id, input) {
1704
1714
  const prompt = input.trim();
1705
1715
  const record = this.sessions.get(id);
1706
- if (this.disposed || !prompt || !record || record.title || this.topicRequests.has(id))
1716
+ if (this.disposed || !prompt || !record)
1707
1717
  return;
1708
- this.topicRequests.add(id);
1709
- void generateSessionTopic(prompt, record.cwd, this.config.language, this.config.systemAi)
1710
- .then(({ title, description }) => {
1711
- if (!this.disposed && this.sessions.has(id))
1712
- this.setSessionTopic(id, title, description);
1713
- })
1714
- .catch((error) => console.error(`[ProcessManager] Failed to generate session topic ${id}:`, getErrorMessage(error)))
1715
- .finally(() => this.topicRequests.delete(id));
1718
+ this.topicCoordinator.request(id, {
1719
+ messages: snapshotMessages(record),
1720
+ input: prompt,
1721
+ cwd: record.cwd,
1722
+ language: this.config.language,
1723
+ ai: resolveCommitAiContext(record, this.config),
1724
+ onGenerating: (generating) => {
1725
+ if (!this.disposed)
1726
+ this.setSessionTopicGenerating(id, generating);
1727
+ },
1728
+ onTopic: ({ title, description }) => {
1729
+ if (!this.disposed && this.sessions.has(id))
1730
+ this.setSessionTopic(id, title, description);
1731
+ },
1732
+ onError: (error) => {
1733
+ console.error(`[ProcessManager] Failed to generate session topic ${id}:`, getErrorMessage(error));
1734
+ },
1735
+ });
1716
1736
  }
1717
1737
  defaultAutonomyPolicy(mode) {
1718
1738
  if (mode === "agent" || mode === "agent-max" || mode === "managed" || mode === "native" || mode === "full-access") {
@@ -1,6 +1,32 @@
1
- import type { SystemAiConfig } from "./types.js";
1
+ import { type QuickCommitAiOptions } from "./git-quick-commit.js";
2
+ import type { ConversationTurn } from "./types.js";
2
3
  export interface SessionTopic {
3
4
  title: string;
4
5
  description: string;
5
6
  }
6
- export declare function generateSessionTopic(userMessage: string, cwd?: string, language?: string, systemAi?: SystemAiConfig): Promise<SessionTopic>;
7
+ export interface SessionTopicRequest {
8
+ messages?: readonly ConversationTurn[];
9
+ input: string;
10
+ cwd?: string;
11
+ language?: string;
12
+ ai?: QuickCommitAiOptions;
13
+ onGenerating(generating: boolean): void;
14
+ onTopic(topic: SessionTopic): void;
15
+ onError(error: unknown): void;
16
+ }
17
+ /** Collect every completed user turn plus the newly submitted input in order. */
18
+ export declare function collectSessionTopicMessages(messages: readonly ConversationTurn[] | undefined, input: string): string[];
19
+ export declare function generateSessionTopic(userMessages: readonly string[], cwd?: string, language?: string, ai?: QuickCommitAiOptions): Promise<SessionTopic>;
20
+ /**
21
+ * Coalesces title refreshes per session. A newer user message invalidates an
22
+ * in-flight result and is summarized together with every earlier user turn.
23
+ */
24
+ export declare class SessionTopicCoordinator {
25
+ private readonly generate;
26
+ private readonly states;
27
+ private disposed;
28
+ constructor(generate?: typeof generateSessionTopic);
29
+ request(sessionId: string, request: SessionTopicRequest): void;
30
+ clear(): void;
31
+ private run;
32
+ }
@@ -1,6 +1,4 @@
1
- import { runClaudePrint } from "./claude-sdk-runner.js";
2
- import { callSystemAiText } from "./system-ai.js";
3
- const TOPIC_TIMEOUT_MS = 45_000;
1
+ import { callConfiguredAiText } from "./git-quick-commit.js";
4
2
  const MAX_PROMPT_LENGTH = 12_000;
5
3
  function cleanTopicText(value, maxLength) {
6
4
  if (typeof value !== "string")
@@ -21,24 +19,123 @@ function parseTopic(raw) {
21
19
  return null;
22
20
  }
23
21
  }
24
- export async function generateSessionTopic(userMessage, cwd, language, systemAi) {
25
- const input = userMessage.trim().slice(0, MAX_PROMPT_LENGTH);
22
+ function turnText(turn) {
23
+ return turn.content
24
+ .filter((block) => block.type === "text")
25
+ .map((block) => block.text.trim())
26
+ .filter(Boolean)
27
+ .join("\n")
28
+ .trim();
29
+ }
30
+ /** Collect every completed user turn plus the newly submitted input in order. */
31
+ export function collectSessionTopicMessages(messages, input) {
32
+ const collected = (messages ?? [])
33
+ .filter((turn) => turn.role === "user")
34
+ .map(turnText)
35
+ .filter(Boolean);
36
+ const next = input.trim();
37
+ if (next)
38
+ collected.push(next);
39
+ return collected;
40
+ }
41
+ function topicConversationInput(userMessages) {
42
+ const sections = userMessages.map((message, index) => `第 ${index + 1} 轮:\n${message.trim()}`);
43
+ const kept = [];
44
+ let remaining = MAX_PROMPT_LENGTH;
45
+ for (let index = sections.length - 1; index >= 0 && remaining > 0; index -= 1) {
46
+ const section = sections[index];
47
+ const value = section.length <= remaining ? section : section.slice(section.length - remaining);
48
+ kept.unshift(value);
49
+ remaining -= value.length + 2;
50
+ }
51
+ return kept.join("\n\n");
52
+ }
53
+ export async function generateSessionTopic(userMessages, cwd, language, ai = {}) {
54
+ const input = topicConversationInput(userMessages);
55
+ if (!input)
56
+ throw new Error("没有可总结的用户消息。");
26
57
  const outputLanguage = language?.trim() || "与用户消息相同的语言";
27
58
  const prompt = [
28
- "请总结下面这条用户发给编码助手的首条消息,用于会话列表展示。",
59
+ "请综合总结下面这段用户与编码助手对话中的所有用户消息,用于会话列表展示。",
29
60
  `使用${outputLanguage}输出。`,
30
61
  "只输出一个 JSON 对象,不要 Markdown、解释或额外文字。",
31
62
  '格式:{"title":"不超过20个字的具体主题标题","description":"不超过60个字的一句话任务描述"}',
32
- "标题避免使用“关于”“请求”“任务”等空泛词,描述保留最重要的目标和对象。",
63
+ "标题避免使用“关于”“请求”“任务”等空泛词,描述保留当前整体目标、关键对象和新增要求。",
64
+ "后续轮次与前面目标有关时必须共同概括;发生目标切换时优先反映最新目标,同时保留仍有效的上下文。",
33
65
  "",
34
- "用户消息:",
66
+ "按发送顺序排列的用户消息:",
35
67
  input,
36
68
  ].join("\n");
37
- const raw = systemAi?.enabled
38
- ? await callSystemAiText(prompt, systemAi, TOPIC_TIMEOUT_MS)
39
- : await runClaudePrint(prompt, { cwd, timeoutMs: TOPIC_TIMEOUT_MS, language });
69
+ const raw = await callConfiguredAiText(prompt, cwd ?? process.cwd(), language ?? "", ai);
40
70
  const topic = parseTopic(raw);
41
71
  if (!topic)
42
72
  throw new Error("模型返回的会话主题格式无效。");
43
73
  return topic;
44
74
  }
75
+ /**
76
+ * Coalesces title refreshes per session. A newer user message invalidates an
77
+ * in-flight result and is summarized together with every earlier user turn.
78
+ */
79
+ export class SessionTopicCoordinator {
80
+ generate;
81
+ states = new Map();
82
+ disposed = false;
83
+ constructor(generate = generateSessionTopic) {
84
+ this.generate = generate;
85
+ }
86
+ request(sessionId, request) {
87
+ if (this.disposed)
88
+ return;
89
+ const input = request.input.trim();
90
+ if (!input)
91
+ return;
92
+ let state = this.states.get(sessionId);
93
+ if (!state) {
94
+ state = {
95
+ revision: 0,
96
+ running: false,
97
+ userMessages: collectSessionTopicMessages(request.messages, input),
98
+ request,
99
+ };
100
+ this.states.set(sessionId, state);
101
+ }
102
+ else {
103
+ state.userMessages.push(input);
104
+ state.request = request;
105
+ }
106
+ state.revision += 1;
107
+ if (!state.running) {
108
+ state.running = true;
109
+ request.onGenerating(true);
110
+ void this.run(sessionId, state);
111
+ }
112
+ }
113
+ clear() {
114
+ this.disposed = true;
115
+ this.states.clear();
116
+ }
117
+ async run(sessionId, state) {
118
+ const revision = state.revision;
119
+ const request = state.request;
120
+ const userMessages = state.userMessages.slice();
121
+ try {
122
+ const topic = await this.generate(userMessages, request.cwd, request.language, request.ai);
123
+ if (!this.disposed && this.states.get(sessionId) === state && state.revision === revision) {
124
+ state.request.onTopic(topic);
125
+ }
126
+ }
127
+ catch (error) {
128
+ if (!this.disposed && this.states.get(sessionId) === state && state.revision === revision) {
129
+ state.request.onError(error);
130
+ }
131
+ }
132
+ if (this.disposed || this.states.get(sessionId) !== state)
133
+ return;
134
+ if (state.revision !== revision) {
135
+ await this.run(sessionId, state);
136
+ return;
137
+ }
138
+ state.running = false;
139
+ state.request.onGenerating(false);
140
+ }
141
+ }
@@ -78,7 +78,7 @@ export declare class StructuredSessionManager {
78
78
  private readonly seenIdempotencyKeys;
79
79
  private emitEvent;
80
80
  private archiveTimer;
81
- private readonly topicRequests;
81
+ private readonly topicCoordinator;
82
82
  private readonly streamEmitTimers;
83
83
  private readonly claudeCliRunner;
84
84
  private readonly codexRunner;
@@ -105,6 +105,7 @@ export declare class StructuredSessionManager {
105
105
  listSlim(): SessionSnapshot[];
106
106
  get(id: string): SessionSnapshot | null;
107
107
  setSessionTopic(id: string, title: string, description: string): SessionSnapshot;
108
+ private setSessionTopicGenerating;
108
109
  /**
109
110
  * Update worktree merge progress on the canonical in-memory snapshot before
110
111
  * persisting it. A null result means this manager does not own the session.
@@ -5,8 +5,9 @@ import { truncateMessagesForTransport } from "./message-truncator.js";
5
5
  import { buildChildEnv } from "./env-utils.js";
6
6
  import { getErrorMessage } from "./error-utils.js";
7
7
  import { resolveSdkClaudeBinary } from "./claude-sdk-runner.js";
8
- import { generateSessionTopic } from "./session-topic.js";
8
+ import { SessionTopicCoordinator } from "./session-topic.js";
9
9
  import { resolveSessionCwd } from "./session-cwd.js";
10
+ import { resolveCommitAiContext } from "./session-ai-context.js";
10
11
  import { CodexRunner } from "./structured-codex-adapter.js";
11
12
  import { normalizeStructuredToolResultContent } from "./structured-content.js";
12
13
  import { buildAppendSystemPromptParts, buildClaudeSdkThinking, ClaudeCliRunner, derivePermissionPolicy, } from "./structured-claude-adapter.js";
@@ -198,7 +199,7 @@ export class StructuredSessionManager {
198
199
  seenIdempotencyKeys = new Map();
199
200
  emitEvent = null;
200
201
  archiveTimer = null;
201
- topicRequests = new Set();
202
+ topicCoordinator = new SessionTopicCoordinator();
202
203
  streamEmitTimers = new Set();
203
204
  claudeCliRunner;
204
205
  codexRunner;
@@ -252,6 +253,7 @@ export class StructuredSessionManager {
252
253
  activeRequestId: null,
253
254
  },
254
255
  selectedModel: snapshot.selectedModel ?? null,
256
+ titleGenerating: false,
255
257
  };
256
258
  this.sessions.set(restored.id, restored);
257
259
  this.storage.saveSession(restored);
@@ -347,7 +349,7 @@ export class StructuredSessionManager {
347
349
  this.streamCheckpointTimers.clear();
348
350
  this.streamCheckpointDirty.clear();
349
351
  this.lastStreamSaveAt.clear();
350
- this.topicRequests.clear();
352
+ this.topicCoordinator.clear();
351
353
  this.emitEvent = null;
352
354
  }
353
355
  trackStreamEmitTimer(timer) {
@@ -452,10 +454,18 @@ export class StructuredSessionManager {
452
454
  const current = this.requireSession(id);
453
455
  const updated = { ...current, title, description, summary: description };
454
456
  this.sessions.set(id, updated);
455
- this.storage.updateSessionRuntimeMetadata(updated);
457
+ this.storage.updateSessionRuntimeMetadata({ ...updated, titleGenerating: undefined });
456
458
  this.emitStructuredSnapshot(updated);
457
459
  return updated;
458
460
  }
461
+ setSessionTopicGenerating(id, titleGenerating) {
462
+ const current = this.sessions.get(id);
463
+ if (!current || current.titleGenerating === titleGenerating)
464
+ return;
465
+ const updated = { ...current, titleGenerating };
466
+ this.sessions.set(id, updated);
467
+ this.emit({ type: "output", sessionId: id, data: { sessionKind: "structured", titleGenerating } });
468
+ }
459
469
  /**
460
470
  * Update worktree merge progress on the canonical in-memory snapshot before
461
471
  * persisting it. A null result means this manager does not own the session.
@@ -484,16 +494,26 @@ export class StructuredSessionManager {
484
494
  }
485
495
  maybeGenerateSessionTopic(id, input) {
486
496
  const session = this.sessions.get(id);
487
- if (this.disposed || !session || session.title || this.topicRequests.has(id))
497
+ if (this.disposed || !session || !input.trim())
488
498
  return;
489
- this.topicRequests.add(id);
490
- void generateSessionTopic(input, session.cwd, this.config.language, this.config.systemAi)
491
- .then(({ title, description }) => {
492
- if (!this.disposed && this.sessions.has(id))
493
- this.setSessionTopic(id, title, description);
494
- })
495
- .catch((error) => console.error(`[StructuredSessionManager] Failed to generate session topic ${id}:`, getErrorMessage(error)))
496
- .finally(() => this.topicRequests.delete(id));
499
+ this.topicCoordinator.request(id, {
500
+ messages: session.messages,
501
+ input,
502
+ cwd: session.cwd,
503
+ language: this.config.language,
504
+ ai: resolveCommitAiContext(session, this.config),
505
+ onGenerating: (generating) => {
506
+ if (!this.disposed)
507
+ this.setSessionTopicGenerating(id, generating);
508
+ },
509
+ onTopic: ({ title, description }) => {
510
+ if (!this.disposed && this.sessions.has(id))
511
+ this.setSessionTopic(id, title, description);
512
+ },
513
+ onError: (error) => {
514
+ console.error(`[StructuredSessionManager] Failed to generate session topic ${id}:`, getErrorMessage(error));
515
+ },
516
+ });
497
517
  }
498
518
  createSession(options) {
499
519
  if (this.disposed)
@@ -570,7 +590,6 @@ export class StructuredSessionManager {
570
590
  const prompt = input.trim();
571
591
  if (!prompt)
572
592
  return session;
573
- this.maybeGenerateSessionTopic(id, prompt);
574
593
  if (opts?.idempotencyKey) {
575
594
  const mapKey = `${id}:${opts.idempotencyKey}`;
576
595
  if (this.seenIdempotencyKeys.has(mapKey)) {
@@ -587,6 +606,7 @@ export class StructuredSessionManager {
587
606
  }
588
607
  }
589
608
  }
609
+ this.maybeGenerateSessionTopic(id, prompt);
590
610
  if (session.structuredState?.inFlight) {
591
611
  const runnerExecution = this.pendingRunnerExecutions.get(id);
592
612
  const sdkAbort = this.pendingSdkAbort.get(id);
package/dist/types.d.ts CHANGED
@@ -525,10 +525,12 @@ export interface SessionSnapshot {
525
525
  };
526
526
  /** 会话摘要:从首条用户消息或当前任务提取 */
527
527
  summary?: string;
528
- /** 由模型根据首条用户消息生成的短标题。 */
528
+ /** 由模型根据全部用户消息生成的短标题。 */
529
529
  title?: string;
530
- /** 由模型根据首条用户消息生成的一句话描述。 */
530
+ /** 由模型根据全部用户消息生成的一句话描述。 */
531
531
  description?: string;
532
+ /** 会话标题正在根据最新消息重新生成;仅用于实时展示,重启时强制清空。 */
533
+ titleGenerating?: boolean;
532
534
  /** 当前正在执行的任务标题(用于会话列表展示) */
533
535
  currentTaskTitle?: string;
534
536
  /** 用户为此会话选定的 Claude 模型(别名或完整 ID)。结构化会话下次 spawn 时使用;PTY 会话仅用于展示。 */