147 lines
3.9 KiB
TypeScript
147 lines
3.9 KiB
TypeScript
// ABOUTME: Double-tap ESC cancels all running operations (agent stream, subagents, chains, pipelines).
|
|
// ABOUTME: Listens for raw terminal ESC input and detects two presses within 400ms.
|
|
|
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
import { matchesKey } from "@mariozechner/pi-tui";
|
|
import { applyExtensionDefaults } from "./lib/themeMap.ts";
|
|
|
|
/** Time window (ms) for two ESC presses to be considered a double-tap. */
|
|
const DOUBLE_TAP_WINDOW = 400;
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
let lastEscTime = 0;
|
|
let unsub: (() => void) | null = null;
|
|
let isAgentRunning = false;
|
|
|
|
function cancelAll(ctx: any) {
|
|
const g = globalThis as any;
|
|
let cancelled = false;
|
|
|
|
// 1. Abort the main agent stream
|
|
if (!ctx.isIdle()) {
|
|
ctx.abort();
|
|
cancelled = true;
|
|
}
|
|
|
|
// 2. Kill all running subagents (exposed by subagent-widget.ts)
|
|
if (typeof g.__piKillAllSubagents === "function") {
|
|
const killed = g.__piKillAllSubagents();
|
|
if (killed > 0) cancelled = true;
|
|
}
|
|
|
|
// 3. Kill running chain process (exposed by agent-chain.ts)
|
|
if (typeof g.__piKillChainProc === "function") {
|
|
if (g.__piKillChainProc()) cancelled = true;
|
|
}
|
|
|
|
// 4. Kill running pipeline processes (exposed by pipeline-team.ts)
|
|
if (typeof g.__piKillPipelineProc === "function") {
|
|
if (g.__piKillPipelineProc()) cancelled = true;
|
|
}
|
|
|
|
// 5. Kill running team agent processes (exposed by agent-team.ts)
|
|
if (typeof g.__piKillTeamProcs === "function") {
|
|
const killed = g.__piKillTeamProcs();
|
|
if (killed > 0) cancelled = true;
|
|
}
|
|
|
|
if (cancelled) {
|
|
ctx.ui.notify("All operations cancelled (ESC ESC)", "warning");
|
|
}
|
|
}
|
|
|
|
function setupInputListener(ctx: any) {
|
|
if (unsub) return; // Already listening
|
|
|
|
unsub = ctx.ui.onTerminalInput((data: string) => {
|
|
// Only detect bare ESC key
|
|
if (!matchesKey(data, "escape")) return undefined;
|
|
|
|
const now = Date.now();
|
|
if (now - lastEscTime < DOUBLE_TAP_WINDOW) {
|
|
// Double-tap detected
|
|
lastEscTime = 0;
|
|
// Only cancel if something is actually running
|
|
if (!ctx.isIdle() || hasRunningOperations()) {
|
|
cancelAll(ctx);
|
|
return { consume: true };
|
|
}
|
|
} else {
|
|
lastEscTime = now;
|
|
}
|
|
|
|
// Don't consume — let the normal ESC handler work
|
|
return undefined;
|
|
});
|
|
}
|
|
|
|
/** Check if there are running subagents, chains, or pipelines. */
|
|
function hasRunningOperations(): boolean {
|
|
const g = globalThis as any;
|
|
|
|
// Check subagents
|
|
if (typeof g.__piHasRunningSubagents === "function" && g.__piHasRunningSubagents()) {
|
|
return true;
|
|
}
|
|
|
|
// Check chain
|
|
if (g.__piActiveChain && typeof g.__piHasRunningChain === "function" && g.__piHasRunningChain()) {
|
|
return true;
|
|
}
|
|
|
|
// Check pipeline
|
|
if (g.__piActivePipeline && typeof g.__piHasRunningPipeline === "function" && g.__piHasRunningPipeline()) {
|
|
return true;
|
|
}
|
|
|
|
// Check team
|
|
if (typeof g.__piHasRunningTeam === "function" && g.__piHasRunningTeam()) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// ── Track agent state for status hint ─────────────────
|
|
|
|
pi.on("agent_start", async (_event, ctx) => {
|
|
isAgentRunning = true;
|
|
if (ctx.hasUI) {
|
|
ctx.ui.setStatus("esc-hint", "\x1b[2m ESC ESC to cancel\x1b[0m");
|
|
}
|
|
});
|
|
|
|
pi.on("agent_end", async (_event, ctx) => {
|
|
isAgentRunning = false;
|
|
if (ctx.hasUI) {
|
|
ctx.ui.setStatus("esc-hint", undefined);
|
|
}
|
|
});
|
|
|
|
// ── Session lifecycle ─────────────────────────────────
|
|
|
|
pi.on("session_start", async (_event, ctx) => {
|
|
applyExtensionDefaults(import.meta.url, ctx);
|
|
lastEscTime = 0;
|
|
isAgentRunning = false;
|
|
if (ctx.hasUI) {
|
|
setupInputListener(ctx);
|
|
}
|
|
});
|
|
|
|
pi.on("session_switch", async (_event, ctx) => {
|
|
lastEscTime = 0;
|
|
isAgentRunning = false;
|
|
if (ctx.hasUI) {
|
|
ctx.ui.setStatus("esc-hint", undefined);
|
|
}
|
|
});
|
|
|
|
pi.on("session_shutdown", async () => {
|
|
if (unsub) {
|
|
unsub();
|
|
unsub = null;
|
|
}
|
|
});
|
|
}
|