feat: add shell process type to bg-shell for persistent interactive sessions (#236)
Shell-type processes provide a persistent execution environment where shell state accumulates across commands. Key behaviors: - Auto-transitions to ready status after spawn (200ms delay) - Defaults to user's shell when no command specified - Extended dead process TTL (6x normal) for potential restart - Command history tracking via commandHistory field - Prompt guideline for discoverability Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
daca368ba2
commit
df1c2b5b54
1 changed files with 28 additions and 10 deletions
|
|
@ -88,7 +88,7 @@ type ProcessStatus =
|
|||
| "exited"
|
||||
| "crashed";
|
||||
|
||||
type ProcessType = "server" | "build" | "test" | "watcher" | "generic";
|
||||
type ProcessType = "server" | "build" | "test" | "watcher" | "generic" | "shell";
|
||||
|
||||
interface ProcessEvent {
|
||||
type:
|
||||
|
|
@ -164,6 +164,8 @@ interface BgProcess {
|
|||
lastErrorCount: number;
|
||||
/** Last warning count snapshot for diff detection */
|
||||
lastWarningCount: number;
|
||||
/** Command history for shell-type sessions */
|
||||
commandHistory: string[];
|
||||
/** Dedup tracker: hash → count of repeated lines */
|
||||
lineDedup: Map<string, number>;
|
||||
/** Total raw lines (before dedup) for token savings calc */
|
||||
|
|
@ -583,7 +585,9 @@ function startProcess(opts: StartOptions): BgProcess {
|
|||
const env = { ...process.env, ...(opts.env || {}) };
|
||||
|
||||
const { shell, args: shellArgs } = getShellConfig();
|
||||
const proc = spawn(shell, [...shellArgs, sanitizeCommand(opts.command)], {
|
||||
// Shell sessions default to the user's shell if no command specified
|
||||
const command = processType === "shell" && !opts.command ? shell : opts.command;
|
||||
const proc = spawn(shell, [...shellArgs, sanitizeCommand(command)], {
|
||||
cwd: opts.cwd,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env,
|
||||
|
|
@ -592,8 +596,8 @@ function startProcess(opts: StartOptions): BgProcess {
|
|||
|
||||
const bg: BgProcess = {
|
||||
id,
|
||||
label: opts.label || opts.command.slice(0, 60),
|
||||
command: opts.command,
|
||||
label: opts.label || command.slice(0, 60),
|
||||
command,
|
||||
cwd: opts.cwd,
|
||||
startedAt: Date.now(),
|
||||
proc,
|
||||
|
|
@ -615,14 +619,15 @@ function startProcess(opts: StartOptions): BgProcess {
|
|||
group: opts.group || null,
|
||||
lastErrorCount: 0,
|
||||
lastWarningCount: 0,
|
||||
commandHistory: [],
|
||||
lineDedup: new Map(),
|
||||
totalRawLines: 0,
|
||||
envKeys: Object.keys(opts.env || {}),
|
||||
restartCount: 0,
|
||||
startConfig: {
|
||||
command: opts.command,
|
||||
command,
|
||||
cwd: opts.cwd,
|
||||
label: opts.label || opts.command.slice(0, 60),
|
||||
label: opts.label || command.slice(0, 60),
|
||||
processType,
|
||||
readyPattern: opts.readyPattern || null,
|
||||
readyPort: opts.readyPort || null,
|
||||
|
|
@ -630,7 +635,7 @@ function startProcess(opts: StartOptions): BgProcess {
|
|||
},
|
||||
};
|
||||
|
||||
addEvent(bg, { type: "started", detail: `Process started: ${opts.command.slice(0, 100)}` });
|
||||
addEvent(bg, { type: "started", detail: `Process started: ${command.slice(0, 100)}` });
|
||||
|
||||
proc.stdout?.on("data", (chunk: Buffer) => {
|
||||
const lines = chunk.toString().split("\n");
|
||||
|
|
@ -687,6 +692,15 @@ function startProcess(opts: StartOptions): BgProcess {
|
|||
startPortProbing(bg, bg.readyPort);
|
||||
}
|
||||
|
||||
// Shell sessions are ready immediately after spawn
|
||||
if (bg.processType === "shell") {
|
||||
setTimeout(() => {
|
||||
if (bg.alive && bg.status === "starting") {
|
||||
transitionToReady(bg, "Shell session initialized");
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
processes.set(id, bg);
|
||||
return bg;
|
||||
}
|
||||
|
|
@ -1011,8 +1025,11 @@ function formatTimeAgo(timestamp: number): string {
|
|||
function pruneDeadProcesses(): void {
|
||||
const now = Date.now();
|
||||
for (const [id, bg] of processes) {
|
||||
if (!bg.alive && now - bg.startedAt > DEAD_PROCESS_TTL) {
|
||||
processes.delete(id);
|
||||
if (!bg.alive) {
|
||||
const ttl = bg.processType === "shell" ? DEAD_PROCESS_TTL * 6 : DEAD_PROCESS_TTL;
|
||||
if (now - bg.startedAt > ttl) {
|
||||
processes.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1191,6 +1208,7 @@ export default function (pi: ExtensionAPI) {
|
|||
"Use 'restart' to kill and relaunch with the same config — preserves restart count.",
|
||||
"Background processes are auto-classified (server/build/test/watcher) based on the command.",
|
||||
"Process crashes and errors are automatically surfaced as alerts at the start of your next turn — you don't need to poll.",
|
||||
"To create a persistent shell session: bg_shell start with type:'shell'. The session stays alive for interactive use with 'send', 'send_and_wait', or 'run'.",
|
||||
],
|
||||
|
||||
parameters: Type.Object({
|
||||
|
|
@ -1239,7 +1257,7 @@ export default function (pi: ExtensionAPI) {
|
|||
Type.Number({ description: "Timeout in milliseconds (for wait_for_ready, send_and_wait). Default: 30000" }),
|
||||
),
|
||||
type: Type.Optional(
|
||||
StringEnum(["server", "build", "test", "watcher", "generic"] as const),
|
||||
StringEnum(["server", "build", "test", "watcher", "generic", "shell"] as const),
|
||||
),
|
||||
ready_pattern: Type.Optional(
|
||||
Type.String({ description: "Regex pattern that indicates the process is ready (for start)" }),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue