@co0ontty/wand 4.49.0 → 4.50.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": "e521372a025630229f6976b2c6800084e851a8e7",
3
- "builtAt": "2026-08-23T23:10:59.104Z",
4
- "version": "4.49.0",
2
+ "commit": "9ef09d524d2ae51803783b8064ebeee58110edf4",
3
+ "builtAt": "2026-08-24T01:04:59.373Z",
4
+ "version": "4.50.0",
5
5
  "channel": "stable"
6
6
  }
@@ -14,7 +14,7 @@ import { buildLanguageDirective, buildManagedAutonomyDirective } from "./languag
14
14
  import { prepareSessionWorktree } from "./git-worktree.js";
15
15
  import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
16
16
  import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToPiLevel } from "./structured-provider-common.js";
17
- import { SessionTopicCoordinator, shouldGenerateSessionTopicFromPtyInput } from "./session-topic.js";
17
+ import { provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromPtyInput, } from "./session-topic.js";
18
18
  import { getErrorMessage } from "./error-utils.js";
19
19
  import { resolveSystemAiContext } from "./session-ai-context.js";
20
20
  import { resolveSessionCwd } from "./session-cwd.js";
@@ -1925,6 +1925,11 @@ export class ProcessManager extends EventEmitter {
1925
1925
  const record = this.sessions.get(id);
1926
1926
  if (this.disposed || !prompt || !record)
1927
1927
  return;
1928
+ const blockedTitles = sessionTopicBlocklistForSnapshot(record, this.storage);
1929
+ const provisional = provisionalSessionTopic(prompt, blockedTitles);
1930
+ if (provisional && (record.title !== provisional.title || record.description !== provisional.description)) {
1931
+ this.setSessionTopic(id, provisional.title, provisional.description);
1932
+ }
1928
1933
  this.topicCoordinator.request(id, {
1929
1934
  messages: snapshotMessages(record),
1930
1935
  input: prompt,
@@ -1936,8 +1941,9 @@ export class ProcessManager extends EventEmitter {
1936
1941
  this.setSessionTopicGenerating(id, generating);
1937
1942
  },
1938
1943
  onTopic: ({ title, description }) => {
1939
- if (!this.disposed && this.sessions.has(id))
1944
+ if (!this.disposed && this.sessions.has(id) && shouldAcceptGeneratedSessionTitle(title, blockedTitles)) {
1940
1945
  this.setSessionTopic(id, title, description);
1946
+ }
1941
1947
  },
1942
1948
  onError: (error) => {
1943
1949
  console.error(`[ProcessManager] Failed to generate session topic ${id}:`, getErrorMessage(error));
@@ -5,9 +5,17 @@ import { asyncRoute } from "./express-async.js";
5
5
  import { getErrorMessage } from "./error-utils.js";
6
6
  import { expandHomePath } from "./middleware/path-safety.js";
7
7
  import { checkSessionWorktreeMergeabilityAsync, cleanupWorktreeSync, prepareSessionWorktree, resolveWorktreeTargetBranchAsync, } from "./git-worktree.js";
8
+ import { collectSessionTopicBlocklist } from "./session-topic.js";
8
9
  import { resolveSessionDisplayTitle } from "./session-transport.js";
9
10
  import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, } from "./workspace-binding.js";
10
11
  const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
12
+ function workspaceSessionTitle(session, names = {}) {
13
+ return resolveSessionDisplayTitle(session, collectSessionTopicBlocklist({
14
+ taskName: names.taskName,
15
+ workspaceName: names.workspaceName,
16
+ cwd: session.cwd,
17
+ }));
18
+ }
11
19
  function parseDefaultProvider(value) {
12
20
  return typeof value === "string" && PROVIDERS.has(value) ? value : undefined;
13
21
  }
@@ -189,7 +197,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
189
197
  ...workspaceWithCounts(storage, workspace),
190
198
  sessions: storage.listSessionsByWorkspace(workspace.id).map((session) => ({
191
199
  ...session,
192
- title: resolveSessionDisplayTitle(session),
200
+ title: workspaceSessionTitle(session, { workspaceName: workspace.name }),
193
201
  })),
194
202
  });
195
203
  });
@@ -280,13 +288,13 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
280
288
  const sessionLimit = parseBoundedCount(req.query.maxSessions);
281
289
  backfillSessionWorkspaces(storage);
282
290
  const workspaces = storage.listWorkspaces();
283
- const summarize = (session) => ({
291
+ const summarize = (session, names = {}) => ({
284
292
  id: session.id,
285
293
  provider: session.provider,
286
294
  sessionKind: session.sessionKind,
287
295
  runner: session.runner,
288
296
  command: session.command,
289
- title: resolveSessionDisplayTitle(session),
297
+ title: workspaceSessionTitle(session, names),
290
298
  status: session.status,
291
299
  cwd: session.cwd,
292
300
  startedAt: session.startedAt,
@@ -307,7 +315,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
307
315
  const sessions = allSessions
308
316
  .slice(0, sessionLimit ?? undefined)
309
317
  .map((session) => ({
310
- ...summarize(session),
318
+ ...summarize(session, { taskName: task.name, workspaceName: workspace.name }),
311
319
  workspaceTaskId: task.id,
312
320
  }));
313
321
  return {
@@ -357,7 +365,9 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
357
365
  // workspaceId 过滤时,不属于目标目录组的会话直接排除(含合成组)。
358
366
  if (workspaceFilter && group?.workspaceId !== workspaceFilter)
359
367
  continue;
360
- group?.standaloneSessions.push(summarize(session));
368
+ group?.standaloneSessions.push(summarize(session, {
369
+ workspaceName: group.workspaceName,
370
+ }));
361
371
  }
362
372
  res.json([...groups.values()]);
363
373
  });
@@ -499,7 +509,13 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
499
509
  res.json({
500
510
  ...task,
501
511
  cwd: task.worktree?.path ?? workspace?.cwd ?? "",
502
- sessions: storage.listSessionsByWorkspaceTask(task.id),
512
+ sessions: storage.listSessionsByWorkspaceTask(task.id).map((session) => ({
513
+ ...session,
514
+ title: workspaceSessionTitle(session, {
515
+ taskName: task.name,
516
+ workspaceName: workspace?.name,
517
+ }),
518
+ })),
503
519
  });
504
520
  });
505
521
  // 改名 / 状态
@@ -19,6 +19,32 @@ export interface SessionTopicRequest {
19
19
  onTopic(topic: SessionTopic): void;
20
20
  onError(error: unknown): void;
21
21
  }
22
+ export declare function isSameSessionTitle(left: string, right: string): boolean;
23
+ /** Parent task / directory names must not become a terminal's displayed title. */
24
+ export declare function collectSessionTopicBlocklist(input: {
25
+ taskName?: string | null;
26
+ workspaceName?: string | null;
27
+ cwd?: string;
28
+ }): string[];
29
+ export declare function shouldAcceptGeneratedSessionTitle(title: string, blockedTitles?: readonly string[]): boolean;
30
+ /** Immediate title from the user's command, without waiting for the model. */
31
+ export declare function summarizeSessionTitleFromInput(input: string, options?: {
32
+ blockedTitles?: readonly string[];
33
+ }): string;
34
+ export declare function summarizeSessionDescriptionFromInput(input: string): string;
35
+ export declare function provisionalSessionTopic(input: string, blockedTitles?: readonly string[]): SessionTopic | null;
36
+ export declare function sessionTopicBlocklistForSnapshot(snapshot: {
37
+ cwd?: string;
38
+ workspaceTaskId?: string;
39
+ }, storage: {
40
+ getWorkspaceTask(id: string): {
41
+ name: string;
42
+ workspaceId: string;
43
+ } | null;
44
+ getWorkspace(id: string): {
45
+ name: string;
46
+ } | null;
47
+ }): string[];
22
48
  /** Collect every completed user turn plus the newly submitted input in order. */
23
49
  export declare function collectSessionTopicMessages(messages: readonly ConversationTurn[] | undefined, input: string): string[];
24
50
  export declare function generateSessionTopic(userMessages: readonly string[], cwd?: string, language?: string, ai?: QuickCommitAiOptions): Promise<SessionTopic>;
@@ -7,19 +7,87 @@ const MAX_PROMPT_LENGTH = 12_000;
7
7
  export function shouldGenerateSessionTopicFromPtyInput(view, shortcutKey) {
8
8
  return view !== "terminal" || shortcutKey === "enter_text";
9
9
  }
10
+ const TITLE_MAX_LENGTH = 24;
11
+ const DESCRIPTION_MAX_LENGTH = 120;
10
12
  function cleanTopicText(value, maxLength) {
11
13
  if (typeof value !== "string")
12
14
  return "";
13
15
  return value.replace(/\s+/g, " ").trim().slice(0, maxLength);
14
16
  }
