mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 08:03:39 +00:00
feat: add /clear command to reset context and start fresh session
This commit is contained in:
parent
59af0fd53a
commit
1b6a70ccb1
6 changed files with 131 additions and 54 deletions
|
|
@ -77,6 +77,8 @@ export class Agent {
|
|||
private messageTransformer: (messages: AppMessage[]) => Message[] | Promise<Message[]>;
|
||||
private messageQueue: Array<QueuedMessage<AppMessage>> = [];
|
||||
private queueMode: "all" | "one-at-a-time";
|
||||
private runningPrompt?: Promise<void>;
|
||||
private resolveRunningPrompt?: () => void;
|
||||
|
||||
constructor(opts: AgentOptions) {
|
||||
this._state = { ...this._state, ...opts.initialState };
|
||||
|
|
@ -148,12 +150,37 @@ export class Agent {
|
|||
this.abortController?.abort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the current prompt completes.
|
||||
* Returns immediately resolved promise if no prompt is running.
|
||||
*/
|
||||
waitForIdle(): Promise<void> {
|
||||
return this.runningPrompt ?? Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all messages and state. Call abort() first if a prompt is in flight.
|
||||
*/
|
||||
reset() {
|
||||
this._state.messages = [];
|
||||
this._state.isStreaming = false;
|
||||
this._state.streamMessage = null;
|
||||
this._state.pendingToolCalls = new Set<string>();
|
||||
this._state.error = undefined;
|
||||
this.messageQueue = [];
|
||||
}
|
||||
|
||||
async prompt(input: string, attachments?: Attachment[]) {
|
||||
const model = this._state.model;
|
||||
if (!model) {
|
||||
throw new Error("No model configured");
|
||||
}
|
||||
|
||||
// Set up running prompt tracking
|
||||
this.runningPrompt = new Promise<void>((resolve) => {
|
||||
this.resolveRunningPrompt = resolve;
|
||||
});
|
||||
|
||||
// Build user message with attachments
|
||||
const content: Array<TextContent | ImageContent> = [{ type: "text", text: input }];
|
||||
if (attachments?.length) {
|
||||
|
|
@ -322,6 +349,9 @@ export class Agent {
|
|||
this._state.streamMessage = null;
|
||||
this._state.pendingToolCalls = new Set<string>();
|
||||
this.abortController = undefined;
|
||||
this.resolveRunningPrompt?.();
|
||||
this.runningPrompt = undefined;
|
||||
this.resolveRunningPrompt = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **`/clear` Command**: New slash command to reset the conversation context and start a fresh session. Aborts any in-flight agent work, clears all messages, and creates a new session file. ([#48](https://github.com/badlogic/pi-mono/pull/48))
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Markdown Link Rendering**: Fixed links with identical text and href (e.g., `https://github.com/badlogic/pi-mono/pull/48/files`) being rendered twice. Now correctly compares raw text instead of styled text (which contains ANSI codes) when determining if link text matches href.
|
||||
|
|
|
|||
|
|
@ -445,6 +445,16 @@ Logout from OAuth providers:
|
|||
|
||||
Shows a list of logged-in providers to logout from.
|
||||
|
||||
### /clear
|
||||
|
||||
Clear the conversation context and start a fresh session:
|
||||
|
||||
```
|
||||
/clear
|
||||
```
|
||||
|
||||
Aborts any in-flight agent work, clears all messages, and creates a new session file.
|
||||
|
||||
## Editor Features
|
||||
|
||||
The interactive input editor includes several productivity features:
|
||||
|
|
|
|||
|
|
@ -21,17 +21,6 @@ const __dirname = dirname(__filename);
|
|||
const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
||||
const VERSION = packageJson.version;
|
||||
|
||||
const envApiKeyMap: Record<KnownProvider, string[]> = {
|
||||
google: ["GEMINI_API_KEY"],
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
||||
xai: ["XAI_API_KEY"],
|
||||
groq: ["GROQ_API_KEY"],
|
||||
cerebras: ["CEREBRAS_API_KEY"],
|
||||
openrouter: ["OPENROUTER_API_KEY"],
|
||||
zai: ["ZAI_API_KEY"],
|
||||
};
|
||||
|
||||
const defaultModelPerProvider: Record<KnownProvider, string> = {
|
||||
anthropic: "claude-sonnet-4-5",
|
||||
openai: "gpt-5.1-codex",
|
||||
|
|
@ -455,14 +444,9 @@ async function runInteractiveMode(
|
|||
scopedModels,
|
||||
);
|
||||
|
||||
// Initialize TUI
|
||||
// Initialize TUI (subscribes to agent events internally)
|
||||
await renderer.init();
|
||||
|
||||
// Set interrupt callback
|
||||
renderer.setInterruptCallback(() => {
|
||||
agent.abort();
|
||||
});
|
||||
|
||||
// Render any existing messages (from --continue mode)
|
||||
renderer.renderInitialMessages(agent.state);
|
||||
|
||||
|
|
@ -471,12 +455,6 @@ async function runInteractiveMode(
|
|||
renderer.showWarning(modelFallbackMessage);
|
||||
}
|
||||
|
||||
// Subscribe to agent events
|
||||
agent.subscribe(async (event) => {
|
||||
// Pass all events to the renderer
|
||||
await renderer.handleEvent(event, agent.state);
|
||||
});
|
||||
|
||||
// Interactive loop
|
||||
while (true) {
|
||||
const userInput = await renderer.getUserInput();
|
||||
|
|
@ -683,11 +661,6 @@ export async function main(args: string[]) {
|
|||
// Load previous messages if continuing or resuming
|
||||
// This may update initialModel if restoring from session
|
||||
if (parsed.continue || parsed.resume) {
|
||||
const messages = sessionManager.loadMessages();
|
||||
if (messages.length > 0 && shouldPrintMessages) {
|
||||
console.log(chalk.dim(`Loaded ${messages.length} messages from previous session`));
|
||||
}
|
||||
|
||||
// Load and restore model (overrides initialModel if found and has API key)
|
||||
const savedModel = sessionManager.loadModel();
|
||||
if (savedModel) {
|
||||
|
|
@ -831,9 +804,6 @@ export async function main(args: string[]) {
|
|||
}
|
||||
}
|
||||
|
||||
// Note: Session will be started lazily after first user+assistant message exchange
|
||||
// (unless continuing/resuming, in which case it's already initialized)
|
||||
|
||||
// Log loaded context files (they're already in the system prompt)
|
||||
if (shouldPrintMessages && !parsed.continue && !parsed.resume) {
|
||||
const contextFiles = loadProjectContextFiles();
|
||||
|
|
@ -845,19 +815,6 @@ export async function main(args: string[]) {
|
|||
}
|
||||
}
|
||||
|
||||
// Subscribe to agent events to save messages
|
||||
agent.subscribe((event) => {
|
||||
// Save messages on completion
|
||||
if (event.type === "message_end") {
|
||||
sessionManager.saveMessage(event.message);
|
||||
|
||||
// Check if we should initialize session now (after first user+assistant exchange)
|
||||
if (sessionManager.shouldInitializeSession(agent.state.messages)) {
|
||||
sessionManager.startSession(agent.state);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Route to appropriate mode
|
||||
if (mode === "rpc") {
|
||||
// RPC mode - headless operation
|
||||
|
|
@ -890,8 +847,6 @@ export async function main(args: string[]) {
|
|||
}
|
||||
} else {
|
||||
// Parse current and last versions
|
||||
const currentParts = VERSION.split(".").map(Number);
|
||||
const current = { major: currentParts[0] || 0, minor: currentParts[1] || 0, patch: currentParts[2] || 0 };
|
||||
const changelogPath = getChangelogPath();
|
||||
const entries = parseChangelog(changelogPath);
|
||||
const newEntries = getNewEntries(entries, lastVersion);
|
||||
|
|
|
|||
|
|
@ -97,6 +97,13 @@ export class SessionManager {
|
|||
this.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);
|
||||
}
|
||||
|
||||
/** Reset to a fresh session. Clears pending messages and starts a new session file. */
|
||||
reset(): void {
|
||||
this.pendingMessages = [];
|
||||
this.sessionInitialized = false;
|
||||
this.initNewSession();
|
||||
}
|
||||
|
||||
private findMostRecentlyModifiedSession(): string | null {
|
||||
try {
|
||||
const files = readdirSync(this.sessionDir)
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export class TuiRenderer {
|
|||
private isInitialized = false;
|
||||
private onInputCallback?: (text: string) => void;
|
||||
private loadingAnimation: Loader | null = null;
|
||||
private onInterruptCallback?: () => void;
|
||||
|
||||
private lastSigintTime = 0;
|
||||
private changelogMarkdown: string | null = null;
|
||||
private newVersion: string | null = null;
|
||||
|
|
@ -94,6 +94,9 @@ export class TuiRenderer {
|
|||
// Tool output expansion state
|
||||
private toolOutputExpanded = false;
|
||||
|
||||
// Agent subscription unsubscribe function
|
||||
private unsubscribe?: () => void;
|
||||
|
||||
constructor(
|
||||
agent: Agent,
|
||||
sessionManager: SessionManager,
|
||||
|
|
@ -170,6 +173,11 @@ export class TuiRenderer {
|
|||
description: "Select color theme (opens selector UI)",
|
||||
};
|
||||
|
||||
const clearCommand: SlashCommand = {
|
||||
name: "clear",
|
||||
description: "Clear context and start a fresh session",
|
||||
};
|
||||
|
||||
// Setup autocomplete for file paths and slash commands
|
||||
const autocompleteProvider = new CombinedAutocompleteProvider(
|
||||
[
|
||||
|
|
@ -183,6 +191,7 @@ export class TuiRenderer {
|
|||
loginCommand,
|
||||
logoutCommand,
|
||||
queueCommand,
|
||||
clearCommand,
|
||||
],
|
||||
process.cwd(),
|
||||
);
|
||||
|
|
@ -265,7 +274,7 @@ export class TuiRenderer {
|
|||
// Set up custom key handlers on the editor
|
||||
this.editor.onEscape = () => {
|
||||
// Intercept Escape key when processing
|
||||
if (this.loadingAnimation && this.onInterruptCallback) {
|
||||
if (this.loadingAnimation) {
|
||||
// Get all queued messages
|
||||
const queuedText = this.queuedMessages.join("\n\n");
|
||||
|
||||
|
|
@ -286,7 +295,7 @@ export class TuiRenderer {
|
|||
this.agent.clearMessageQueue();
|
||||
|
||||
// Abort
|
||||
this.onInterruptCallback();
|
||||
this.agent.abort();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -383,6 +392,13 @@ export class TuiRenderer {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check for /clear command
|
||||
if (text === "/clear") {
|
||||
this.handleClearCommand();
|
||||
this.editor.setText("");
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal message submission - validate model and API key first
|
||||
const currentModel = this.agent.state.model;
|
||||
if (!currentModel) {
|
||||
|
|
@ -436,6 +452,9 @@ export class TuiRenderer {
|
|||
this.ui.start();
|
||||
this.isInitialized = true;
|
||||
|
||||
// Subscribe to agent events for UI updates and session saving
|
||||
this.subscribeToAgent();
|
||||
|
||||
// Set up theme file watcher for live reload
|
||||
onThemeChange(() => {
|
||||
this.ui.invalidate();
|
||||
|
|
@ -444,7 +463,24 @@ export class TuiRenderer {
|
|||
});
|
||||
}
|
||||
|
||||
async handleEvent(event: AgentEvent, state: AgentState): Promise<void> {
|
||||
private subscribeToAgent(): void {
|
||||
this.unsubscribe = this.agent.subscribe(async (event) => {
|
||||
// Handle UI updates
|
||||
await this.handleEvent(event, this.agent.state);
|
||||
|
||||
// Save messages to session
|
||||
if (event.type === "message_end") {
|
||||
this.sessionManager.saveMessage(event.message);
|
||||
|
||||
// Check if we should initialize session now (after first user+assistant exchange)
|
||||
if (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {
|
||||
this.sessionManager.startSession(this.agent.state);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleEvent(event: AgentEvent, state: AgentState): Promise<void> {
|
||||
if (!this.isInitialized) {
|
||||
await this.init();
|
||||
}
|
||||
|
|
@ -710,10 +746,6 @@ export class TuiRenderer {
|
|||
});
|
||||
}
|
||||
|
||||
setInterruptCallback(callback: () => void): void {
|
||||
this.onInterruptCallback = callback;
|
||||
}
|
||||
|
||||
private handleCtrlC(): void {
|
||||
// Handle Ctrl+C double-press logic
|
||||
const now = Date.now();
|
||||
|
|
@ -1373,6 +1405,45 @@ export class TuiRenderer {
|
|||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
private async handleClearCommand(): Promise<void> {
|
||||
// Unsubscribe first to prevent processing abort events
|
||||
this.unsubscribe?.();
|
||||
|
||||
// Abort 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();
|
||||
|
||||
// Reset agent and session
|
||||
this.agent.reset();
|
||||
this.sessionManager.reset();
|
||||
|
||||
// Resubscribe to agent
|
||||
this.subscribeToAgent();
|
||||
|
||||
// Clear UI state
|
||||
this.chatContainer.clear();
|
||||
this.pendingMessagesContainer.clear();
|
||||
this.queuedMessages = [];
|
||||
this.streamingComponent = null;
|
||||
this.pendingTools.clear();
|
||||
this.isFirstUserMessage = true;
|
||||
|
||||
// Show confirmation
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Text(theme.fg("accent", "✓ Context cleared") + "\n" + theme.fg("muted", "Started fresh session"), 1, 1),
|
||||
);
|
||||
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
private updatePendingMessagesDisplay(): void {
|
||||
this.pendingMessagesContainer.clear();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue