mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 19:04:37 +00:00
Release v0.7.28
- Add message queuing with configurable modes (one-at-a-time/all) (#15) - Add /queue command to select queue mode - Add TruncatedText component for proper viewport-aware text truncation - Queue mode setting persists in ~/.pi/agent/settings.json - Visual feedback for queued messages with proper ANSI handling - Press Escape to abort and restore queued messages to editor
This commit is contained in:
parent
e694d435fd
commit
d44073b140
18 changed files with 477 additions and 108 deletions
|
|
@ -760,6 +760,7 @@ export async function main(args: string[]) {
|
|||
thinkingLevel: "off",
|
||||
tools: codingTools,
|
||||
},
|
||||
queueMode: settingsManager.getQueueMode(),
|
||||
transport: new ProviderTransport({
|
||||
// Dynamic API key lookup based on current model's provider
|
||||
getApiKey: async () => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export interface Settings {
|
|||
lastChangelogVersion?: string;
|
||||
defaultProvider?: string;
|
||||
defaultModel?: string;
|
||||
queueMode?: "all" | "one-at-a-time";
|
||||
}
|
||||
|
||||
export class SettingsManager {
|
||||
|
|
@ -78,4 +79,13 @@ export class SettingsManager {
|
|||
this.settings.defaultModel = modelId;
|
||||
this.save();
|
||||
}
|
||||
|
||||
getQueueMode(): "all" | "one-at-a-time" {
|
||||
return this.settings.queueMode || "one-at-a-time";
|
||||
}
|
||||
|
||||
setQueueMode(mode: "all" | "one-at-a-time"): void {
|
||||
this.settings.queueMode = mode;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
packages/coding-agent/src/tui/queue-mode-selector.ts
Normal file
64
packages/coding-agent/src/tui/queue-mode-selector.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { type Component, Container, type SelectItem, SelectList } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width
|
||||
*/
|
||||
class DynamicBorder implements Component {
|
||||
render(width: number): string[] {
|
||||
return [chalk.blue("─".repeat(Math.max(1, width)))];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders a queue mode selector with borders
|
||||
*/
|
||||
export class QueueModeSelectorComponent extends Container {
|
||||
private selectList: SelectList;
|
||||
|
||||
constructor(
|
||||
currentMode: "all" | "one-at-a-time",
|
||||
onSelect: (mode: "all" | "one-at-a-time") => void,
|
||||
onCancel: () => void,
|
||||
) {
|
||||
super();
|
||||
|
||||
const queueModes: SelectItem[] = [
|
||||
{
|
||||
value: "one-at-a-time",
|
||||
label: "one-at-a-time",
|
||||
description: "Process queued messages one by one (recommended)",
|
||||
},
|
||||
{ value: "all", label: "all", description: "Process all queued messages at once" },
|
||||
];
|
||||
|
||||
// Add top border
|
||||
this.addChild(new DynamicBorder());
|
||||
|
||||
// Create selector
|
||||
this.selectList = new SelectList(queueModes, 2);
|
||||
|
||||
// Preselect current mode
|
||||
const currentIndex = queueModes.findIndex((item) => item.value === currentMode);
|
||||
if (currentIndex !== -1) {
|
||||
this.selectList.setSelectedIndex(currentIndex);
|
||||
}
|
||||
|
||||
this.selectList.onSelect = (item) => {
|
||||
onSelect(item.value as "all" | "one-at-a-time");
|
||||
};
|
||||
|
||||
this.selectList.onCancel = () => {
|
||||
onCancel();
|
||||
};
|
||||
|
||||
this.addChild(this.selectList);
|
||||
|
||||
// Add bottom border
|
||||
this.addChild(new DynamicBorder());
|
||||
}
|
||||
|
||||
getSelectList(): SelectList {
|
||||
return this.selectList;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
ProcessTerminal,
|
||||
Spacer,
|
||||
Text,
|
||||
TruncatedText,
|
||||
TUI,
|
||||
} from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
|
|
@ -26,6 +27,7 @@ import { DynamicBorder } from "./dynamic-border.js";
|
|||
import { FooterComponent } from "./footer.js";
|
||||
import { ModelSelectorComponent } from "./model-selector.js";
|
||||
import { OAuthSelectorComponent } from "./oauth-selector.js";
|
||||
import { QueueModeSelectorComponent } from "./queue-mode-selector.js";
|
||||
import { ThinkingSelectorComponent } from "./thinking-selector.js";
|
||||
import { ToolExecutionComponent } from "./tool-execution.js";
|
||||
import { UserMessageComponent } from "./user-message.js";
|
||||
|
|
@ -37,6 +39,7 @@ import { UserMessageSelectorComponent } from "./user-message-selector.js";
|
|||
export class TuiRenderer {
|
||||
private ui: TUI;
|
||||
private chatContainer: Container;
|
||||
private pendingMessagesContainer: Container;
|
||||
private statusContainer: Container;
|
||||
private editor: CustomEditor;
|
||||
private editorContainer: Container; // Container to swap between editor and selector
|
||||
|
|
@ -53,6 +56,9 @@ export class TuiRenderer {
|
|||
private changelogMarkdown: string | null = null;
|
||||
private newVersion: string | null = null;
|
||||
|
||||
// Message queueing
|
||||
private queuedMessages: string[] = [];
|
||||
|
||||
// Streaming message tracking
|
||||
private streamingComponent: AssistantMessageComponent | null = null;
|
||||
|
||||
|
|
@ -62,6 +68,9 @@ export class TuiRenderer {
|
|||
// Thinking level selector
|
||||
private thinkingSelector: ThinkingSelectorComponent | null = null;
|
||||
|
||||
// Queue mode selector
|
||||
private queueModeSelector: QueueModeSelectorComponent | null = null;
|
||||
|
||||
// Model selector
|
||||
private modelSelector: ModelSelectorComponent | null = null;
|
||||
|
||||
|
|
@ -98,6 +107,7 @@ export class TuiRenderer {
|
|||
this.scopedModels = scopedModels;
|
||||
this.ui = new TUI(new ProcessTerminal());
|
||||
this.chatContainer = new Container();
|
||||
this.pendingMessagesContainer = new Container();
|
||||
this.statusContainer = new Container();
|
||||
this.editor = new CustomEditor();
|
||||
this.editorContainer = new Container(); // Container to hold editor or selector
|
||||
|
|
@ -145,6 +155,11 @@ export class TuiRenderer {
|
|||
description: "Logout from OAuth provider",
|
||||
};
|
||||
|
||||
const queueCommand: SlashCommand = {
|
||||
name: "queue",
|
||||
description: "Select message queue mode (opens selector UI)",
|
||||
};
|
||||
|
||||
// Setup autocomplete for file paths and slash commands
|
||||
const autocompleteProvider = new CombinedAutocompleteProvider(
|
||||
[
|
||||
|
|
@ -156,6 +171,7 @@ export class TuiRenderer {
|
|||
branchCommand,
|
||||
loginCommand,
|
||||
logoutCommand,
|
||||
queueCommand,
|
||||
],
|
||||
process.cwd(),
|
||||
);
|
||||
|
|
@ -228,6 +244,7 @@ export class TuiRenderer {
|
|||
}
|
||||
|
||||
this.ui.addChild(this.chatContainer);
|
||||
this.ui.addChild(this.pendingMessagesContainer);
|
||||
this.ui.addChild(this.statusContainer);
|
||||
this.ui.addChild(new Spacer(1));
|
||||
this.ui.addChild(this.editorContainer); // Use container that can hold editor or selector
|
||||
|
|
@ -238,6 +255,26 @@ export class TuiRenderer {
|
|||
this.editor.onEscape = () => {
|
||||
// Intercept Escape key when processing
|
||||
if (this.loadingAnimation && this.onInterruptCallback) {
|
||||
// Get all queued messages
|
||||
const queuedText = this.queuedMessages.join("\n\n");
|
||||
|
||||
// Get current editor text
|
||||
const currentText = this.editor.getText();
|
||||
|
||||
// Combine: queued messages + current editor text
|
||||
const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
|
||||
|
||||
// Put back in editor
|
||||
this.editor.setText(combinedText);
|
||||
|
||||
// Clear queued messages
|
||||
this.queuedMessages = [];
|
||||
this.updatePendingMessagesDisplay();
|
||||
|
||||
// Clear agent's queue too
|
||||
this.agent.clearMessageQueue();
|
||||
|
||||
// Abort
|
||||
this.onInterruptCallback();
|
||||
}
|
||||
};
|
||||
|
|
@ -321,6 +358,13 @@ export class TuiRenderer {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check for /queue command
|
||||
if (text === "/queue") {
|
||||
this.showQueueModeSelector();
|
||||
this.editor.setText("");
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal message submission - validate model and API key first
|
||||
const currentModel = this.agent.state.model;
|
||||
if (!currentModel) {
|
||||
|
|
@ -343,6 +387,27 @@ export class TuiRenderer {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if agent is currently streaming
|
||||
if (this.agent.state.isStreaming) {
|
||||
// Queue the message instead of submitting
|
||||
this.queuedMessages.push(text);
|
||||
|
||||
// Queue in agent
|
||||
await this.agent.queueMessage({
|
||||
role: "user",
|
||||
content: [{ type: "text", text }],
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// Update pending messages display
|
||||
this.updatePendingMessagesDisplay();
|
||||
|
||||
// Clear editor
|
||||
this.editor.setText("");
|
||||
this.ui.requestRender();
|
||||
return;
|
||||
}
|
||||
|
||||
// All good, proceed with submission
|
||||
if (this.onInputCallback) {
|
||||
this.onInputCallback(text);
|
||||
|
|
@ -365,7 +430,7 @@ export class TuiRenderer {
|
|||
switch (event.type) {
|
||||
case "agent_start":
|
||||
// Show loading animation
|
||||
this.editor.disableSubmit = true;
|
||||
// Note: Don't disable submit - we handle queuing in onSubmit callback
|
||||
// Stop old loader before clearing
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
|
|
@ -378,6 +443,18 @@ 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 queuedIndex = this.queuedMessages.indexOf(messageText);
|
||||
if (queuedIndex !== -1) {
|
||||
// Remove from queued messages
|
||||
this.queuedMessages.splice(queuedIndex, 1);
|
||||
this.updatePendingMessagesDisplay();
|
||||
}
|
||||
|
||||
// Show user message immediately and clear editor
|
||||
this.addMessageToChat(event.message);
|
||||
this.editor.setText("");
|
||||
|
|
@ -497,7 +574,7 @@ export class TuiRenderer {
|
|||
this.streamingComponent = null;
|
||||
}
|
||||
this.pendingTools.clear();
|
||||
this.editor.disableSubmit = false;
|
||||
// Note: Don't need to re-enable submit - we never disable it
|
||||
this.ui.requestRender();
|
||||
break;
|
||||
}
|
||||
|
|
@ -810,6 +887,48 @@ export class TuiRenderer {
|
|||
this.ui.setFocus(this.editor);
|
||||
}
|
||||
|
||||
private showQueueModeSelector(): void {
|
||||
// Create queue mode selector with current mode
|
||||
this.queueModeSelector = new QueueModeSelectorComponent(
|
||||
this.agent.getQueueMode(),
|
||||
(mode) => {
|
||||
// Apply the selected queue mode
|
||||
this.agent.setQueueMode(mode);
|
||||
|
||||
// Save queue mode to settings
|
||||
this.settingsManager.setQueueMode(mode);
|
||||
|
||||
// Show confirmation message with proper spacing
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const confirmText = new Text(chalk.dim(`Queue mode: ${mode}`), 1, 0);
|
||||
this.chatContainer.addChild(confirmText);
|
||||
|
||||
// Hide selector and show editor again
|
||||
this.hideQueueModeSelector();
|
||||
this.ui.requestRender();
|
||||
},
|
||||
() => {
|
||||
// Just hide the selector
|
||||
this.hideQueueModeSelector();
|
||||
this.ui.requestRender();
|
||||
},
|
||||
);
|
||||
|
||||
// Replace editor with selector
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(this.queueModeSelector);
|
||||
this.ui.setFocus(this.queueModeSelector.getSelectList());
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
private hideQueueModeSelector(): void {
|
||||
// Replace selector with editor in the container
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(this.editor);
|
||||
this.queueModeSelector = null;
|
||||
this.ui.setFocus(this.editor);
|
||||
}
|
||||
|
||||
private showModelSelector(): void {
|
||||
// Create model selector with current model
|
||||
this.modelSelector = new ModelSelectorComponent(
|
||||
|
|
@ -1171,6 +1290,19 @@ export class TuiRenderer {
|
|||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
private updatePendingMessagesDisplay(): void {
|
||||
this.pendingMessagesContainer.clear();
|
||||
|
||||
if (this.queuedMessages.length > 0) {
|
||||
this.pendingMessagesContainer.addChild(new Spacer(1));
|
||||
|
||||
for (const message of this.queuedMessages) {
|
||||
const queuedText = chalk.dim("Queued: " + message);
|
||||
this.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue