@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.
- package/dist/build-info.json +4 -4
- package/dist/git-quick-commit.d.ts +6 -1
- package/dist/git-quick-commit.js +8 -4
- package/dist/process-manager.d.ts +2 -1
- package/dist/process-manager.js +33 -13
- package/dist/session-topic.d.ts +28 -2
- package/dist/session-topic.js +108 -11
- package/dist/structured-session-manager.d.ts +2 -1
- package/dist/structured-session-manager.js +34 -14
- package/dist/types.d.ts +4 -2
- package/dist/web-ui/content/scripts.js +35 -35
- 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-07-
|
|
4
|
-
"version": "4.
|
|
5
|
-
"channel": "
|
|
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. */
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/process-manager.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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.
|
|
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
|
|
1716
|
+
if (this.disposed || !prompt || !record)
|
|
1707
1717
|
return;
|
|
1708
|
-
this.
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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") {
|
package/dist/session-topic.d.ts
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
|
-
import type
|
|
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
|
|
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
|
+
}
|
package/dist/session-topic.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
25
|
-
|
|
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 =
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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 ||
|
|
497
|
+
if (this.disposed || !session || !input.trim())
|
|
488
498
|
return;
|
|
489
|
-
this.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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 会话仅用于展示。 */
|