mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 08:04:44 +00:00
feat(coding-agent): complete steer()/followUp() migration
- Update settings-manager with steeringMode/followUpMode (migrates old queueMode) - Update sdk.ts to use new mode options - Update settings-selector UI to show both modes - Add Alt+Enter keybind for follow-up messages - Update RPC API: steer/follow_up commands, set_steering_mode/set_follow_up_mode - Update rpc-client with new methods - Delete dead code: queue-mode-selector.ts - Update tests for new API - Update mom/context.ts stubs - Update web-ui example
This commit is contained in:
parent
58c423ba36
commit
3ae02a6849
12 changed files with 173 additions and 106 deletions
|
|
@ -305,7 +305,8 @@ export function loadSettings(cwd?: string, agentDir?: string): Settings {
|
||||||
defaultProvider: manager.getDefaultProvider(),
|
defaultProvider: manager.getDefaultProvider(),
|
||||||
defaultModel: manager.getDefaultModel(),
|
defaultModel: manager.getDefaultModel(),
|
||||||
defaultThinkingLevel: manager.getDefaultThinkingLevel(),
|
defaultThinkingLevel: manager.getDefaultThinkingLevel(),
|
||||||
queueMode: manager.getQueueMode(),
|
steeringMode: manager.getSteeringMode(),
|
||||||
|
followUpMode: manager.getFollowUpMode(),
|
||||||
theme: manager.getTheme(),
|
theme: manager.getTheme(),
|
||||||
compaction: manager.getCompactionSettings(),
|
compaction: manager.getCompactionSettings(),
|
||||||
retry: manager.getRetrySettings(),
|
retry: manager.getRetrySettings(),
|
||||||
|
|
@ -626,7 +627,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||||
return hookRunner.emitContext(messages);
|
return hookRunner.emitContext(messages);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
queueMode: settingsManager.getQueueMode(),
|
steeringMode: settingsManager.getSteeringMode(),
|
||||||
|
followUpMode: settingsManager.getFollowUpMode(),
|
||||||
getApiKey: async () => {
|
getApiKey: async () => {
|
||||||
const currentModel = agent.state.model;
|
const currentModel = agent.state.model;
|
||||||
if (!currentModel) {
|
if (!currentModel) {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@ export interface Settings {
|
||||||
defaultProvider?: string;
|
defaultProvider?: string;
|
||||||
defaultModel?: string;
|
defaultModel?: string;
|
||||||
defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||||
queueMode?: "all" | "one-at-a-time";
|
steeringMode?: "all" | "one-at-a-time";
|
||||||
|
followUpMode?: "all" | "one-at-a-time";
|
||||||
theme?: string;
|
theme?: string;
|
||||||
compaction?: CompactionSettings;
|
compaction?: CompactionSettings;
|
||||||
branchSummary?: BranchSummarySettings;
|
branchSummary?: BranchSummarySettings;
|
||||||
|
|
@ -125,13 +126,24 @@ export class SettingsManager {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(path, "utf-8");
|
const content = readFileSync(path, "utf-8");
|
||||||
return JSON.parse(content);
|
const settings = JSON.parse(content);
|
||||||
|
return SettingsManager.migrateSettings(settings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Warning: Could not read settings file ${path}: ${error}`);
|
console.error(`Warning: Could not read settings file ${path}: ${error}`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Migrate old settings format to new format */
|
||||||
|
private static migrateSettings(settings: Record<string, unknown>): Settings {
|
||||||
|
// Migrate queueMode -> steeringMode
|
||||||
|
if ("queueMode" in settings && !("steeringMode" in settings)) {
|
||||||
|
settings.steeringMode = settings.queueMode;
|
||||||
|
delete settings.queueMode;
|
||||||
|
}
|
||||||
|
return settings as Settings;
|
||||||
|
}
|
||||||
|
|
||||||
private loadProjectSettings(): Settings {
|
private loadProjectSettings(): Settings {
|
||||||
if (!this.projectSettingsPath || !existsSync(this.projectSettingsPath)) {
|
if (!this.projectSettingsPath || !existsSync(this.projectSettingsPath)) {
|
||||||
return {};
|
return {};
|
||||||
|
|
@ -139,7 +151,8 @@ export class SettingsManager {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(this.projectSettingsPath, "utf-8");
|
const content = readFileSync(this.projectSettingsPath, "utf-8");
|
||||||
return JSON.parse(content);
|
const settings = JSON.parse(content);
|
||||||
|
return SettingsManager.migrateSettings(settings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Warning: Could not read project settings file: ${error}`);
|
console.error(`Warning: Could not read project settings file: ${error}`);
|
||||||
return {};
|
return {};
|
||||||
|
|
@ -204,12 +217,21 @@ export class SettingsManager {
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
getQueueMode(): "all" | "one-at-a-time" {
|
getSteeringMode(): "all" | "one-at-a-time" {
|
||||||
return this.settings.queueMode || "one-at-a-time";
|
return this.settings.steeringMode || "one-at-a-time";
|
||||||
}
|
}
|
||||||
|
|
||||||
setQueueMode(mode: "all" | "one-at-a-time"): void {
|
setSteeringMode(mode: "all" | "one-at-a-time"): void {
|
||||||
this.globalSettings.queueMode = mode;
|
this.globalSettings.steeringMode = mode;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
getFollowUpMode(): "all" | "one-at-a-time" {
|
||||||
|
return this.settings.followUpMode || "one-at-a-time";
|
||||||
|
}
|
||||||
|
|
||||||
|
setFollowUpMode(mode: "all" | "one-at-a-time"): void {
|
||||||
|
this.globalSettings.followUpMode = mode;
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
Editor,
|
Editor,
|
||||||
|
isAltEnter,
|
||||||
isCtrlC,
|
isCtrlC,
|
||||||
isCtrlD,
|
isCtrlD,
|
||||||
isCtrlG,
|
isCtrlG,
|
||||||
|
|
@ -28,8 +29,14 @@ export class CustomEditor extends Editor {
|
||||||
public onCtrlT?: () => void;
|
public onCtrlT?: () => void;
|
||||||
public onCtrlG?: () => void;
|
public onCtrlG?: () => void;
|
||||||
public onCtrlZ?: () => void;
|
public onCtrlZ?: () => void;
|
||||||
|
public onAltEnter?: () => void;
|
||||||
|
|
||||||
handleInput(data: string): void {
|
handleInput(data: string): void {
|
||||||
|
// Intercept Alt+Enter for follow-up messages
|
||||||
|
if (isAltEnter(data) && this.onAltEnter) {
|
||||||
|
this.onAltEnter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Intercept Ctrl+G for external editor
|
// Intercept Ctrl+G for external editor
|
||||||
if (isCtrlG(data) && this.onCtrlG) {
|
if (isCtrlG(data) && this.onCtrlG) {
|
||||||
this.onCtrlG();
|
this.onCtrlG();
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
import { Container, type SelectItem, SelectList } from "@mariozechner/pi-tui";
|
|
||||||
import { getSelectListTheme } from "../theme/theme.js";
|
|
||||||
import { DynamicBorder } from "./dynamic-border.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, getSelectListTheme());
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -24,7 +24,8 @@ const THINKING_DESCRIPTIONS: Record<ThinkingLevel, string> = {
|
||||||
export interface SettingsConfig {
|
export interface SettingsConfig {
|
||||||
autoCompact: boolean;
|
autoCompact: boolean;
|
||||||
showImages: boolean;
|
showImages: boolean;
|
||||||
queueMode: "all" | "one-at-a-time";
|
steeringMode: "all" | "one-at-a-time";
|
||||||
|
followUpMode: "all" | "one-at-a-time";
|
||||||
thinkingLevel: ThinkingLevel;
|
thinkingLevel: ThinkingLevel;
|
||||||
availableThinkingLevels: ThinkingLevel[];
|
availableThinkingLevels: ThinkingLevel[];
|
||||||
currentTheme: string;
|
currentTheme: string;
|
||||||
|
|
@ -36,7 +37,8 @@ export interface SettingsConfig {
|
||||||
export interface SettingsCallbacks {
|
export interface SettingsCallbacks {
|
||||||
onAutoCompactChange: (enabled: boolean) => void;
|
onAutoCompactChange: (enabled: boolean) => void;
|
||||||
onShowImagesChange: (enabled: boolean) => void;
|
onShowImagesChange: (enabled: boolean) => void;
|
||||||
onQueueModeChange: (mode: "all" | "one-at-a-time") => void;
|
onSteeringModeChange: (mode: "all" | "one-at-a-time") => void;
|
||||||
|
onFollowUpModeChange: (mode: "all" | "one-at-a-time") => void;
|
||||||
onThinkingLevelChange: (level: ThinkingLevel) => void;
|
onThinkingLevelChange: (level: ThinkingLevel) => void;
|
||||||
onThemeChange: (theme: string) => void;
|
onThemeChange: (theme: string) => void;
|
||||||
onThemePreview?: (theme: string) => void;
|
onThemePreview?: (theme: string) => void;
|
||||||
|
|
@ -127,10 +129,17 @@ export class SettingsSelectorComponent extends Container {
|
||||||
values: ["true", "false"],
|
values: ["true", "false"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "queue-mode",
|
id: "steering-mode",
|
||||||
label: "Queue mode",
|
label: "Steering mode",
|
||||||
description: "How to process queued messages while agent is working",
|
description: "How to deliver steering messages (Enter while streaming)",
|
||||||
currentValue: config.queueMode,
|
currentValue: config.steeringMode,
|
||||||
|
values: ["one-at-a-time", "all"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "follow-up-mode",
|
||||||
|
label: "Follow-up mode",
|
||||||
|
description: "How to deliver follow-up messages (queued until agent finishes)",
|
||||||
|
currentValue: config.followUpMode,
|
||||||
values: ["one-at-a-time", "all"],
|
values: ["one-at-a-time", "all"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -227,8 +236,11 @@ export class SettingsSelectorComponent extends Container {
|
||||||
case "show-images":
|
case "show-images":
|
||||||
callbacks.onShowImagesChange(newValue === "true");
|
callbacks.onShowImagesChange(newValue === "true");
|
||||||
break;
|
break;
|
||||||
case "queue-mode":
|
case "steering-mode":
|
||||||
callbacks.onQueueModeChange(newValue as "all" | "one-at-a-time");
|
callbacks.onSteeringModeChange(newValue as "all" | "one-at-a-time");
|
||||||
|
break;
|
||||||
|
case "follow-up-mode":
|
||||||
|
callbacks.onFollowUpModeChange(newValue as "all" | "one-at-a-time");
|
||||||
break;
|
break;
|
||||||
case "hide-thinking":
|
case "hide-thinking":
|
||||||
callbacks.onHideThinkingBlockChange(newValue === "true");
|
callbacks.onHideThinkingBlockChange(newValue === "true");
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,9 @@ export class InteractiveMode {
|
||||||
theme.fg("dim", "!") +
|
theme.fg("dim", "!") +
|
||||||
theme.fg("muted", " to run bash") +
|
theme.fg("muted", " to run bash") +
|
||||||
"\n" +
|
"\n" +
|
||||||
|
theme.fg("dim", "alt+enter") +
|
||||||
|
theme.fg("muted", " to queue follow-up") +
|
||||||
|
"\n" +
|
||||||
theme.fg("dim", "drop files") +
|
theme.fg("dim", "drop files") +
|
||||||
theme.fg("muted", " to attach");
|
theme.fg("muted", " to attach");
|
||||||
const header = new Text(`${logo}\n${instructions}`, 1, 0);
|
const header = new Text(`${logo}\n${instructions}`, 1, 0);
|
||||||
|
|
@ -776,6 +779,7 @@ export class InteractiveMode {
|
||||||
this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
|
this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
|
||||||
this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
|
this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
|
||||||
this.editor.onCtrlG = () => this.openExternalEditor();
|
this.editor.onCtrlG = () => this.openExternalEditor();
|
||||||
|
this.editor.onAltEnter = () => this.handleAltEnter();
|
||||||
|
|
||||||
this.editor.onChange = (text: string) => {
|
this.editor.onChange = (text: string) => {
|
||||||
const wasBashMode = this.isBashMode;
|
const wasBashMode = this.isBashMode;
|
||||||
|
|
@ -920,9 +924,9 @@ export class InteractiveMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue regular messages if agent is streaming
|
// Queue steering message if agent is streaming (interrupts current work)
|
||||||
if (this.session.isStreaming) {
|
if (this.session.isStreaming) {
|
||||||
await this.session.queueMessage(text);
|
await this.session.steer(text);
|
||||||
this.updatePendingMessagesDisplay();
|
this.updatePendingMessagesDisplay();
|
||||||
this.editor.addToHistory(text);
|
this.editor.addToHistory(text);
|
||||||
this.editor.setText("");
|
this.editor.setText("");
|
||||||
|
|
@ -1447,6 +1451,24 @@ export class InteractiveMode {
|
||||||
process.kill(0, "SIGTSTP");
|
process.kill(0, "SIGTSTP");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleAltEnter(): Promise<void> {
|
||||||
|
const text = this.editor.getText().trim();
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
// Alt+Enter queues a follow-up message (waits until agent finishes)
|
||||||
|
if (this.session.isStreaming) {
|
||||||
|
await this.session.followUp(text);
|
||||||
|
this.updatePendingMessagesDisplay();
|
||||||
|
this.editor.addToHistory(text);
|
||||||
|
this.editor.setText("");
|
||||||
|
this.ui.requestRender();
|
||||||
|
}
|
||||||
|
// If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
|
||||||
|
else if (this.editor.onSubmit) {
|
||||||
|
this.editor.onSubmit(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private updateEditorBorderColor(): void {
|
private updateEditorBorderColor(): void {
|
||||||
if (this.isBashMode) {
|
if (this.isBashMode) {
|
||||||
this.editor.borderColor = theme.getBashModeBorderColor();
|
this.editor.borderColor = theme.getBashModeBorderColor();
|
||||||
|
|
@ -1651,7 +1673,8 @@ export class InteractiveMode {
|
||||||
{
|
{
|
||||||
autoCompact: this.session.autoCompactionEnabled,
|
autoCompact: this.session.autoCompactionEnabled,
|
||||||
showImages: this.settingsManager.getShowImages(),
|
showImages: this.settingsManager.getShowImages(),
|
||||||
queueMode: this.session.queueMode,
|
steeringMode: this.session.steeringMode,
|
||||||
|
followUpMode: this.session.followUpMode,
|
||||||
thinkingLevel: this.session.thinkingLevel,
|
thinkingLevel: this.session.thinkingLevel,
|
||||||
availableThinkingLevels: this.session.getAvailableThinkingLevels(),
|
availableThinkingLevels: this.session.getAvailableThinkingLevels(),
|
||||||
currentTheme: this.settingsManager.getTheme() || "dark",
|
currentTheme: this.settingsManager.getTheme() || "dark",
|
||||||
|
|
@ -1672,8 +1695,11 @@ export class InteractiveMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onQueueModeChange: (mode) => {
|
onSteeringModeChange: (mode) => {
|
||||||
this.session.setQueueMode(mode);
|
this.session.setSteeringMode(mode);
|
||||||
|
},
|
||||||
|
onFollowUpModeChange: (mode) => {
|
||||||
|
this.session.setFollowUpMode(mode);
|
||||||
},
|
},
|
||||||
onThinkingLevelChange: (level) => {
|
onThinkingLevelChange: (level) => {
|
||||||
this.session.setThinkingLevel(level);
|
this.session.setThinkingLevel(level);
|
||||||
|
|
|
||||||
|
|
@ -173,10 +173,17 @@ export class RpcClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue a message while agent is streaming.
|
* Queue a steering message to interrupt the agent mid-run.
|
||||||
*/
|
*/
|
||||||
async queueMessage(message: string): Promise<void> {
|
async steer(message: string): Promise<void> {
|
||||||
await this.send({ type: "queue_message", message });
|
await this.send({ type: "steer", message });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue a follow-up message to be processed after the agent finishes.
|
||||||
|
*/
|
||||||
|
async followUp(message: string): Promise<void> {
|
||||||
|
await this.send({ type: "follow_up", message });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -248,10 +255,17 @@ export class RpcClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set queue mode.
|
* Set steering mode.
|
||||||
*/
|
*/
|
||||||
async setQueueMode(mode: "all" | "one-at-a-time"): Promise<void> {
|
async setSteeringMode(mode: "all" | "one-at-a-time"): Promise<void> {
|
||||||
await this.send({ type: "set_queue_mode", mode });
|
await this.send({ type: "set_steering_mode", mode });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set follow-up mode.
|
||||||
|
*/
|
||||||
|
async setFollowUpMode(mode: "all" | "one-at-a-time"): Promise<void> {
|
||||||
|
await this.send({ type: "set_follow_up_mode", mode });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -253,9 +253,14 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
||||||
return success(id, "prompt");
|
return success(id, "prompt");
|
||||||
}
|
}
|
||||||
|
|
||||||
case "queue_message": {
|
case "steer": {
|
||||||
await session.queueMessage(command.message);
|
await session.steer(command.message);
|
||||||
return success(id, "queue_message");
|
return success(id, "steer");
|
||||||
|
}
|
||||||
|
|
||||||
|
case "follow_up": {
|
||||||
|
await session.followUp(command.message);
|
||||||
|
return success(id, "follow_up");
|
||||||
}
|
}
|
||||||
|
|
||||||
case "abort": {
|
case "abort": {
|
||||||
|
|
@ -279,7 +284,8 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
||||||
thinkingLevel: session.thinkingLevel,
|
thinkingLevel: session.thinkingLevel,
|
||||||
isStreaming: session.isStreaming,
|
isStreaming: session.isStreaming,
|
||||||
isCompacting: session.isCompacting,
|
isCompacting: session.isCompacting,
|
||||||
queueMode: session.queueMode,
|
steeringMode: session.steeringMode,
|
||||||
|
followUpMode: session.followUpMode,
|
||||||
sessionFile: session.sessionFile,
|
sessionFile: session.sessionFile,
|
||||||
sessionId: session.sessionId,
|
sessionId: session.sessionId,
|
||||||
autoCompactionEnabled: session.autoCompactionEnabled,
|
autoCompactionEnabled: session.autoCompactionEnabled,
|
||||||
|
|
@ -334,12 +340,17 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// Queue Mode
|
// Queue Modes
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
case "set_queue_mode": {
|
case "set_steering_mode": {
|
||||||
session.setQueueMode(command.mode);
|
session.setSteeringMode(command.mode);
|
||||||
return success(id, "set_queue_mode");
|
return success(id, "set_steering_mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
case "set_follow_up_mode": {
|
||||||
|
session.setFollowUpMode(command.mode);
|
||||||
|
return success(id, "set_follow_up_mode");
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ import type { CompactionResult } from "../../core/compaction/index.js";
|
||||||
export type RpcCommand =
|
export type RpcCommand =
|
||||||
// Prompting
|
// Prompting
|
||||||
| { id?: string; type: "prompt"; message: string; images?: ImageContent[] }
|
| { id?: string; type: "prompt"; message: string; images?: ImageContent[] }
|
||||||
| { id?: string; type: "queue_message"; message: string }
|
| { id?: string; type: "steer"; message: string }
|
||||||
|
| { id?: string; type: "follow_up"; message: string }
|
||||||
| { id?: string; type: "abort" }
|
| { id?: string; type: "abort" }
|
||||||
| { id?: string; type: "new_session"; parentSession?: string }
|
| { id?: string; type: "new_session"; parentSession?: string }
|
||||||
|
|
||||||
|
|
@ -34,8 +35,9 @@ export type RpcCommand =
|
||||||
| { id?: string; type: "set_thinking_level"; level: ThinkingLevel }
|
| { id?: string; type: "set_thinking_level"; level: ThinkingLevel }
|
||||||
| { id?: string; type: "cycle_thinking_level" }
|
| { id?: string; type: "cycle_thinking_level" }
|
||||||
|
|
||||||
// Queue mode
|
// Queue modes
|
||||||
| { id?: string; type: "set_queue_mode"; mode: "all" | "one-at-a-time" }
|
| { id?: string; type: "set_steering_mode"; mode: "all" | "one-at-a-time" }
|
||||||
|
| { id?: string; type: "set_follow_up_mode"; mode: "all" | "one-at-a-time" }
|
||||||
|
|
||||||
// Compaction
|
// Compaction
|
||||||
| { id?: string; type: "compact"; customInstructions?: string }
|
| { id?: string; type: "compact"; customInstructions?: string }
|
||||||
|
|
@ -69,7 +71,8 @@ export interface RpcSessionState {
|
||||||
thinkingLevel: ThinkingLevel;
|
thinkingLevel: ThinkingLevel;
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
isCompacting: boolean;
|
isCompacting: boolean;
|
||||||
queueMode: "all" | "one-at-a-time";
|
steeringMode: "all" | "one-at-a-time";
|
||||||
|
followUpMode: "all" | "one-at-a-time";
|
||||||
sessionFile?: string;
|
sessionFile?: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
autoCompactionEnabled: boolean;
|
autoCompactionEnabled: boolean;
|
||||||
|
|
@ -85,7 +88,8 @@ export interface RpcSessionState {
|
||||||
export type RpcResponse =
|
export type RpcResponse =
|
||||||
// Prompting (async - events follow)
|
// Prompting (async - events follow)
|
||||||
| { id?: string; type: "response"; command: "prompt"; success: true }
|
| { id?: string; type: "response"; command: "prompt"; success: true }
|
||||||
| { id?: string; type: "response"; command: "queue_message"; success: true }
|
| { id?: string; type: "response"; command: "steer"; success: true }
|
||||||
|
| { id?: string; type: "response"; command: "follow_up"; success: true }
|
||||||
| { id?: string; type: "response"; command: "abort"; success: true }
|
| { id?: string; type: "response"; command: "abort"; success: true }
|
||||||
| { id?: string; type: "response"; command: "new_session"; success: true; data: { cancelled: boolean } }
|
| { id?: string; type: "response"; command: "new_session"; success: true; data: { cancelled: boolean } }
|
||||||
|
|
||||||
|
|
@ -125,8 +129,9 @@ export type RpcResponse =
|
||||||
data: { level: ThinkingLevel } | null;
|
data: { level: ThinkingLevel } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue mode
|
// Queue modes
|
||||||
| { id?: string; type: "response"; command: "set_queue_mode"; success: true }
|
| { id?: string; type: "response"; command: "set_steering_mode"; success: true }
|
||||||
|
| { id?: string; type: "response"; command: "set_follow_up_mode"; success: true }
|
||||||
|
|
||||||
// Compaction
|
// Compaction
|
||||||
| { id?: string; type: "response"; command: "compact"; success: true; data: CompactionResult }
|
| { id?: string; type: "response"; command: "compact"; success: true; data: CompactionResult }
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ describe("AgentSession concurrent prompt guard", () => {
|
||||||
|
|
||||||
// Second prompt should reject
|
// Second prompt should reject
|
||||||
await expect(session.prompt("Second message")).rejects.toThrow(
|
await expect(session.prompt("Second message")).rejects.toThrow(
|
||||||
"Agent is already processing. Use queueMessage() to queue messages during streaming.",
|
"Agent is already processing. Use steer() or followUp() to queue messages during streaming.",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
|
|
@ -135,15 +135,31 @@ describe("AgentSession concurrent prompt guard", () => {
|
||||||
await firstPrompt.catch(() => {}); // Ignore abort error
|
await firstPrompt.catch(() => {}); // Ignore abort error
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow queueMessage() while streaming", async () => {
|
it("should allow steer() while streaming", async () => {
|
||||||
createSession();
|
createSession();
|
||||||
|
|
||||||
// Start first prompt
|
// Start first prompt
|
||||||
const firstPrompt = session.prompt("First message");
|
const firstPrompt = session.prompt("First message");
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
|
||||||
// queueMessage should work while streaming
|
// steer should work while streaming
|
||||||
expect(() => session.queueMessage("Queued message")).not.toThrow();
|
expect(() => session.steer("Steering message")).not.toThrow();
|
||||||
|
expect(session.pendingMessageCount).toBe(1);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await session.abort();
|
||||||
|
await firstPrompt.catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow followUp() while streaming", async () => {
|
||||||
|
createSession();
|
||||||
|
|
||||||
|
// Start first prompt
|
||||||
|
const firstPrompt = session.prompt("First message");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
|
||||||
|
// followUp should work while streaming
|
||||||
|
expect(() => session.followUp("Follow-up message")).not.toThrow();
|
||||||
expect(session.pendingMessageCount).toBe(1);
|
expect(session.pendingMessageCount).toBe(1);
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
|
|
|
||||||
|
|
@ -495,11 +495,19 @@ export class MomSettingsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compatibility methods for AgentSession
|
// Compatibility methods for AgentSession
|
||||||
getQueueMode(): "all" | "one-at-a-time" {
|
getSteeringMode(): "all" | "one-at-a-time" {
|
||||||
return "one-at-a-time"; // Mom processes one message at a time
|
return "one-at-a-time"; // Mom processes one message at a time
|
||||||
}
|
}
|
||||||
|
|
||||||
setQueueMode(_mode: "all" | "one-at-a-time"): void {
|
setSteeringMode(_mode: "all" | "one-at-a-time"): void {
|
||||||
|
// No-op for mom
|
||||||
|
}
|
||||||
|
|
||||||
|
getFollowUpMode(): "all" | "one-at-a-time" {
|
||||||
|
return "one-at-a-time"; // Mom processes one message at a time
|
||||||
|
}
|
||||||
|
|
||||||
|
setFollowUpMode(_mode: "all" | "one-at-a-time"): void {
|
||||||
// No-op for mom
|
// No-op for mom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -346,7 +346,7 @@ const renderApp = () => {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
// Demo: Inject custom message (will appear on next agent run)
|
// Demo: Inject custom message (will appear on next agent run)
|
||||||
if (agent) {
|
if (agent) {
|
||||||
agent.queueMessage(
|
agent.steer(
|
||||||
createSystemNotification(
|
createSystemNotification(
|
||||||
"This is a custom message! It appears in the UI but is never sent to the LLM.",
|
"This is a custom message! It appears in the UI but is never sent to the LLM.",
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue