fix(coding-agent): clear tree navigation compaction state

closes #3688
This commit is contained in:
Mario Zechner
2026-04-25 17:03:09 +02:00
parent 3e7ffff184
commit a363b668ac
3 changed files with 165 additions and 120 deletions

View File

@@ -2711,136 +2711,144 @@ export class AgentSession {
// Set up abort controller for summarization
this._branchSummaryAbortController = new AbortController();
let extensionSummary: { summary: string; details?: unknown } | undefined;
let fromExtension = false;
// Emit session_before_tree event
if (this._extensionRunner.hasHandlers("session_before_tree")) {
const result = (await this._extensionRunner.emit({
type: "session_before_tree",
preparation,
signal: this._branchSummaryAbortController.signal,
})) as SessionBeforeTreeResult | undefined;
try {
let extensionSummary: { summary: string; details?: unknown } | undefined;
let fromExtension = false;
if (result?.cancel) {
return { cancelled: true };
// Emit session_before_tree event
if (this._extensionRunner.hasHandlers("session_before_tree")) {
const result = (await this._extensionRunner.emit({
type: "session_before_tree",
preparation,
signal: this._branchSummaryAbortController.signal,
})) as SessionBeforeTreeResult | undefined;
if (result?.cancel) {
return { cancelled: true };
}
if (result?.summary && options.summarize) {
extensionSummary = result.summary;
fromExtension = true;
}
// Allow extensions to override instructions and label
if (result?.customInstructions !== undefined) {
customInstructions = result.customInstructions;
}
if (result?.replaceInstructions !== undefined) {
replaceInstructions = result.replaceInstructions;
}
if (result?.label !== undefined) {
label = result.label;
}
}
if (result?.summary && options.summarize) {
extensionSummary = result.summary;
fromExtension = true;
// Run default summarizer if needed
let summaryText: string | undefined;
let summaryDetails: unknown;
if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
const model = this.model!;
const { apiKey, headers } = await this._getRequiredRequestAuth(model);
const branchSummarySettings = this.settingsManager.getBranchSummarySettings();
const result = await generateBranchSummary(entriesToSummarize, {
model,
apiKey,
headers,
signal: this._branchSummaryAbortController.signal,
customInstructions,
replaceInstructions,
reserveTokens: branchSummarySettings.reserveTokens,
});
if (result.aborted) {
return { cancelled: true, aborted: true };
}
if (result.error) {
throw new Error(result.error);
}
summaryText = result.summary;
summaryDetails = {
readFiles: result.readFiles || [],
modifiedFiles: result.modifiedFiles || [],
};
} else if (extensionSummary) {
summaryText = extensionSummary.summary;
summaryDetails = extensionSummary.details;
}
// Allow extensions to override instructions and label
if (result?.customInstructions !== undefined) {
customInstructions = result.customInstructions;
}
if (result?.replaceInstructions !== undefined) {
replaceInstructions = result.replaceInstructions;
}
if (result?.label !== undefined) {
label = result.label;
}
}
// Determine the new leaf position based on target type
let newLeafId: string | null;
let editorText: string | undefined;
// Run default summarizer if needed
let summaryText: string | undefined;
let summaryDetails: unknown;
if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
const model = this.model!;
const { apiKey, headers } = await this._getRequiredRequestAuth(model);
const branchSummarySettings = this.settingsManager.getBranchSummarySettings();
const result = await generateBranchSummary(entriesToSummarize, {
model,
apiKey,
headers,
signal: this._branchSummaryAbortController.signal,
customInstructions,
replaceInstructions,
reserveTokens: branchSummarySettings.reserveTokens,
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
// User message: leaf = parent (null if root), text goes to editor
newLeafId = targetEntry.parentId;
editorText = this._extractUserMessageText(targetEntry.message.content);
} else if (targetEntry.type === "custom_message") {
// Custom message: leaf = parent (null if root), text goes to editor
newLeafId = targetEntry.parentId;
editorText =
typeof targetEntry.content === "string"
? targetEntry.content
: targetEntry.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("");
} else {
// Non-user message: leaf = selected node
newLeafId = targetId;
}
// Switch leaf (with or without summary)
// Summary is attached at the navigation target position (newLeafId), not the old branch
let summaryEntry: BranchSummaryEntry | undefined;
if (summaryText) {
// Create summary at target position (can be null for root)
const summaryId = this.sessionManager.branchWithSummary(
newLeafId,
summaryText,
summaryDetails,
fromExtension,
);
summaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry;
// Attach label to the summary entry
if (label) {
this.sessionManager.appendLabelChange(summaryId, label);
}
} else if (newLeafId === null) {
// No summary, navigating to root - reset leaf
this.sessionManager.resetLeaf();
} else {
// No summary, navigating to non-root
this.sessionManager.branch(newLeafId);
}
// Attach label to target entry when not summarizing (no summary entry to label)
if (label && !summaryText) {
this.sessionManager.appendLabelChange(targetId, label);
}
// Update agent state
const sessionContext = this.sessionManager.buildSessionContext();
this.agent.state.messages = sessionContext.messages;
// Emit session_tree event
await this._extensionRunner.emit({
type: "session_tree",
newLeafId: this.sessionManager.getLeafId(),
oldLeafId,
summaryEntry,
fromExtension: summaryText ? fromExtension : undefined,
});
// Emit to custom tools
return { editorText, cancelled: false, summaryEntry };
} finally {
this._branchSummaryAbortController = undefined;
if (result.aborted) {
return { cancelled: true, aborted: true };
}
if (result.error) {
throw new Error(result.error);
}
summaryText = result.summary;
summaryDetails = {
readFiles: result.readFiles || [],
modifiedFiles: result.modifiedFiles || [],
};
} else if (extensionSummary) {
summaryText = extensionSummary.summary;
summaryDetails = extensionSummary.details;
}
// Determine the new leaf position based on target type
let newLeafId: string | null;
let editorText: string | undefined;
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
// User message: leaf = parent (null if root), text goes to editor
newLeafId = targetEntry.parentId;
editorText = this._extractUserMessageText(targetEntry.message.content);
} else if (targetEntry.type === "custom_message") {
// Custom message: leaf = parent (null if root), text goes to editor
newLeafId = targetEntry.parentId;
editorText =
typeof targetEntry.content === "string"
? targetEntry.content
: targetEntry.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("");
} else {
// Non-user message: leaf = selected node
newLeafId = targetId;
}
// Switch leaf (with or without summary)
// Summary is attached at the navigation target position (newLeafId), not the old branch
let summaryEntry: BranchSummaryEntry | undefined;
if (summaryText) {
// Create summary at target position (can be null for root)
const summaryId = this.sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails, fromExtension);
summaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry;
// Attach label to the summary entry
if (label) {
this.sessionManager.appendLabelChange(summaryId, label);
}
} else if (newLeafId === null) {
// No summary, navigating to root - reset leaf
this.sessionManager.resetLeaf();
} else {
// No summary, navigating to non-root
this.sessionManager.branch(newLeafId);
}
// Attach label to target entry when not summarizing (no summary entry to label)
if (label && !summaryText) {
this.sessionManager.appendLabelChange(targetId, label);
}
// Update agent state
const sessionContext = this.sessionManager.buildSessionContext();
this.agent.state.messages = sessionContext.messages;
// Emit session_tree event
await this._extensionRunner.emit({
type: "session_tree",
newLeafId: this.sessionManager.getLeafId(),
oldLeafId,
summaryEntry,
fromExtension: summaryText ? fromExtension : undefined,
});
// Emit to custom tools
this._branchSummaryAbortController = undefined;
return { editorText, cancelled: false, summaryEntry };
}
/**