@co0ontty/wand 1.58.2 → 1.59.0-beta.g811865c

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": "b8a4c751866ffa12a95f41dae242e33fa4df73f1",
3
- "builtAt": "2026-06-13T00:21:20.030Z",
4
- "version": "1.58.2",
5
- "channel": "stable"
2
+ "commit": "811865c1d2a63c20c1134e9048b8db5dcbed829c",
3
+ "builtAt": "2026-06-13T04:12:19.582Z",
4
+ "version": "1.59.0-beta.g811865c",
5
+ "channel": "beta"
6
6
  }
@@ -49,6 +49,7 @@ export declare class ProcessManager extends EventEmitter {
49
49
  private readonly lastPersistedMessageState;
50
50
  /** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数(旧服务器进程已死) */
51
51
  private orphanRecoveredCount;
52
+ private readonly topicRequests;
52
53
  constructor(config: WandConfig, storage: WandStorage, configDir?: string);
53
54
  on(event: "process", listener: ProcessEventHandler): this;
54
55
  /** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数量(仅用于启动摘要展示)。 */
@@ -109,6 +110,8 @@ export declare class ProcessManager extends EventEmitter {
109
110
  /** Lightweight snapshot for list views — omits output and messages. */
110
111
  private snapshotSlim;
111
112
  private isPermissionBlocked;
113
+ setSessionTopic(id: string, title: string, description: string): SessionSnapshot;
114
+ private maybeGenerateSessionTopic;
112
115
  private defaultAutonomyPolicy;
113
116
  resolveEscalation(id: string, requestId: string, resolution?: "approve_once" | "approve_turn" | "deny"): SessionSnapshot;
114
117
  approvePermission(id: string): SessionSnapshot;
@@ -14,6 +14,8 @@ import { buildLanguageDirective, buildManagedAutonomyDirective } from "./languag
14
14
  import { prepareSessionWorktree } from "./git-worktree.js";
15
15
  import { getCodexResumeCommandSessionId, getResumeCommandSessionId } from "./resume-policy.js";
16
16
  import { applyThinkingEffortToPrompt, normalizeThinkingEffort } from "./structured-session-manager.js";
17
+ import { generateSessionTopic } from "./session-topic.js";
18
+ import { getErrorMessage } from "./error-utils.js";
17
19
  function resolveProviderFromCommand(command) {
18
20
  return /^codex\b/.test(command.trim()) ? "codex" : "claude";
19
21
  }
@@ -568,6 +570,7 @@ export class ProcessManager extends EventEmitter {
568
570
  lastPersistedMessageState = new Map();
569
571
  /** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数(旧服务器进程已死) */
570
572
  orphanRecoveredCount = 0;
573
+ topicRequests = new Set();
571
574
  constructor(config, storage, configDir) {
572
575
  super();
573
576
  this.config = config;
@@ -930,6 +933,8 @@ export class ProcessManager extends EventEmitter {
930
933
  });
931
934
  }
932
935
  this.emitEvent({ type: "started", sessionId: id, data: this.snapshot(record) });
936
+ if (initialInput)
937
+ this.maybeGenerateSessionTopic(id, initialInput);
933
938
  let initialInputSent = false;
934
939
  const sendInitialInput = () => {
935
940
  if (initialInputSent || !initialInput)
@@ -1231,6 +1236,8 @@ export class ProcessManager extends EventEmitter {
1231
1236
  console.error(`[ProcessManager] Rejecting input: session ${id} has no PTY`);
1232
1237
  throw new SessionInputError("Session is not running.", "SESSION_NO_PTY", id, record.status);
1233
1238
  }
1239
+ if (view !== "terminal")
1240
+ this.maybeGenerateSessionTopic(id, input);
1234
1241
  // Log shortcut key interactions for auto-confirm and mode analysis
1235
1242
  if (shortcutKey) {
1236
1243
  const outputLines = record.output.split("\n");
@@ -1522,7 +1529,9 @@ export class ProcessManager extends EventEmitter {
1522
1529
  autoRecovered: record.autoRecovered ?? false,
1523
1530
  autoApprovePermissions: record.autoApprovePermissions || undefined,
1524
1531
  approvalStats: record.approvalStats.total > 0 ? record.approvalStats : undefined,
1525
- summary: deriveSessionSummary(messages, record.currentTask?.title ?? null),
1532
+ summary: record.description ?? deriveSessionSummary(messages, record.currentTask?.title ?? null),
1533
+ title: record.title,
1534
+ description: record.description,
1526
1535
  currentTaskTitle: record.status === "running" ? record.currentTask?.title ?? undefined : undefined,
1527
1536
  selectedModel: record.selectedModel ?? null,
1528
1537
  thinkingEffort: record.thinkingEffort ?? null,
@@ -1542,6 +1551,29 @@ export class ProcessManager extends EventEmitter {
1542
1551
  isPermissionBlocked(record) {
1543
1552
  return record.ptyPermissionBlocked || record.pendingEscalation !== null;
1544
1553
  }
1554
+ setSessionTopic(id, title, description) {
1555
+ const record = this.mustGet(id);
1556
+ record.title = title;
1557
+ record.description = description;
1558
+ const snapshot = this.snapshot(record);
1559
+ this.storage.saveSessionMetadata(snapshot);
1560
+ this.emitEvent({ type: "output", sessionId: id, data: { title, description, summary: description } });
1561
+ return snapshot;
1562
+ }
1563
+ maybeGenerateSessionTopic(id, input) {
1564
+ const prompt = input.trim();
1565
+ const record = this.sessions.get(id);
1566
+ if (!prompt || !record || record.title || this.topicRequests.has(id))
1567
+ return;
1568
+ this.topicRequests.add(id);
1569
+ void generateSessionTopic(prompt, record.cwd, this.config.language)
1570
+ .then(({ title, description }) => {
1571
+ if (this.sessions.has(id))
1572
+ this.setSessionTopic(id, title, description);
1573
+ })
1574
+ .catch((error) => console.error(`[ProcessManager] Failed to generate session topic ${id}:`, getErrorMessage(error)))
1575
+ .finally(() => this.topicRequests.delete(id));
1576
+ }
1545
1577
  defaultAutonomyPolicy(mode) {
1546
1578
  if (mode === "agent" || mode === "agent-max" || mode === "managed" || mode === "native" || mode === "full-access") {
1547
1579
  return "agent";
package/dist/server.js CHANGED
@@ -89,8 +89,8 @@ const DISPLAY_VERSION = BUILD_INFO.version || PKG_VERSION;
89
89
  let cachedGitHubApk = null;
90
90
  let gitHubApkCacheTs = 0;
91
91
  const GITHUB_APK_CACHE_TTL = 10 * 60 * 1000; // 10 minutes
92
- // 客户端代码没变时 CI 会跳过该平台的构建,最新 release 上可能没有 .apk/.dmg;
93
- // 因此不能只查 /releases/latest,要按时间倒序遍历最近的 releases,取第一个带对应产物的。
92
+ // 按时间倒序遍历最近的 releases,取第一个带对应产物的。正常 tag release
93
+ // 会为每个平台出包;这里保留回退以兼容旧 release 或某次平台构建失败。
94
94
  async function fetchGitHubReleaseAssetByExt(ext) {
95
95
  const apiUrl = PKG_REPO_URL.replace("github.com", "api.github.com/repos") + "/releases?per_page=30";
96
96
  const resp = await fetch(apiUrl, {
@@ -118,8 +118,8 @@ async function fetchGitHubLatestApk(forceRefresh = false) {
118
118
  const hit = await fetchGitHubReleaseAssetByExt(".apk");
119
119
  if (!hit)
120
120
  return cachedGitHubApk ?? null;
121
- // 版本号优先从文件名提取:未变化的平台沿用老包,挂它的 release tag 可能更新,
122
- // 用 tag 当版本会让客户端「提示新版、下载到旧包」死循环。
121
+ // 版本号优先从文件名提取;回退到旧 release asset 时不能把当前 release tag
122
+ // 误当成产物版本。
123
123
  const version = extractAndroidApkVersion(hit.asset.name)
124
124
  ?? extractAndroidApkVersion(hit.tagName)
125
125
  ?? hit.tagName.replace(/^v/, "");
@@ -193,7 +193,7 @@ async function fetchGitHubLatestDmg(forceRefresh = false) {
193
193
  const hit = await fetchGitHubReleaseAssetByExt(".dmg");
194
194
  if (!hit)
195
195
  return cachedGitHubDmg ?? null;
196
- // 同 APK:版本号优先从文件名提取,避免沿用老包时把 release tag 当成新版本。
196
+ // 同 APK:版本号优先从文件名提取,避免回退到旧 asset 时把 release tag 当成新版本。
197
197
  const version = extractMacosDmgVersion(hit.asset.name)
198
198
  ?? extractMacosDmgVersion(hit.tagName)
199
199
  ?? hit.tagName.replace(/^v/, "");
@@ -0,0 +1,5 @@
1
+ export interface SessionTopic {
2
+ title: string;
3
+ description: string;
4
+ }
5
+ export declare function generateSessionTopic(userMessage: string, cwd?: string, language?: string): Promise<SessionTopic>;
@@ -0,0 +1,41 @@
1
+ import { runClaudePrint } from "./claude-sdk-runner.js";
2
+ const TOPIC_TIMEOUT_MS = 45_000;
3
+ const MAX_PROMPT_LENGTH = 12_000;
4
+ function cleanTopicText(value, maxLength) {
5
+ if (typeof value !== "string")
6
+ return "";
7
+ return value.replace(/\s+/g, " ").trim().slice(0, maxLength);
8
+ }
9
+ function parseTopic(raw) {
10
+ const json = raw.match(/\{[\s\S]*\}/)?.[0];
11
+ if (!json)
12
+ return null;
13
+ try {
14
+ const parsed = JSON.parse(json);
15
+ const title = cleanTopicText(parsed.title, 40);
16
+ const description = cleanTopicText(parsed.description, 120);
17
+ return title && description ? { title, description } : null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export async function generateSessionTopic(userMessage, cwd, language) {
24
+ const input = userMessage.trim().slice(0, MAX_PROMPT_LENGTH);
25
+ const outputLanguage = language?.trim() || "与用户消息相同的语言";
26
+ const prompt = [
27
+ "请总结下面这条用户发给编码助手的首条消息,用于会话列表展示。",
28
+ `使用${outputLanguage}输出。`,
29
+ "只输出一个 JSON 对象,不要 Markdown、解释或额外文字。",
30
+ '格式:{"title":"不超过20个字的具体主题标题","description":"不超过60个字的一句话任务描述"}',
31
+ "标题避免使用“关于”“请求”“任务”等空泛词,描述保留最重要的目标和对象。",
32
+ "",
33
+ "用户消息:",
34
+ input,
35
+ ].join("\n");
36
+ const raw = await runClaudePrint(prompt, { cwd, timeoutMs: TOPIC_TIMEOUT_MS, language });
37
+ const topic = parseTopic(raw);
38
+ if (!topic)
39
+ throw new Error("模型返回的会话主题格式无效。");
40
+ return topic;
41
+ }
package/dist/storage.js CHANGED
@@ -54,12 +54,12 @@ function mapWorktreeMergeFields(row) {
54
54
  }
55
55
  function sessionSelectFields() {
56
56
  return `id, provider, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, queued_messages, structured_state
57
- , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info`;
57
+ , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description`;
58
58
  }
59
59
  function sessionPersistFields() {
60
60
  return `id, command, cwd, mode, status, exit_code, started_at, ended_at, output
61
61
  , archived, archived_at, claude_session_id, provider, session_kind, runner, messages, queued_messages, structured_state
62
- , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info`;
62
+ , resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description`;
63
63
  }
64
64
  function sessionPersistAssignments() {
65
65
  return `command = excluded.command,
@@ -84,7 +84,9 @@ function sessionPersistAssignments() {
84
84
  worktree_enabled = excluded.worktree_enabled,
85
85
  worktree_info = excluded.worktree_info,
86
86
  worktree_merge_status = excluded.worktree_merge_status,
87
- worktree_merge_info = excluded.worktree_merge_info`;
87
+ worktree_merge_info = excluded.worktree_merge_info,
88
+ title = excluded.title,
89
+ description = excluded.description`;
88
90
  }
89
91
  function sessionMetadataAssignments() {
90
92
  return `command = ?, cwd = ?, mode = ?, status = ?, exit_code = ?,
@@ -92,7 +94,8 @@ function sessionMetadataAssignments() {
92
94
  archived = ?, archived_at = ?, claude_session_id = ?,
93
95
  provider = ?, session_kind = ?, runner = ?, structured_state = ?,
94
96
  resumed_from_session_id = ?, auto_recovered = ?,
95
- worktree_enabled = ?, worktree_info = ?, worktree_merge_status = ?, worktree_merge_info = ?`;
97
+ worktree_enabled = ?, worktree_info = ?, worktree_merge_status = ?, worktree_merge_info = ?,
98
+ title = ?, description = ?`;
96
99
  }
97
100
  function sessionPersistValues(snapshot) {
98
101
  return [
@@ -120,6 +123,8 @@ function sessionPersistValues(snapshot) {
120
123
  serializeWorktreeInfo(snapshot.worktree),
121
124
  snapshot.worktreeMergeStatus ?? null,
122
125
  serializeWorktreeMergeInfo(snapshot.worktreeMergeInfo),
126
+ snapshot.title ?? null,
127
+ snapshot.description ?? null,
123
128
  ];
124
129
  }
125
130
  function sessionMetadataValues(snapshot) {
@@ -145,6 +150,8 @@ function sessionMetadataValues(snapshot) {
145
150
  serializeWorktreeInfo(snapshot.worktree),
146
151
  snapshot.worktreeMergeStatus ?? null,
147
152
  serializeWorktreeMergeInfo(snapshot.worktreeMergeInfo),
153
+ snapshot.title ?? null,
154
+ snapshot.description ?? null,
148
155
  snapshot.id,
149
156
  ];
150
157
  }
@@ -173,6 +180,8 @@ function mapSessionCore(row) {
173
180
  autoRecovered: Boolean(row.auto_recovered),
174
181
  worktreeEnabled: Boolean(row.worktree_enabled),
175
182
  worktree: parseWorktreeInfo(row.worktree_info) ?? null,
183
+ title: row.title ?? undefined,
184
+ description: row.description ?? undefined,
176
185
  ...mapWorktreeMergeFields(row),
177
186
  };
178
187
  }
@@ -214,7 +223,9 @@ const INIT_SQL = `
214
223
  worktree_enabled INTEGER NOT NULL DEFAULT 0,
215
224
  worktree_info TEXT,
216
225
  worktree_merge_status TEXT,
217
- worktree_merge_info TEXT
226
+ worktree_merge_info TEXT,
227
+ title TEXT,
228
+ description TEXT
218
229
  );
219
230
 
220
231
  CREATE TABLE IF NOT EXISTS app_config (
@@ -342,7 +353,7 @@ export class WandStorage {
342
353
  this.db
343
354
  .prepare(`INSERT INTO command_sessions (
344
355
  ${sessionPersistFields()}
345
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
356
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
346
357
  ON CONFLICT(id) DO UPDATE SET
347
358
  ${sessionPersistAssignments()}`)
348
359
  .run(...sessionPersistValues(snapshot));
@@ -428,6 +439,8 @@ const SCHEMA_MIGRATIONS = [
428
439
  ["worktree_info", "ALTER TABLE command_sessions ADD COLUMN worktree_info TEXT"],
429
440
  ["worktree_merge_status", "ALTER TABLE command_sessions ADD COLUMN worktree_merge_status TEXT"],
430
441
  ["worktree_merge_info", "ALTER TABLE command_sessions ADD COLUMN worktree_merge_info TEXT"],
442
+ ["title", "ALTER TABLE command_sessions ADD COLUMN title TEXT"],
443
+ ["description", "ALTER TABLE command_sessions ADD COLUMN description TEXT"],
431
444
  ];
432
445
  function ensureCommandSessionSchema(db) {
433
446
  const columns = db.prepare("PRAGMA table_info(command_sessions)").all();
@@ -68,6 +68,7 @@ export declare class StructuredSessionManager {
68
68
  private readonly seenIdempotencyKeys;
69
69
  private emitEvent;
70
70
  private archiveTimer;
71
+ private readonly topicRequests;
71
72
  constructor(storage: WandStorage, config: WandConfig, logger?: SessionLogger | null);
72
73
  private archiveExpiredSessions;
73
74
  setEventEmitter(emitEvent: (event: ProcessEvent) => void): void;
@@ -82,6 +83,8 @@ export declare class StructuredSessionManager {
82
83
  /** Return lightweight snapshots for the session list (no output/messages). */
83
84
  listSlim(): SessionSnapshot[];
84
85
  get(id: string): SessionSnapshot | null;
86
+ setSessionTopic(id: string, title: string, description: string): SessionSnapshot;
87
+ private maybeGenerateSessionTopic;
85
88
  createSession(options: CreateStructuredSessionOptions): SessionSnapshot;
86
89
  sendMessage(id: string, input: string, opts?: {
87
90
  interrupt?: boolean;
@@ -10,6 +10,7 @@ import { buildChildEnv, isRunningAsRoot } from "./env-utils.js";
10
10
  import { getErrorMessage } from "./error-utils.js";
11
11
  import { resolveSdkClaudeBinary } from "./claude-sdk-runner.js";
12
12
  import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
13
+ import { generateSessionTopic } from "./session-topic.js";
13
14
  function defaultStructuredRunner(provider) {
14
15
  return provider === "codex" ? "codex-cli-exec" : "claude-cli-print";
15
16
  }
@@ -358,6 +359,9 @@ function buildStructuredOutputPayload(snapshot) {
358
359
  queuedMessages: snapshot.queuedMessages,
359
360
  sessionKind: "structured",
360
361
  structuredState: snapshot.structuredState,
362
+ title: snapshot.title,
363
+ description: snapshot.description,
364
+ summary: snapshot.description ?? snapshot.summary,
361
365
  };
362
366
  }
363
367
  function buildIncrementalStructuredPayload(snapshot, cardDefaults) {
@@ -410,6 +414,7 @@ export class StructuredSessionManager {
410
414
  seenIdempotencyKeys = new Map();
411
415
  emitEvent = null;
412
416
  archiveTimer = null;
417
+ topicRequests = new Set();
413
418
  constructor(storage, config, logger = null) {
414
419
  this.storage = storage;
415
420
  this.config = config;
@@ -503,6 +508,27 @@ export class StructuredSessionManager {
503
508
  const s = this.sessions.get(id);
504
509
  return s ? withSummary(s) : null;
505
510
  }
511
+ setSessionTopic(id, title, description) {
512
+ const current = this.requireSession(id);
513
+ const updated = { ...current, title, description, summary: description };
514
+ this.sessions.set(id, updated);
515
+ this.storage.saveSessionMetadata(updated);
516
+ this.emitStructuredSnapshot(updated);
517
+ return updated;
518
+ }
519
+ maybeGenerateSessionTopic(id, input) {
520
+ const session = this.sessions.get(id);
521
+ if (!session || session.title || this.topicRequests.has(id))
522
+ return;
523
+ this.topicRequests.add(id);
524
+ void generateSessionTopic(input, session.cwd, this.config.language)
525
+ .then(({ title, description }) => {
526
+ if (this.sessions.has(id))
527
+ this.setSessionTopic(id, title, description);
528
+ })
529
+ .catch((error) => console.error(`[StructuredSessionManager] Failed to generate session topic ${id}:`, getErrorMessage(error)))
530
+ .finally(() => this.topicRequests.delete(id));
531
+ }
506
532
  createSession(options) {
507
533
  const id = randomUUID();
508
534
  const startedAt = new Date().toISOString();
@@ -562,6 +588,7 @@ export class StructuredSessionManager {
562
588
  const prompt = input.trim();
563
589
  if (!prompt)
564
590
  return session;
591
+ this.maybeGenerateSessionTopic(id, prompt);
565
592
  if (opts?.idempotencyKey) {
566
593
  const mapKey = `${id}:${opts.idempotencyKey}`;
567
594
  if (this.seenIdempotencyKeys.has(mapKey)) {
package/dist/types.d.ts CHANGED
@@ -426,6 +426,10 @@ export interface SessionSnapshot {
426
426
  };
427
427
  /** 会话摘要:从首条用户消息或当前任务提取 */
428
428
  summary?: string;
429
+ /** 由模型根据首条用户消息生成的短标题。 */
430
+ title?: string;
431
+ /** 由模型根据首条用户消息生成的一句话描述。 */
432
+ description?: string;
429
433
  /** 当前正在执行的任务标题(用于会话列表展示) */
430
434
  currentTaskTitle?: string;
431
435
  /** 用户为此会话选定的 Claude 模型(别名或完整 ID)。结构化会话下次 spawn 时使用;PTY 会话仅用于展示。 */