@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.
- package/dist/build-info.json +3 -3
- package/dist/process-manager.js +8 -2
- package/dist/server-workspace-routes.js +22 -6
- package/dist/session-topic.d.ts +26 -0
- package/dist/session-topic.js +73 -4
- package/dist/session-transport.d.ts +1 -1
- package/dist/session-transport.js +10 -4
- package/dist/structured-session-manager.js +8 -2
- package/dist/web-ui/content/scripts.js +70 -70
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-08-
|
|
4
|
-
"version": "4.
|
|
2
|
+
"commit": "9ef09d524d2ae51803783b8064ebeee58110edf4",
|
|
3
|
+
"builtAt": "2026-08-24T01:04:59.373Z",
|
|
4
|
+
"version": "4.50.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
package/dist/process-manager.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
// 改名 / 状态
|
package/dist/session-topic.d.ts
CHANGED
|
@@ -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>;
|
package/dist/session-topic.js
CHANGED
|
@@ -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,
|
|
22
|
-
const description = cleanTopicText(parsed.description,
|
|
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
|
|
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
|
-
|
|
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));
|