fix(coding-agent): unify compaction UI events closes #2617
This commit is contained in:
@@ -112,9 +112,10 @@ export function parseSkillBlock(text: string): ParsedSkillBlock | null {
|
||||
/** Session-specific events that extend the core AgentEvent */
|
||||
export type AgentSessionEvent =
|
||||
| AgentEvent
|
||||
| { type: "auto_compaction_start"; reason: "threshold" | "overflow" }
|
||||
| { type: "compaction_start"; reason: "manual" | "threshold" | "overflow" }
|
||||
| {
|
||||
type: "auto_compaction_end";
|
||||
type: "compaction_end";
|
||||
reason: "manual" | "threshold" | "overflow";
|
||||
result: CompactionResult | undefined;
|
||||
aborted: boolean;
|
||||
willRetry: boolean;
|
||||
@@ -1627,6 +1628,7 @@ export class AgentSession {
|
||||
this._disconnectFromAgent();
|
||||
await this.abort();
|
||||
this._compactionAbortController = new AbortController();
|
||||
this._emit({ type: "compaction_start", reason: "manual" });
|
||||
|
||||
try {
|
||||
if (!this.model) {
|
||||
@@ -1719,12 +1721,32 @@ export class AgentSession {
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
const compactionResult = {
|
||||
summary,
|
||||
firstKeptEntryId,
|
||||
tokensBefore,
|
||||
details,
|
||||
};
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
reason: "manual",
|
||||
result: compactionResult,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return compactionResult;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
reason: "manual",
|
||||
result: undefined,
|
||||
aborted,
|
||||
willRetry: false,
|
||||
errorMessage: aborted ? undefined : `Compaction failed: ${message}`,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
this._compactionAbortController = undefined;
|
||||
this._reconnectToAgent();
|
||||
@@ -1787,7 +1809,8 @@ export class AgentSession {
|
||||
if (sameModel && isContextOverflow(assistantMessage, contextWindow)) {
|
||||
if (this._overflowRecoveryAttempted) {
|
||||
this._emit({
|
||||
type: "auto_compaction_end",
|
||||
type: "compaction_end",
|
||||
reason: "overflow",
|
||||
result: undefined,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
@@ -1842,18 +1865,30 @@ export class AgentSession {
|
||||
private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<void> {
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
|
||||
this._emit({ type: "auto_compaction_start", reason });
|
||||
this._emit({ type: "compaction_start", reason });
|
||||
this._autoCompactionAbortController = new AbortController();
|
||||
|
||||
try {
|
||||
if (!this.model) {
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
reason,
|
||||
result: undefined,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model);
|
||||
if (!authResult.ok || !authResult.apiKey) {
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
reason,
|
||||
result: undefined,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { apiKey, headers } = authResult;
|
||||
@@ -1862,7 +1897,13 @@ export class AgentSession {
|
||||
|
||||
const preparation = prepareCompaction(pathEntries, settings);
|
||||
if (!preparation) {
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: false, willRetry: false });
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
reason,
|
||||
result: undefined,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1879,7 +1920,13 @@ export class AgentSession {
|
||||
})) as SessionBeforeCompactResult | undefined;
|
||||
|
||||
if (extensionResult?.cancel) {
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: true, willRetry: false });
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
reason,
|
||||
result: undefined,
|
||||
aborted: true,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1917,7 +1964,13 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
if (this._autoCompactionAbortController.signal.aborted) {
|
||||
this._emit({ type: "auto_compaction_end", result: undefined, aborted: true, willRetry: false });
|
||||
this._emit({
|
||||
type: "compaction_end",
|
||||
reason,
|
||||
result: undefined,
|
||||
aborted: true,
|
||||
willRetry: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1945,7 +1998,7 @@ export class AgentSession {
|
||||
tokensBefore,
|
||||
details,
|
||||
};
|
||||
this._emit({ type: "auto_compaction_end", result, aborted: false, willRetry });
|
||||
this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
|
||||
|
||||
if (willRetry) {
|
||||
const messages = this.agent.state.messages;
|
||||
@@ -1967,7 +2020,8 @@ export class AgentSession {
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
||||
this._emit({
|
||||
type: "auto_compaction_end",
|
||||
type: "compaction_end",
|
||||
reason,
|
||||
result: undefined,
|
||||
aborted: false,
|
||||
willRetry: false,
|
||||
|
||||
@@ -57,7 +57,7 @@ import type {
|
||||
} from "../../core/extensions/index.js";
|
||||
import { FooterDataProvider, type ReadonlyFooterDataProvider } from "../../core/footer-data-provider.js";
|
||||
import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.js";
|
||||
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
||||
|
||||
import { findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
|
||||
import { DefaultPackageManager } from "../../core/package-manager.js";
|
||||
import type { ResourceDiagnostic } from "../../core/resource-loader.js";
|
||||
@@ -1290,10 +1290,8 @@ export class InteractiveMode {
|
||||
compact: (options) => {
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await this.executeCompaction(options?.customInstructions, false);
|
||||
if (result) {
|
||||
options?.onComplete?.(result);
|
||||
}
|
||||
const result = await this.session.compact(options?.customInstructions);
|
||||
options?.onComplete?.(result);
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
options?.onError?.(err);
|
||||
@@ -2398,58 +2396,56 @@ export class InteractiveMode {
|
||||
this.ui.requestRender();
|
||||
break;
|
||||
|
||||
case "auto_compaction_start": {
|
||||
case "compaction_start": {
|
||||
// Keep editor active; submissions are queued during compaction.
|
||||
// Set up escape to abort auto-compaction
|
||||
this.autoCompactionEscapeHandler = this.defaultEditor.onEscape;
|
||||
this.defaultEditor.onEscape = () => {
|
||||
this.session.abortCompaction();
|
||||
};
|
||||
// Show compacting indicator with reason
|
||||
this.statusContainer.clear();
|
||||
const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
|
||||
const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
|
||||
const label =
|
||||
event.reason === "manual"
|
||||
? `Compacting context... ${cancelHint}`
|
||||
: `${event.reason === "overflow" ? "Context overflow detected, " : ""}Auto-compacting... ${cancelHint}`;
|
||||
this.autoCompactionLoader = new Loader(
|
||||
this.ui,
|
||||
(spinner) => theme.fg("accent", spinner),
|
||||
(text) => theme.fg("muted", text),
|
||||
`${reasonText}Auto-compacting... (${keyText("app.interrupt")} to cancel)`,
|
||||
label,
|
||||
);
|
||||
this.statusContainer.addChild(this.autoCompactionLoader);
|
||||
this.ui.requestRender();
|
||||
break;
|
||||
}
|
||||
|
||||
case "auto_compaction_end": {
|
||||
// Restore escape handler
|
||||
case "compaction_end": {
|
||||
if (this.autoCompactionEscapeHandler) {
|
||||
this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
|
||||
this.autoCompactionEscapeHandler = undefined;
|
||||
}
|
||||
// Stop loader
|
||||
if (this.autoCompactionLoader) {
|
||||
this.autoCompactionLoader.stop();
|
||||
this.autoCompactionLoader = undefined;
|
||||
this.statusContainer.clear();
|
||||
}
|
||||
// Handle result
|
||||
if (event.aborted) {
|
||||
this.showStatus("Auto-compaction cancelled");
|
||||
if (event.reason === "manual") {
|
||||
this.showError("Compaction cancelled");
|
||||
} else {
|
||||
this.showStatus("Auto-compaction cancelled");
|
||||
}
|
||||
} else if (event.result) {
|
||||
// Rebuild chat to show compacted state
|
||||
this.chatContainer.clear();
|
||||
this.rebuildChatFromMessages();
|
||||
// Add compaction component at bottom so user sees it without scrolling
|
||||
this.addMessageToChat({
|
||||
role: "compactionSummary",
|
||||
tokensBefore: event.result.tokensBefore,
|
||||
summary: event.result.summary,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
this.footer.invalidate();
|
||||
} else if (event.errorMessage) {
|
||||
// Compaction failed (e.g., quota exceeded, API error)
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
|
||||
if (event.reason === "manual") {
|
||||
this.showError(event.errorMessage);
|
||||
} else {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
|
||||
}
|
||||
}
|
||||
void this.flushCompactionQueue({ willRetry: event.willRetry });
|
||||
this.ui.requestRender();
|
||||
@@ -4573,63 +4569,21 @@ export class InteractiveMode {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.executeCompaction(customInstructions, false);
|
||||
await this.executeCompaction(customInstructions);
|
||||
}
|
||||
|
||||
private async executeCompaction(customInstructions?: string, isAuto = false): Promise<CompactionResult | undefined> {
|
||||
// Stop loading animation
|
||||
private async executeCompaction(customInstructions?: string): Promise<CompactionResult | undefined> {
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
this.loadingAnimation = undefined;
|
||||
}
|
||||
this.statusContainer.clear();
|
||||
|
||||
// Set up escape handler during compaction
|
||||
const originalOnEscape = this.defaultEditor.onEscape;
|
||||
this.defaultEditor.onEscape = () => {
|
||||
this.session.abortCompaction();
|
||||
};
|
||||
|
||||
// Show compacting status
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
|
||||
const label = isAuto ? `Auto-compacting context... ${cancelHint}` : `Compacting context... ${cancelHint}`;
|
||||
const compactingLoader = new Loader(
|
||||
this.ui,
|
||||
(spinner) => theme.fg("accent", spinner),
|
||||
(text) => theme.fg("muted", text),
|
||||
label,
|
||||
);
|
||||
this.statusContainer.addChild(compactingLoader);
|
||||
this.ui.requestRender();
|
||||
|
||||
let result: CompactionResult | undefined;
|
||||
|
||||
try {
|
||||
result = await this.session.compact(customInstructions);
|
||||
|
||||
// Rebuild UI
|
||||
this.rebuildChatFromMessages();
|
||||
|
||||
// Add compaction component at bottom so user sees it without scrolling
|
||||
const msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());
|
||||
this.addMessageToChat(msg);
|
||||
|
||||
this.footer.invalidate();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
|
||||
this.showError("Compaction cancelled");
|
||||
} else {
|
||||
this.showError(`Compaction failed: ${message}`);
|
||||
}
|
||||
} finally {
|
||||
compactingLoader.stop();
|
||||
this.statusContainer.clear();
|
||||
this.defaultEditor.onEscape = originalOnEscape;
|
||||
return await this.session.compact(customInstructions);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
void this.flushCompactionQueue({ willRetry: false });
|
||||
return result;
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
|
||||
Reference in New Issue
Block a user