17
+ function normalizeComparableTitle(value) {
18
+ return value.replace(/\s+/g, " ").trim().toLowerCase();
19
+ }
20
+ export function isSameSessionTitle(left, right) {
21
+ const first = normalizeComparableTitle(left);
22
+ const second = normalizeComparableTitle(right);
23
+ return Boolean(first && second && first === second);
24
+ }
25
+ /** Parent task / directory names must not become a terminal's displayed title. */
26
+ export function collectSessionTopicBlocklist(input) {
27
+ const cwd = (input.cwd ?? "").replace(/[\\/]+$/, "");
28
+ return [...new Set([input.taskName, input.workspaceName, cwd.split(/[\\/]/).pop()]
29
+ .map((value) => value?.replace(/\s+/g, " ").trim() ?? "")
30
+ .filter(Boolean))];
31
+ }
32
+ export function shouldAcceptGeneratedSessionTitle(title, blockedTitles = []) {
33
+ const cleaned = title.replace(/\s+/g, " ").trim();
34
+ if (!cleaned || cleaned === "会话")
35
+ return false;
36
+ return !blockedTitles.some((blocked) => isSameSessionTitle(cleaned, blocked));
37
+ }
38
+ function clipTitle(value, maxLength = TITLE_MAX_LENGTH) {
39
+ if (value.length <= maxLength)
40
+ return value;
41
+ const sliced = value.slice(0, maxLength);
42
+ const lastSpace = sliced.lastIndexOf(" ");
43
+ if (lastSpace >= Math.floor(maxLength * 0.55))
44
+ return sliced.slice(0, lastSpace);
45
+ return sliced;
46
+ }
47
+ /** Immediate title from the user's command, without waiting for the model. */
48
+ export function summarizeSessionTitleFromInput(input, options = {}) {
49
+ const blockedTitles = options.blockedTitles ?? [];
50
+ const lines = input
51
+ .split(/\r?\n/)
52
+ .map((part) => part.replace(/^#+\s*/, "").replace(/\s+/g, " ").trim())
53
+ .filter(Boolean);
54
+ const fallback = clipTitle(lines[0] ?? "");
55
+ for (const line of lines) {
56
+ const title = clipTitle(line);
57
+ if (shouldAcceptGeneratedSessionTitle(title, blockedTitles))
58
+ return title;
59
+ }
60
+ return fallback;
61
+ }
62
+ export function summarizeSessionDescriptionFromInput(input) {
63
+ return cleanTopicText(input, DESCRIPTION_MAX_LENGTH);
64
+ }
65
+ export function provisionalSessionTopic(input, blockedTitles = []) {
66
+ const title = summarizeSessionTitleFromInput(input, { blockedTitles });
67
+ if (!title)
68
+ return null;
69
+ return {
70
+ title,
71
+ description: summarizeSessionDescriptionFromInput(input) || title,
72
+ };
73
+ }
74
+ export function sessionTopicBlocklistForSnapshot(snapshot, storage) {
75
+ const task = snapshot.workspaceTaskId ? storage.getWorkspaceTask(snapshot.workspaceTaskId) : null;
76
+ const workspace = task ? storage.getWorkspace(task.workspaceId) : null;
77
+ return collectSessionTopicBlocklist({
78
+ taskName: task?.name,
79
+ workspaceName: workspace?.name,
80
+ cwd: snapshot.cwd,
81
+ });
82
+ }
15
83
  function parseTopic(raw) {
16
84
  const json = raw.match(/\{[\s\S]*\}/)?.[0];
17
85
  if (!json)
18
86
  return null;
19
87
  try {
20
88
  const parsed = JSON.parse(json);
21
- const title = cleanTopicText(parsed.title, 40);
22
- const description = cleanTopicText(parsed.description, 120);
89
+ const title = cleanTopicText(parsed.title, TITLE_MAX_LENGTH);
90
+ const description = cleanTopicText(parsed.description, DESCRIPTION_MAX_LENGTH);
23
91
  return title && description ? { title, description } : null;
24
92
  }
25
93
  catch {
@@ -63,11 +131,12 @@ export async function generateSessionTopic(userMessages, cwd, language, ai = {})
63
131
  throw new Error("没有可总结的用户消息。");
64
132
  const outputLanguage = language?.trim() || "与用户消息相同的语言";
65
133
  const prompt = [
66
- "请综合总结下面这段用户与编码助手对话中的所有用户消息,用于会话列表展示。",
134
+ "请综合总结下面这段用户与编码助手对话中的所有用户消息,用于单个终端/会话的列表标题。",
67
135
  `使用${outputLanguage}输出。`,
68
136
  "只输出一个 JSON 对象,不要 Markdown、解释或额外文字。",
69
137
  '格式:{"title":"不超过20个字的具体主题标题","description":"不超过60个字的一句话任务描述"}',
70
- "标题避免使用“关于”“请求”“任务”等空泛词,描述保留当前整体目标、关键对象和新增要求。",
138
+ "标题必须概括该会话用户命令自己的具体工作,不要复述项目名、目录名或上级任务标题,也不要使用“关于”“请求”“任务”等空泛词。",
139
+ "描述保留当前整体目标、关键对象和新增要求。",
71
140
  "后续轮次与前面目标有关时必须共同概括;发生目标切换时优先反映最新目标,同时保留仍有效的上下文。",
72
141
  "",
73
142
  "按发送顺序排列的用户消息:",
@@ -23,7 +23,7 @@ export interface SessionDetailDTO extends SessionBaseDTO {
23
23
  leadingBlockTotal?: number;
24
24
  }
25
25
  /** Resolve every client-visible title on the server so all clients show the same value. */
26
- export declare function resolveSessionDisplayTitle(snapshot: SessionSnapshot): string;
26
+ export declare function resolveSessionDisplayTitle(snapshot: SessionSnapshot, blockedTitles?: readonly string[]): string;
27
27
  export declare function toSessionListItemDTO(snapshot: SessionSnapshot): SessionListItemDTO;
28
28
  export interface SessionDetailDTOOptions {
29
29
  output?: string;
@@ -1,18 +1,24 @@
1
1
  import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
2
+ import { shouldAcceptGeneratedSessionTitle, summarizeSessionTitleFromInput, } from "./session-topic.js";
2
3
  export const SESSION_TRANSPORT_OUTPUT_LIMIT = 200_000;
3
4
  function cleanDisplayTitle(value) {
4
5
  return value?.replace(/\s+/g, " ").trim().slice(0, 40) ?? "";
5
6
  }
6
7
  /** Resolve every client-visible title on the server so all clients show the same value. */
7
- export function resolveSessionDisplayTitle(snapshot) {
8
- for (const candidate of [snapshot.title, snapshot.description, snapshot.summary]) {
8
+ export function resolveSessionDisplayTitle(snapshot, blockedTitles = []) {
9
+ for (const candidate of [snapshot.title, snapshot.description]) {
9
10
  const title = cleanDisplayTitle(candidate);
10
- if (title)
11
+ if (title && shouldAcceptGeneratedSessionTitle(title, blockedTitles))
11
12
  return title;
12
13
  }
14
+ const fromSummary = summarizeSessionTitleFromInput(snapshot.summary ?? "", { blockedTitles });
15
+ if (fromSummary && shouldAcceptGeneratedSessionTitle(fromSummary, blockedTitles))
16
+ return fromSummary;
13
17
  const cwd = snapshot.cwd.replace(/[\\/]+$/, "");
14
18
  const directory = cleanDisplayTitle(cwd.split(/[\\/]/).pop());
15
- return directory || "会话";
19
+ if (directory && shouldAcceptGeneratedSessionTitle(directory, blockedTitles))
20
+ return directory;
21
+ return "会话";
16
22
  }
17
23
  /** Explicit allow-list separating the server's session object from its wire DTO. */
18
24
  function sessionBase(snapshot) {
@@ -5,7 +5,7 @@ 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 { SessionTopicCoordinator } from "./session-topic.js";
8
+ import { provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, } from "./session-topic.js";
9
9
  import { resolveSessionCwd } from "./session-cwd.js";
10
10
  import { resolveSystemAiContext } from "./session-ai-context.js";
11
11
  import { CodexRunner } from "./structured-codex-adapter.js";
@@ -889,6 +889,11 @@ export class StructuredSessionManager {
889
889
  const session = this.sessions.get(id);
890
890
  if (this.disposed || !session || !input.trim())
891
891
  return;
892
+ const blockedTitles = sessionTopicBlocklistForSnapshot(session, this.storage);
893
+ const provisional = provisionalSessionTopic(input, blockedTitles);
894
+ if (provisional && (session.title !== provisional.title || session.description !== provisional.description)) {
895
+ this.setSessionTopic(id, provisional.title, provisional.description);
896
+ }
892
897
  this.topicCoordinator.request(id, {
893
898
  messages: session.messages,
894
899
  input,
@@ -900,8 +905,9 @@ export class StructuredSessionManager {
900
905
  this.setSessionTopicGenerating(id, generating);
901
906
  },
902
907
  onTopic: ({ title, description }) => {
903
- if (!this.disposed && this.sessions.has(id))
908
+ if (!this.disposed && this.sessions.has(id) && shouldAcceptGeneratedSessionTitle(title, blockedTitles)) {
904
909
  this.setSessionTopic(id, title, description);
910
+ }
905
911
  },
906
912
  onError: (error) => {
907
913
  console.error(`[StructuredSessionManager] Failed to generate session topic ${id}:`, getErrorMessage(error));