mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 13:04:08 +00:00
feat(coding-agent): context compaction with /compact, /autocompact, and auto-trigger
- Add /compact command for manual context compaction with optional custom instructions - Add /autocompact command to toggle automatic compaction - Auto-trigger compaction when context usage exceeds threshold (contextWindow - reserveTokens) - Add CompactionComponent for TUI display with collapsed/expanded states - Add compaction events to HTML export with collapsible summary - Refactor export-html.ts to eliminate duplication between session and streaming formats - Use setTimeout to break out of agent event handler for safe async compaction - Show compaction summary in TUI after compaction completes fixes #92
This commit is contained in:
parent
bddb99fa7c
commit
c89b1ec3c2
6 changed files with 803 additions and 1473 deletions
|
|
@ -8,7 +8,7 @@
|
|||
import type { AppMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { AssistantMessage, Model, Usage } from "@mariozechner/pi-ai";
|
||||
import { complete } from "@mariozechner/pi-ai";
|
||||
import { type CompactionEntry, loadSessionFromEntries, type SessionEntry } from "./session-manager.js";
|
||||
import type { CompactionEntry, SessionEntry } from "./session-manager.js";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
|
|
@ -225,8 +225,10 @@ export async function compact(
|
|||
signal?: AbortSignal,
|
||||
customInstructions?: string,
|
||||
): Promise<CompactionEntry> {
|
||||
// Reconstruct current messages from entries
|
||||
const { messages: currentMessages } = loadSessionFromEntries(entries);
|
||||
// Don't compact if the last entry is already a compaction
|
||||
if (entries.length > 0 && entries[entries.length - 1].type === "compaction") {
|
||||
throw new Error("Already compacted");
|
||||
}
|
||||
|
||||
// Find previous compaction boundary
|
||||
let prevCompactionIndex = -1;
|
||||
|
|
@ -246,9 +248,29 @@ export async function compact(
|
|||
// Find cut point (entry index) within the valid range
|
||||
const firstKeptEntryIndex = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
|
||||
|
||||
// Generate summary from the full current context
|
||||
// Extract messages to summarize (before the cut point)
|
||||
const messagesToSummarize: AppMessage[] = [];
|
||||
for (let i = boundaryStart; i < firstKeptEntryIndex; i++) {
|
||||
const entry = entries[i];
|
||||
if (entry.type === "message") {
|
||||
messagesToSummarize.push(entry.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Also include the previous summary if there was a compaction
|
||||
if (prevCompactionIndex >= 0) {
|
||||
const prevCompaction = entries[prevCompactionIndex] as CompactionEntry;
|
||||
// Prepend the previous summary as context
|
||||
messagesToSummarize.unshift({
|
||||
role: "user",
|
||||
content: `Previous session summary:\n${prevCompaction.summary}`,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Generate summary from messages before the cut point
|
||||
const summary = await generateSummary(
|
||||
currentMessages,
|
||||
messagesToSummarize,
|
||||
model,
|
||||
settings.reserveTokens,
|
||||
apiKey,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -72,17 +72,21 @@ export interface LoadedSession {
|
|||
model: { provider: string; modelId: string } | null;
|
||||
}
|
||||
|
||||
const SUMMARY_PREFIX = `Another language model worked on this task and produced a summary. Use this to continue the work without duplicating effort:
|
||||
export const SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
|
||||
|
||||
<summary>
|
||||
`;
|
||||
|
||||
export const SUMMARY_SUFFIX = `
|
||||
</summary>`;
|
||||
|
||||
/**
|
||||
* Create a user message containing the summary with the standard prefix.
|
||||
*/
|
||||
export function createSummaryMessage(summary: string): AppMessage {
|
||||
return {
|
||||
role: "user",
|
||||
content: SUMMARY_PREFIX + summary,
|
||||
content: SUMMARY_PREFIX + summary + SUMMARY_SUFFIX,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
|
@ -115,6 +119,18 @@ export function parseSessionEntries(content: string): SessionEntry[] {
|
|||
* 2. Keep all entries from firstKeptEntryIndex onwards (extracting messages)
|
||||
* 3. Prepend summary as user message
|
||||
*/
|
||||
/**
|
||||
* Get the latest compaction entry from session entries, if any.
|
||||
*/
|
||||
export function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
if (entries[i].type === "compaction") {
|
||||
return entries[i] as CompactionEntry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {
|
||||
// Find model and thinking level (always scan all entries)
|
||||
let thinkingLevel = "off";
|
||||
|
|
|
|||
54
packages/coding-agent/src/tui/compaction.ts
Normal file
54
packages/coding-agent/src/tui/compaction.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Component that renders a compaction indicator with collapsed/expanded state.
|
||||
* Collapsed: shows "Context compacted from X tokens"
|
||||
* Expanded: shows the full summary rendered as markdown (like a user message)
|
||||
*/
|
||||
export class CompactionComponent extends Container {
|
||||
private expanded = false;
|
||||
private tokensBefore: number;
|
||||
private summary: string;
|
||||
|
||||
constructor(tokensBefore: number, summary: string) {
|
||||
super();
|
||||
this.tokensBefore = tokensBefore;
|
||||
this.summary = summary;
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
setExpanded(expanded: boolean): void {
|
||||
this.expanded = expanded;
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
private updateDisplay(): void {
|
||||
this.clear();
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
if (this.expanded) {
|
||||
// Show header + summary as markdown (like user message)
|
||||
const header = `**Context compacted from ${this.tokensBefore.toLocaleString()} tokens**\n\n`;
|
||||
this.addChild(
|
||||
new Markdown(header + this.summary, 1, 1, getMarkdownTheme(), {
|
||||
bgColor: (text: string) => theme.bg("userMessageBg", text),
|
||||
color: (text: string) => theme.fg("userMessageText", text),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// Collapsed: just show the header line with user message styling
|
||||
const isMac = process.platform === "darwin";
|
||||
const shortcut = isMac ? "CMD+O" : "CTRL+O";
|
||||
this.addChild(
|
||||
new Text(
|
||||
theme.fg("userMessageText", `--- Earlier messages compacted (${shortcut} to expand) ---`),
|
||||
1,
|
||||
1,
|
||||
(text: string) => theme.bg("userMessageBg", text),
|
||||
),
|
||||
);
|
||||
}
|
||||
this.addChild(new Spacer(1));
|
||||
}
|
||||
}
|
||||
|
|
@ -22,12 +22,19 @@ import { calculateContextTokens, compact, getLastAssistantUsage, shouldCompact }
|
|||
import { APP_NAME, getDebugLogPath, getModelsPath, getOAuthPath } from "../config.js";
|
||||
import { exportSessionToHtml } from "../export-html.js";
|
||||
import { getApiKeyForModel, getAvailableModels, invalidateOAuthCache } from "../model-config.js";
|
||||
import { listOAuthProviders, login, logout } from "../oauth/index.js";
|
||||
import { loadSessionFromEntries, type SessionManager } from "../session-manager.js";
|
||||
import { listOAuthProviders, login, logout, type SupportedOAuthProvider } from "../oauth/index.js";
|
||||
import {
|
||||
getLatestCompactionEntry,
|
||||
loadSessionFromEntries,
|
||||
type SessionManager,
|
||||
SUMMARY_PREFIX,
|
||||
SUMMARY_SUFFIX,
|
||||
} from "../session-manager.js";
|
||||
import type { SettingsManager } from "../settings-manager.js";
|
||||
import { expandSlashCommand, type FileSlashCommand, loadSlashCommands } from "../slash-commands.js";
|
||||
import { getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "../theme/theme.js";
|
||||
import { AssistantMessageComponent } from "./assistant-message.js";
|
||||
import { CompactionComponent } from "./compaction.js";
|
||||
import { CustomEditor } from "./custom-editor.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { FooterComponent } from "./footer.js";
|
||||
|
|
@ -564,58 +571,7 @@ export class TuiRenderer {
|
|||
if (!shouldCompact(contextTokens, contextWindow, settings)) return;
|
||||
|
||||
// Trigger auto-compaction
|
||||
await this.handleAutoCompaction();
|
||||
}
|
||||
|
||||
private async handleAutoCompaction(): Promise<void> {
|
||||
// Unsubscribe to stop processing events
|
||||
this.unsubscribe?.();
|
||||
|
||||
// Abort current agent run and wait for completion
|
||||
this.agent.abort();
|
||||
await this.agent.waitForIdle();
|
||||
|
||||
// Stop loading animation
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
this.loadingAnimation = null;
|
||||
}
|
||||
this.statusContainer.clear();
|
||||
|
||||
// Show compacting status
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(theme.fg("muted", "Auto-compacting context..."), 1, 1));
|
||||
this.ui.requestRender();
|
||||
|
||||
try {
|
||||
const apiKey = await getApiKeyForModel(this.agent.state.model);
|
||||
if (!apiKey) {
|
||||
throw new Error(`No API key for ${this.agent.state.model.provider}`);
|
||||
}
|
||||
|
||||
const entries = this.sessionManager.loadEntries();
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
const compactionEntry = await compact(entries, this.agent.state.model, settings, apiKey);
|
||||
|
||||
// Save and reload
|
||||
this.sessionManager.saveCompaction(compactionEntry);
|
||||
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
||||
this.agent.replaceMessages(loaded.messages);
|
||||
|
||||
// Rebuild UI
|
||||
this.chatContainer.clear();
|
||||
this.rebuildChatFromMessages();
|
||||
|
||||
this.showSuccess(
|
||||
"✓ Context auto-compacted",
|
||||
`Reduced from ${compactionEntry.tokensBefore.toLocaleString()} tokens`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.showError(`Auto-compaction failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
// Resubscribe
|
||||
this.subscribeToAgent();
|
||||
await this.executeCompaction(undefined, true);
|
||||
}
|
||||
|
||||
private async handleEvent(event: AgentEvent, state: AgentState): Promise<void> {
|
||||
|
|
@ -648,9 +604,12 @@ export class TuiRenderer {
|
|||
case "message_start":
|
||||
if (event.message.role === "user") {
|
||||
// Check if this is a queued message
|
||||
const userMsg = event.message as any;
|
||||
const textBlocks = userMsg.content.filter((c: any) => c.type === "text");
|
||||
const messageText = textBlocks.map((c: any) => c.text).join("");
|
||||
const userMsg = event.message;
|
||||
const textBlocks =
|
||||
typeof userMsg.content === "string"
|
||||
? [{ type: "text", text: userMsg.content }]
|
||||
: userMsg.content.filter((c) => c.type === "text");
|
||||
const messageText = textBlocks.map((c) => c.text).join("");
|
||||
|
||||
const queuedIndex = this.queuedMessages.indexOf(messageText);
|
||||
if (queuedIndex !== -1) {
|
||||
|
|
@ -789,17 +748,20 @@ export class TuiRenderer {
|
|||
|
||||
private addMessageToChat(message: Message): void {
|
||||
if (message.role === "user") {
|
||||
const userMsg = message as any;
|
||||
const userMsg = message;
|
||||
// Extract text content from content blocks
|
||||
const textBlocks = userMsg.content.filter((c: any) => c.type === "text");
|
||||
const textContent = textBlocks.map((c: any) => c.text).join("");
|
||||
const textBlocks =
|
||||
typeof userMsg.content === "string"
|
||||
? [{ type: "text", text: userMsg.content }]
|
||||
: userMsg.content.filter((c) => c.type === "text");
|
||||
const textContent = textBlocks.map((c) => c.text).join("");
|
||||
if (textContent) {
|
||||
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
||||
this.chatContainer.addChild(userComponent);
|
||||
this.isFirstUserMessage = false;
|
||||
}
|
||||
} else if (message.role === "assistant") {
|
||||
const assistantMsg = message as AssistantMessage;
|
||||
const assistantMsg = message;
|
||||
|
||||
// Add assistant message component
|
||||
const assistantComponent = new AssistantMessageComponent(assistantMsg);
|
||||
|
|
@ -819,18 +781,32 @@ export class TuiRenderer {
|
|||
// Update editor border color based on current thinking level
|
||||
this.updateEditorBorderColor();
|
||||
|
||||
// Get compaction info if any
|
||||
const compactionEntry = getLatestCompactionEntry(this.sessionManager.loadEntries());
|
||||
|
||||
// Render messages
|
||||
for (let i = 0; i < state.messages.length; i++) {
|
||||
const message = state.messages[i];
|
||||
|
||||
if (message.role === "user") {
|
||||
const userMsg = message as any;
|
||||
const textBlocks = userMsg.content.filter((c: any) => c.type === "text");
|
||||
const textContent = textBlocks.map((c: any) => c.text).join("");
|
||||
const userMsg = message;
|
||||
const textBlocks =
|
||||
typeof userMsg.content === "string"
|
||||
? [{ type: "text", text: userMsg.content }]
|
||||
: userMsg.content.filter((c) => c.type === "text");
|
||||
const textContent = textBlocks.map((c) => c.text).join("");
|
||||
if (textContent) {
|
||||
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
||||
this.chatContainer.addChild(userComponent);
|
||||
this.isFirstUserMessage = false;
|
||||
// Check if this is a compaction summary message
|
||||
if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
|
||||
const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
|
||||
const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
} else {
|
||||
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
||||
this.chatContainer.addChild(userComponent);
|
||||
this.isFirstUserMessage = false;
|
||||
}
|
||||
}
|
||||
} else if (message.role === "assistant") {
|
||||
const assistantMsg = message as AssistantMessage;
|
||||
|
|
@ -892,18 +868,32 @@ export class TuiRenderer {
|
|||
this.isFirstUserMessage = true;
|
||||
this.pendingTools.clear();
|
||||
|
||||
// Get compaction info if any
|
||||
const compactionEntry = getLatestCompactionEntry(this.sessionManager.loadEntries());
|
||||
|
||||
for (const message of this.agent.state.messages) {
|
||||
if (message.role === "user") {
|
||||
const userMsg = message as any;
|
||||
const textBlocks = userMsg.content.filter((c: any) => c.type === "text");
|
||||
const textContent = textBlocks.map((c: any) => c.text).join("");
|
||||
const userMsg = message;
|
||||
const textBlocks =
|
||||
typeof userMsg.content === "string"
|
||||
? [{ type: "text", text: userMsg.content }]
|
||||
: userMsg.content.filter((c) => c.type === "text");
|
||||
const textContent = textBlocks.map((c) => c.text).join("");
|
||||
if (textContent) {
|
||||
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
||||
this.chatContainer.addChild(userComponent);
|
||||
this.isFirstUserMessage = false;
|
||||
// Check if this is a compaction summary message
|
||||
if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
|
||||
const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
|
||||
const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
} else {
|
||||
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
||||
this.chatContainer.addChild(userComponent);
|
||||
this.isFirstUserMessage = false;
|
||||
}
|
||||
}
|
||||
} else if (message.role === "assistant") {
|
||||
const assistantMsg = message as AssistantMessage;
|
||||
const assistantMsg = message;
|
||||
const assistantComponent = new AssistantMessageComponent(assistantMsg);
|
||||
this.chatContainer.addChild(assistantComponent);
|
||||
|
||||
|
|
@ -1095,10 +1085,12 @@ export class TuiRenderer {
|
|||
private toggleToolOutputExpansion(): void {
|
||||
this.toolOutputExpanded = !this.toolOutputExpanded;
|
||||
|
||||
// Update all tool execution components
|
||||
// Update all tool execution and compaction components
|
||||
for (const child of this.chatContainer.children) {
|
||||
if (child instanceof ToolExecutionComponent) {
|
||||
child.setExpanded(this.toolOutputExpanded);
|
||||
} else if (child instanceof CompactionComponent) {
|
||||
child.setExpanded(this.toolOutputExpanded);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1445,7 +1437,7 @@ export class TuiRenderer {
|
|||
// Create OAuth selector
|
||||
this.oauthSelector = new OAuthSelectorComponent(
|
||||
mode,
|
||||
async (providerId: any) => {
|
||||
async (providerId: string) => {
|
||||
// Hide selector first
|
||||
this.hideOAuthSelector();
|
||||
|
||||
|
|
@ -1457,7 +1449,7 @@ export class TuiRenderer {
|
|||
|
||||
try {
|
||||
await login(
|
||||
providerId,
|
||||
providerId as SupportedOAuthProvider,
|
||||
(url: string) => {
|
||||
// Show auth URL to user
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
|
|
@ -1509,7 +1501,7 @@ export class TuiRenderer {
|
|||
} else {
|
||||
// Handle logout
|
||||
try {
|
||||
await logout(providerId);
|
||||
await logout(providerId as SupportedOAuthProvider);
|
||||
|
||||
// Invalidate OAuth cache so footer updates
|
||||
invalidateOAuthCache();
|
||||
|
|
@ -1707,7 +1699,7 @@ export class TuiRenderer {
|
|||
|
||||
private handleDebugCommand(): void {
|
||||
// Force a render and capture all lines with their widths
|
||||
const width = (this.ui as any).terminal.columns;
|
||||
const width = this.ui.terminal.columns;
|
||||
const allLines = this.ui.render(width);
|
||||
|
||||
const debugLogPath = getDebugLogPath();
|
||||
|
|
@ -1737,16 +1729,13 @@ export class TuiRenderer {
|
|||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
private async handleCompactCommand(customInstructions?: string): Promise<void> {
|
||||
// Check if there are any messages to compact
|
||||
const entries = this.sessionManager.loadEntries();
|
||||
const messageCount = entries.filter((e) => e.type === "message").length;
|
||||
|
||||
if (messageCount < 2) {
|
||||
this.showWarning("Nothing to compact (no messages yet)");
|
||||
return;
|
||||
}
|
||||
private compactionAbortController: AbortController | null = null;
|
||||
|
||||
/**
|
||||
* Shared logic to execute context compaction.
|
||||
* Handles aborting agent, showing loader, performing compaction, updating session/UI.
|
||||
*/
|
||||
private async executeCompaction(customInstructions?: string, isAuto = false): Promise<void> {
|
||||
// Unsubscribe first to prevent processing events during compaction
|
||||
this.unsubscribe?.();
|
||||
|
||||
|
|
@ -1761,9 +1750,27 @@ export class TuiRenderer {
|
|||
}
|
||||
this.statusContainer.clear();
|
||||
|
||||
// Show compacting status
|
||||
// Create abort controller for compaction
|
||||
this.compactionAbortController = new AbortController();
|
||||
|
||||
// Set up escape handler during compaction
|
||||
const originalOnEscape = this.editor.onEscape;
|
||||
this.editor.onEscape = () => {
|
||||
if (this.compactionAbortController) {
|
||||
this.compactionAbortController.abort();
|
||||
}
|
||||
};
|
||||
|
||||
// Show compacting status with loader
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(theme.fg("muted", "Compacting context..."), 1, 1));
|
||||
const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
|
||||
const compactingLoader = new Loader(
|
||||
this.ui,
|
||||
(spinner) => theme.fg("accent", spinner),
|
||||
(text) => theme.fg("muted", text),
|
||||
label,
|
||||
);
|
||||
this.statusContainer.addChild(compactingLoader);
|
||||
this.ui.requestRender();
|
||||
|
||||
try {
|
||||
|
|
@ -1773,17 +1780,23 @@ export class TuiRenderer {
|
|||
throw new Error(`No API key for ${this.agent.state.model.provider}`);
|
||||
}
|
||||
|
||||
// Perform compaction
|
||||
// Perform compaction with abort signal
|
||||
const entries = this.sessionManager.loadEntries();
|
||||
const settings = this.settingsManager.getCompactionSettings();
|
||||
const compactionEntry = await compact(
|
||||
entries,
|
||||
this.agent.state.model,
|
||||
settings,
|
||||
apiKey,
|
||||
undefined,
|
||||
this.compactionAbortController.signal,
|
||||
customInstructions,
|
||||
);
|
||||
|
||||
// Check if aborted after compact returned
|
||||
if (this.compactionAbortController.signal.aborted) {
|
||||
throw new Error("Compaction cancelled");
|
||||
}
|
||||
|
||||
// Save compaction to session
|
||||
this.sessionManager.saveCompaction(compactionEntry);
|
||||
|
||||
|
|
@ -1795,19 +1808,49 @@ export class TuiRenderer {
|
|||
this.chatContainer.clear();
|
||||
this.rebuildChatFromMessages();
|
||||
|
||||
// Show success
|
||||
this.showSuccess(
|
||||
"✓ Context compacted",
|
||||
`Reduced from ${compactionEntry.tokensBefore.toLocaleString()} tokens`,
|
||||
);
|
||||
// Add compaction component at current position so user can see/expand the summary
|
||||
const compactionComponent = new CompactionComponent(compactionEntry.tokensBefore, compactionEntry.summary);
|
||||
compactionComponent.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(compactionComponent);
|
||||
|
||||
// Update footer with new state (fixes context % display)
|
||||
this.footer.updateState(this.agent.state);
|
||||
|
||||
// Show success message
|
||||
const successTitle = isAuto ? "✓ Context auto-compacted" : "✓ Context compacted";
|
||||
this.showSuccess(successTitle, `Reduced from ${compactionEntry.tokensBefore.toLocaleString()} tokens`);
|
||||
} catch (error) {
|
||||
this.showError(`Compaction failed: ${error instanceof Error ? error.message : String(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 {
|
||||
// Clean up
|
||||
compactingLoader.stop();
|
||||
this.statusContainer.clear();
|
||||
this.compactionAbortController = null;
|
||||
this.editor.onEscape = originalOnEscape;
|
||||
}
|
||||
|
||||
// Resubscribe to agent
|
||||
this.subscribeToAgent();
|
||||
}
|
||||
|
||||
private async handleCompactCommand(customInstructions?: string): Promise<void> {
|
||||
// Check if there are any messages to compact
|
||||
const entries = this.sessionManager.loadEntries();
|
||||
const messageCount = entries.filter((e) => e.type === "message").length;
|
||||
|
||||
if (messageCount < 2) {
|
||||
this.showWarning("Nothing to compact (no messages yet)");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.executeCompaction(customInstructions, false);
|
||||
}
|
||||
|
||||
private handleAutocompactCommand(): void {
|
||||
const currentEnabled = this.settingsManager.getCompactionEnabled();
|
||||
const newState = !currentEnabled;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue