mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 21:03:19 +00:00
feat(coding-agent): queue compaction submissions, closes #475
Messages submitted during compaction are queued and delivered after compaction completes, preserving steer vs follow-up behavior. Extension commands execute immediately during compaction. Co-authored-by: Thomas Mustier <tmustier@users.noreply.github.com>
This commit is contained in:
parent
1349f02cdd
commit
e182b123a9
2 changed files with 140 additions and 13 deletions
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- Messages submitted during compaction are queued and delivered after compaction completes, preserving steering and follow-up behavior. Extension commands execute immediately during compaction. ([#476](https://github.com/badlogic/pi-mono/pull/476) by [@tmustier](https://github.com/tmustier))
|
||||
- Managed binaries (`fd`, `rg`) now stored in `~/.pi/agent/bin/` instead of `tools/`, eliminating false deprecation warnings ([#470](https://github.com/badlogic/pi-mono/pull/470) by [@mcinteerj](https://github.com/mcinteerj))
|
||||
- Extensions defined in `settings.json` were not loaded ([#463](https://github.com/badlogic/pi-mono/pull/463) by [@melihmucuk](https://github.com/melihmucuk))
|
||||
- OAuth refresh no longer logs users out when multiple pi instances are running ([#466](https://github.com/badlogic/pi-mono/pull/466) by [@Cursivez](https://github.com/Cursivez))
|
||||
|
|
|
|||
|
|
@ -84,6 +84,11 @@ function isExpandable(obj: unknown): obj is Expandable {
|
|||
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
||||
}
|
||||
|
||||
type CompactionQueuedMessage = {
|
||||
text: string;
|
||||
mode: "steer" | "followUp";
|
||||
};
|
||||
|
||||
export class InteractiveMode {
|
||||
private session: AgentSession;
|
||||
private ui: TUI;
|
||||
|
|
@ -140,6 +145,9 @@ export class InteractiveMode {
|
|||
private retryLoader: Loader | undefined = undefined;
|
||||
private retryEscapeHandler?: () => void;
|
||||
|
||||
// Messages queued while compaction is running
|
||||
private compactionQueuedMessages: CompactionQueuedMessage[] = [];
|
||||
|
||||
// Extension UI state
|
||||
private extensionSelector: ExtensionSelectorComponent | undefined = undefined;
|
||||
private extensionInput: ExtensionInputComponent | undefined = undefined;
|
||||
|
|
@ -459,6 +467,7 @@ export class InteractiveMode {
|
|||
// Clear UI state
|
||||
this.chatContainer.clear();
|
||||
this.pendingMessagesContainer.clear();
|
||||
this.compactionQueuedMessages = [];
|
||||
this.streamingComponent = undefined;
|
||||
this.streamingMessage = undefined;
|
||||
this.pendingTools.clear();
|
||||
|
|
@ -1012,12 +1021,7 @@ export class InteractiveMode {
|
|||
if (text === "/compact" || text.startsWith("/compact ")) {
|
||||
const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
|
||||
this.editor.setText("");
|
||||
this.editor.disableSubmit = true;
|
||||
try {
|
||||
await this.handleCompactCommand(customInstructions);
|
||||
} finally {
|
||||
this.editor.disableSubmit = false;
|
||||
}
|
||||
await this.handleCompactCommand(customInstructions);
|
||||
return;
|
||||
}
|
||||
if (text === "/debug") {
|
||||
|
|
@ -1059,8 +1063,15 @@ export class InteractiveMode {
|
|||
}
|
||||
}
|
||||
|
||||
// Block input during compaction
|
||||
// Queue input during compaction (extension commands execute immediately)
|
||||
if (this.session.isCompacting) {
|
||||
if (this.isExtensionCommand(text)) {
|
||||
this.editor.addToHistory(text);
|
||||
this.editor.setText("");
|
||||
await this.session.prompt(text);
|
||||
} else {
|
||||
this.queueCompactionMessage(text, "steer");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1251,8 +1262,7 @@ export class InteractiveMode {
|
|||
break;
|
||||
|
||||
case "auto_compaction_start": {
|
||||
// Disable submit to preserve editor text during compaction
|
||||
this.editor.disableSubmit = true;
|
||||
// Keep editor active; submissions are queued during compaction.
|
||||
// Set up escape to abort auto-compaction
|
||||
this.autoCompactionEscapeHandler = this.editor.onEscape;
|
||||
this.editor.onEscape = () => {
|
||||
|
|
@ -1273,8 +1283,6 @@ export class InteractiveMode {
|
|||
}
|
||||
|
||||
case "auto_compaction_end": {
|
||||
// Re-enable submit
|
||||
this.editor.disableSubmit = false;
|
||||
// Restore escape handler
|
||||
if (this.autoCompactionEscapeHandler) {
|
||||
this.editor.onEscape = this.autoCompactionEscapeHandler;
|
||||
|
|
@ -1302,6 +1310,7 @@ export class InteractiveMode {
|
|||
});
|
||||
this.footer.invalidate();
|
||||
}
|
||||
void this.flushCompactionQueue({ willRetry: event.willRetry });
|
||||
this.ui.requestRender();
|
||||
break;
|
||||
}
|
||||
|
|
@ -1593,6 +1602,18 @@ export class InteractiveMode {
|
|||
const text = this.editor.getText().trim();
|
||||
if (!text) return;
|
||||
|
||||
// Queue input during compaction (extension commands execute immediately)
|
||||
if (this.session.isCompacting) {
|
||||
if (this.isExtensionCommand(text)) {
|
||||
this.editor.addToHistory(text);
|
||||
this.editor.setText("");
|
||||
await this.session.prompt(text);
|
||||
} else {
|
||||
this.queueCompactionMessage(text, "followUp");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Alt+Enter queues a follow-up message (waits until agent finishes)
|
||||
// This handles extension commands (execute immediately), prompt template expansion, and queueing
|
||||
if (this.session.isStreaming) {
|
||||
|
|
@ -1761,8 +1782,14 @@ export class InteractiveMode {
|
|||
|
||||
private updatePendingMessagesDisplay(): void {
|
||||
this.pendingMessagesContainer.clear();
|
||||
const steeringMessages = this.session.getSteeringMessages();
|
||||
const followUpMessages = this.session.getFollowUpMessages();
|
||||
const steeringMessages = [
|
||||
...this.session.getSteeringMessages(),
|
||||
...this.compactionQueuedMessages.filter((msg) => msg.mode === "steer").map((msg) => msg.text),
|
||||
];
|
||||
const followUpMessages = [
|
||||
...this.session.getFollowUpMessages(),
|
||||
...this.compactionQueuedMessages.filter((msg) => msg.mode === "followUp").map((msg) => msg.text),
|
||||
];
|
||||
if (steeringMessages.length > 0 || followUpMessages.length > 0) {
|
||||
this.pendingMessagesContainer.addChild(new Spacer(1));
|
||||
for (const message of steeringMessages) {
|
||||
|
|
@ -1776,6 +1803,102 @@ export class InteractiveMode {
|
|||
}
|
||||
}
|
||||
|
||||
private queueCompactionMessage(text: string, mode: "steer" | "followUp"): void {
|
||||
this.compactionQueuedMessages.push({ text, mode });
|
||||
this.editor.addToHistory(text);
|
||||
this.editor.setText("");
|
||||
this.updatePendingMessagesDisplay();
|
||||
this.showStatus("Queued message for after compaction");
|
||||
}
|
||||
|
||||
private isExtensionCommand(text: string): boolean {
|
||||
if (!text.startsWith("/")) return false;
|
||||
|
||||
const extensionRunner = this.session.extensionRunner;
|
||||
if (!extensionRunner) return false;
|
||||
|
||||
const spaceIndex = text.indexOf(" ");
|
||||
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
||||
return !!extensionRunner.getCommand(commandName);
|
||||
}
|
||||
|
||||
private async flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void> {
|
||||
if (this.compactionQueuedMessages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queuedMessages = [...this.compactionQueuedMessages];
|
||||
this.compactionQueuedMessages = [];
|
||||
this.updatePendingMessagesDisplay();
|
||||
|
||||
const restoreQueue = (error: unknown) => {
|
||||
this.session.clearQueue();
|
||||
this.compactionQueuedMessages = queuedMessages;
|
||||
this.updatePendingMessagesDisplay();
|
||||
this.showError(
|
||||
`Failed to send queued message${queuedMessages.length > 1 ? "s" : ""}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
if (options?.willRetry) {
|
||||
// When retry is pending, queue messages for the retry turn
|
||||
for (const message of queuedMessages) {
|
||||
if (this.isExtensionCommand(message.text)) {
|
||||
await this.session.prompt(message.text);
|
||||
} else if (message.mode === "followUp") {
|
||||
await this.session.followUp(message.text);
|
||||
} else {
|
||||
await this.session.steer(message.text);
|
||||
}
|
||||
}
|
||||
this.updatePendingMessagesDisplay();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find first non-extension-command message to use as prompt
|
||||
const firstPromptIndex = queuedMessages.findIndex((message) => !this.isExtensionCommand(message.text));
|
||||
if (firstPromptIndex === -1) {
|
||||
// All extension commands - execute them all
|
||||
for (const message of queuedMessages) {
|
||||
await this.session.prompt(message.text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute any extension commands before the first prompt
|
||||
const preCommands = queuedMessages.slice(0, firstPromptIndex);
|
||||
const firstPrompt = queuedMessages[firstPromptIndex];
|
||||
const rest = queuedMessages.slice(firstPromptIndex + 1);
|
||||
|
||||
for (const message of preCommands) {
|
||||
await this.session.prompt(message.text);
|
||||
}
|
||||
|
||||
// Send first prompt (starts streaming)
|
||||
const promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {
|
||||
restoreQueue(error);
|
||||
});
|
||||
|
||||
// Queue remaining messages
|
||||
for (const message of rest) {
|
||||
if (this.isExtensionCommand(message.text)) {
|
||||
await this.session.prompt(message.text);
|
||||
} else if (message.mode === "followUp") {
|
||||
await this.session.followUp(message.text);
|
||||
} else {
|
||||
await this.session.steer(message.text);
|
||||
}
|
||||
}
|
||||
this.updatePendingMessagesDisplay();
|
||||
void promptPromise;
|
||||
} catch (error) {
|
||||
restoreQueue(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Move pending bash components from pending area to chat */
|
||||
private flushPendingBashComponents(): void {
|
||||
for (const component of this.pendingBashComponents) {
|
||||
|
|
@ -2089,6 +2212,7 @@ export class InteractiveMode {
|
|||
|
||||
// Clear UI state
|
||||
this.pendingMessagesContainer.clear();
|
||||
this.compactionQueuedMessages = [];
|
||||
this.streamingComponent = undefined;
|
||||
this.streamingMessage = undefined;
|
||||
this.pendingTools.clear();
|
||||
|
|
@ -2549,6 +2673,7 @@ export class InteractiveMode {
|
|||
// Clear UI state
|
||||
this.chatContainer.clear();
|
||||
this.pendingMessagesContainer.clear();
|
||||
this.compactionQueuedMessages = [];
|
||||
this.streamingComponent = undefined;
|
||||
this.streamingMessage = undefined;
|
||||
this.pendingTools.clear();
|
||||
|
|
@ -2702,6 +2827,7 @@ export class InteractiveMode {
|
|||
this.statusContainer.clear();
|
||||
this.editor.onEscape = originalOnEscape;
|
||||
}
|
||||
void this.flushCompactionQueue({ willRetry: false });
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue