feat: implement execution control (pause/resume/abort) for agent mode

Adds execution control system per GitHub issue #113:
- Ctrl+P: Toggle pause/resume during agent execution
- Ctrl+Z: Abort with rollback (undo file changes)
- Ctrl+Shift+S: Toggle step-by-step mode
- Enter: Advance one step when in step mode

New files:
- src/types/execution-control.ts: Type definitions
- src/services/execution-control.ts: Control implementation with rollback
- src/constants/execution-control.ts: Keyboard shortcuts and messages

Modified:
- agent-stream.ts: Integrated execution control into agent loop
- message-handler.ts: Added control functions and callbacks
- app.tsx: Added keyboard shortcut handlers
- help-content.ts: Added help topics for new shortcuts

Closes #113
This commit is contained in:
2026-02-05 18:47:08 -05:00
parent e2cb41f8d3
commit 3d2195f074
8 changed files with 853 additions and 31 deletions

View File

@@ -9,7 +9,13 @@ import {
} from "solid-js";
import { batch } from "solid-js";
import { getFiles } from "@services/file-picker/files";
import { abortCurrentOperation } from "@services/chat-tui-service";
import {
abortCurrentOperation,
togglePauseResume,
setStepMode,
advanceStep,
getExecutionState,
} from "@services/chat-tui-service";
import versionData from "@/version.json";
import { ExitProvider, useExit } from "@tui-solid/context/exit";
import { RouteProvider, useRoute } from "@tui-solid/context/route";
@@ -159,16 +165,74 @@ function AppContent(props: AppProps) {
useKeyboard((evt) => {
// ESC aborts current operation
if (evt.name === "escape") {
const aborted = abortCurrentOperation();
if (aborted) {
toast.info("Operation cancelled");
abortCurrentOperation(false).then((aborted) => {
if (aborted) {
toast.info("Operation cancelled");
}
});
evt.preventDefault();
return;
}
// Ctrl+P toggles pause/resume during execution
if (evt.ctrl && evt.name === "p") {
const toggled = togglePauseResume();
if (toggled) {
const state = getExecutionState();
toast.info(state.state === "paused" ? "⏸ Execution paused" : "▶ Execution resumed");
evt.preventDefault();
return;
}
}
// Ctrl+C exits the application
// Ctrl+Z aborts with rollback
if (evt.ctrl && evt.name === "z") {
const state = getExecutionState();
if (state.state !== "idle") {
abortCurrentOperation(true).then((aborted) => {
if (aborted) {
toast.info(`Aborted with rollback of ${state.rollbackCount} action(s)`);
}
});
evt.preventDefault();
return;
}
}
// Ctrl+Shift+S toggles step mode
if (evt.ctrl && evt.shift && evt.name === "s") {
const state = getExecutionState();
if (state.state !== "idle") {
const isStepMode = state.state === "stepping";
setStepMode(!isStepMode);
toast.info(isStepMode ? "🏃 Step mode disabled" : "🚶 Step mode enabled");
evt.preventDefault();
return;
}
}
// Enter advances step when waiting for step confirmation
if (evt.name === "return" && !evt.ctrl && !evt.shift) {
const state = getExecutionState();
if (state.waitingForStep) {
advanceStep();
evt.preventDefault();
return;
}
}
// Ctrl+C exits the application (with confirmation)
if (evt.ctrl && evt.name === "c") {
// First try to abort current operation
const state = getExecutionState();
if (state.state !== "idle") {
abortCurrentOperation(false).then(() => {
toast.info("Operation cancelled. Press Ctrl+C again to exit.");
});
evt.preventDefault();
return;
}
if (app.interruptPending()) {
exit.exit(0);
evt.preventDefault();