mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 05:02:14 +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
|
### 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))
|
- 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))
|
- 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))
|
- 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";
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CompactionQueuedMessage = {
|
||||||
|
text: string;
|
||||||
|
mode: "steer" | "followUp";
|
||||||
|
};
|
||||||
|
|
||||||
export class InteractiveMode {
|
export class InteractiveMode {
|
||||||
private session: AgentSession;
|
private session: AgentSession;
|
||||||
private ui: TUI;
|
private ui: TUI;
|
||||||
|
|
@ -140,6 +145,9 @@ export class InteractiveMode {
|
||||||
private retryLoader: Loader | undefined = undefined;
|
private retryLoader: Loader | undefined = undefined;
|
||||||
private retryEscapeHandler?: () => void;
|
private retryEscapeHandler?: () => void;
|
||||||
|
|
||||||
|
// Messages queued while compaction is running
|
||||||
|
private compactionQueuedMessages: CompactionQueuedMessage[] = [];
|
||||||
|
|
||||||
// Extension UI state
|
// Extension UI state
|
||||||
private extensionSelector: ExtensionSelectorComponent | undefined = undefined;
|
private extensionSelector: ExtensionSelectorComponent | undefined = undefined;
|
||||||
private extensionInput: ExtensionInputComponent | undefined = undefined;
|
private extensionInput: ExtensionInputComponent | undefined = undefined;
|
||||||
|
|
@ -459,6 +467,7 @@ export class InteractiveMode {
|
||||||
// Clear UI state
|
// Clear UI state
|
||||||
this.chatContainer.clear();
|
this.chatContainer.clear();
|
||||||
this.pendingMessagesContainer.clear();
|
this.pendingMessagesContainer.clear();
|
||||||
|
this.compactionQueuedMessages = [];
|
||||||
this.streamingComponent = undefined;
|
this.streamingComponent = undefined;
|
||||||
this.streamingMessage = undefined;
|
this.streamingMessage = undefined;
|
||||||
this.pendingTools.clear();
|
this.pendingTools.clear();
|
||||||
|
|
@ -1012,12 +1021,7 @@ export class InteractiveMode {
|
||||||
if (text === "/compact" || text.startsWith("/compact ")) {
|
if (text === "/compact" || text.startsWith("/compact ")) {
|
||||||
const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
|
const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
|
||||||
this.editor.setText("");
|
this.editor.setText("");
|
||||||
this.editor.disableSubmit = true;
|
|
||||||
try {
|
|
||||||
await this.handleCompactCommand(customInstructions);
|
await this.handleCompactCommand(customInstructions);
|
||||||
} finally {
|
|
||||||
this.editor.disableSubmit = false;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text === "/debug") {
|
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.session.isCompacting) {
|
||||||
|
if (this.isExtensionCommand(text)) {
|
||||||
|
this.editor.addToHistory(text);
|
||||||
|
this.editor.setText("");
|
||||||
|
await this.session.prompt(text);
|
||||||
|
} else {
|
||||||
|
this.queueCompactionMessage(text, "steer");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1251,8 +1262,7 @@ export class InteractiveMode {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "auto_compaction_start": {
|
case "auto_compaction_start": {
|
||||||
// Disable submit to preserve editor text during compaction
|
// Keep editor active; submissions are queued during compaction.
|
||||||
this.editor.disableSubmit = true;
|
|
||||||
// Set up escape to abort auto-compaction
|
// Set up escape to abort auto-compaction
|
||||||
this.autoCompactionEscapeHandler = this.editor.onEscape;
|
this.autoCompactionEscapeHandler = this.editor.onEscape;
|
||||||
this.editor.onEscape = () => {
|
this.editor.onEscape = () => {
|
||||||
|
|
@ -1273,8 +1283,6 @@ export class InteractiveMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
case "auto_compaction_end": {
|
case "auto_compaction_end": {
|
||||||
// Re-enable submit
|
|
||||||
this.editor.disableSubmit = false;
|
|
||||||
// Restore escape handler
|
// Restore escape handler
|
||||||
if (this.autoCompactionEscapeHandler) {
|
if (this.autoCompactionEscapeHandler) {
|
||||||
this.editor.onEscape = this.autoCompactionEscapeHandler;
|
this.editor.onEscape = this.autoCompactionEscapeHandler;
|
||||||
|
|
@ -1302,6 +1310,7 @@ export class InteractiveMode {
|
||||||
});
|
});
|
||||||
this.footer.invalidate();
|
this.footer.invalidate();
|
||||||
}
|
}
|
||||||
|
void this.flushCompactionQueue({ willRetry: event.willRetry });
|
||||||
this.ui.requestRender();
|
this.ui.requestRender();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -1593,6 +1602,18 @@ export class InteractiveMode {
|
||||||
const text = this.editor.getText().trim();
|
const text = this.editor.getText().trim();
|
||||||
if (!text) return;
|
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)
|
// Alt+Enter queues a follow-up message (waits until agent finishes)
|
||||||
// This handles extension commands (execute immediately), prompt template expansion, and queueing
|
// This handles extension commands (execute immediately), prompt template expansion, and queueing
|
||||||
if (this.session.isStreaming) {
|
if (this.session.isStreaming) {
|
||||||
|
|
@ -1761,8 +1782,14 @@ export class InteractiveMode {
|
||||||
|
|
||||||
private updatePendingMessagesDisplay(): void {
|
private updatePendingMessagesDisplay(): void {
|
||||||
this.pendingMessagesContainer.clear();
|
this.pendingMessagesContainer.clear();
|
||||||
const steeringMessages = this.session.getSteeringMessages();
|
const steeringMessages = [
|
||||||
const followUpMessages = this.session.getFollowUpMessages();
|
...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) {
|
if (steeringMessages.length > 0 || followUpMessages.length > 0) {
|
||||||
this.pendingMessagesContainer.addChild(new Spacer(1));
|
this.pendingMessagesContainer.addChild(new Spacer(1));
|
||||||
for (const message of steeringMessages) {
|
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 */
|
/** Move pending bash components from pending area to chat */
|
||||||
private flushPendingBashComponents(): void {
|
private flushPendingBashComponents(): void {
|
||||||
for (const component of this.pendingBashComponents) {
|
for (const component of this.pendingBashComponents) {
|
||||||
|
|
@ -2089,6 +2212,7 @@ export class InteractiveMode {
|
||||||
|
|
||||||
// Clear UI state
|
// Clear UI state
|
||||||
this.pendingMessagesContainer.clear();
|
this.pendingMessagesContainer.clear();
|
||||||
|
this.compactionQueuedMessages = [];
|
||||||
this.streamingComponent = undefined;
|
this.streamingComponent = undefined;
|
||||||
this.streamingMessage = undefined;
|
this.streamingMessage = undefined;
|
||||||
this.pendingTools.clear();
|
this.pendingTools.clear();
|
||||||
|
|
@ -2549,6 +2673,7 @@ export class InteractiveMode {
|
||||||
// Clear UI state
|
// Clear UI state
|
||||||
this.chatContainer.clear();
|
this.chatContainer.clear();
|
||||||
this.pendingMessagesContainer.clear();
|
this.pendingMessagesContainer.clear();
|
||||||
|
this.compactionQueuedMessages = [];
|
||||||
this.streamingComponent = undefined;
|
this.streamingComponent = undefined;
|
||||||
this.streamingMessage = undefined;
|
this.streamingMessage = undefined;
|
||||||
this.pendingTools.clear();
|
this.pendingTools.clear();
|
||||||
|
|
@ -2702,6 +2827,7 @@ export class InteractiveMode {
|
||||||
this.statusContainer.clear();
|
this.statusContainer.clear();
|
||||||
this.editor.onEscape = originalOnEscape;
|
this.editor.onEscape = originalOnEscape;
|
||||||
}
|
}
|
||||||
|
void this.flushCompactionQueue({ willRetry: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue