From b4ae98ecc8ed25091347ce82735c42dff8b10742 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:22:48 +0530 Subject: [PATCH 01/13] add background job manager and output display --- .../cli/src/services/BackgroundJobManager.ts | 220 ++++++++++++++++++ extensions/cli/src/services/types.ts | 5 + extensions/cli/src/slashCommands.ts | 32 +++ 3 files changed, 257 insertions(+) create mode 100644 extensions/cli/src/services/BackgroundJobManager.ts diff --git a/extensions/cli/src/services/BackgroundJobManager.ts b/extensions/cli/src/services/BackgroundJobManager.ts new file mode 100644 index 00000000000..83816362c6c --- /dev/null +++ b/extensions/cli/src/services/BackgroundJobManager.ts @@ -0,0 +1,220 @@ +import { ChildProcess, spawn } from "child_process"; +import { EventEmitter } from "events"; + +import { logger } from "../util/logger.js"; + +export type BackgroundJobStatus = + | "pending" + | "running" + | "completed" + | "failed" + | "cancelled"; + +export interface BackgroundJob { + id: string; + status: BackgroundJobStatus; + command: string; + output: string; + exitCode: number | null; + startTime: Date; + endTime: Date | null; + error?: string; +} + +const MAX_CONCURRENT_JOBS = 5; + +export class BackgroundJobManager extends EventEmitter { + private jobs: Map = new Map(); + private processes: Map = new Map(); + private jobCounter = 0; + + createJob(command: string): BackgroundJob | null { + const runningCount = this.getRunningJobCount(); + if (runningCount >= MAX_CONCURRENT_JOBS) { + logger.warn( + `Cannot create background job: limit of ${MAX_CONCURRENT_JOBS} reached`, + ); + return null; + } + + const id = `bg-${++this.jobCounter}-${Date.now()}`; + const job: BackgroundJob = { + id, + status: "pending", + command, + output: "", + exitCode: null, + startTime: new Date(), + endTime: null, + }; + + this.jobs.set(id, job); + this.emit("jobCreated", job); + return job; + } + + startJob(jobId: string, shell: string, args: string[]): ChildProcess | null { + const job = this.jobs.get(jobId); + if (!job) { + logger.error(`Cannot start job ${jobId}: job not found`); + return null; + } + + job.status = "running"; + this.emit("jobStarted", job); + + const child = spawn(shell, args, { stdio: "pipe" }); + this.processes.set(jobId, child); + + child.stdout?.on("data", (data: Buffer) => { + this.appendOutput(jobId, data.toString()); + }); + + child.stderr?.on("data", (data: Buffer) => { + this.appendOutput(jobId, data.toString()); + }); + + child.on("close", (code: number | null) => { + this.completeJob(jobId, code ?? 0); + }); + + child.on("error", (error: Error) => { + this.failJob(jobId, error.message); + }); + + return child; + } + + createJobWithProcess( + command: string, + child: ChildProcess, + existingOutput: string = "", + ): BackgroundJob | null { + const runningCount = this.getRunningJobCount(); + if (runningCount >= MAX_CONCURRENT_JOBS) { + logger.warn( + `Cannot create background job: limit of ${MAX_CONCURRENT_JOBS} reached`, + ); + return null; + } + + const id = `bg-${++this.jobCounter}-${Date.now()}`; + const job: BackgroundJob = { + id, + status: "running", + command, + output: existingOutput, + exitCode: null, + startTime: new Date(), + endTime: null, + }; + + this.jobs.set(id, job); + this.processes.set(id, child); + this.emit("jobCreated", job); + + child.stdout?.on("data", (data: Buffer) => { + this.appendOutput(id, data.toString()); + }); + + child.stderr?.on("data", (data: Buffer) => { + this.appendOutput(id, data.toString()); + }); + + child.on("close", (code: number | null) => { + this.completeJob(id, code ?? 0); + }); + + child.on("error", (error: Error) => { + this.failJob(id, error.message); + }); + + return job; + } + + appendOutput(jobId: string, data: string): void { + const job = this.jobs.get(jobId); + if (job) { + job.output += data; + this.emit("jobOutput", job, data); + } + } + + completeJob(jobId: string, exitCode: number): void { + const job = this.jobs.get(jobId); + if (job) { + job.status = exitCode === 0 ? "completed" : "failed"; + job.exitCode = exitCode; + job.endTime = new Date(); + this.processes.delete(jobId); + this.emit("jobCompleted", job); + } + } + + failJob(jobId: string, error: string): void { + const job = this.jobs.get(jobId); + if (job) { + job.status = "failed"; + job.error = error; + job.endTime = new Date(); + this.processes.delete(jobId); + this.emit("jobFailed", job); + } + } + + cancelJob(jobId: string): boolean { + const job = this.jobs.get(jobId); + const process = this.processes.get(jobId); + + if (!job) return false; + + if (process) { + process.kill(); + this.processes.delete(jobId); + } + + job.status = "cancelled"; + job.endTime = new Date(); + this.emit("jobCancelled", job); + return true; + } + + getJob(jobId: string): BackgroundJob | undefined { + return this.jobs.get(jobId); + } + + getRunningJobs(): BackgroundJob[] { + return Array.from(this.jobs.values()).filter( + (job) => job.status === "running" || job.status === "pending", + ); + } + + getAllJobs(): BackgroundJob[] { + return Array.from(this.jobs.values()); + } + + getRunningJobCount(): number { + return this.getRunningJobs().length; + } + + killAllJobs(): void { + for (const [jobId, process] of this.processes) { + process.kill(); + const job = this.jobs.get(jobId); + if (job) { + job.status = "cancelled"; + job.endTime = new Date(); + } + } + this.processes.clear(); + this.emit("allJobsKilled"); + } + + reset(): void { + this.killAllJobs(); + this.jobs.clear(); + this.jobCounter = 0; + } +} + +export const backgroundJobManager = new BackgroundJobManager(); diff --git a/extensions/cli/src/services/types.ts b/extensions/cli/src/services/types.ts index b9804bcd26b..9965a8fabec 100644 --- a/extensions/cli/src/services/types.ts +++ b/extensions/cli/src/services/types.ts @@ -130,6 +130,10 @@ export interface ArtifactUploadServiceState { lastError: string | null; } +export type { + BackgroundJob, + BackgroundJobStatus, +} from "./BackgroundJobManager.js"; export type { ChatHistoryState } from "./ChatHistoryService.js"; export type { FileIndexServiceState } from "./FileIndexService.js"; export type { GitAiIntegrationServiceState } from "./GitAiIntegrationService.js"; @@ -153,6 +157,7 @@ export const SERVICE_NAMES = { AGENT_FILE: "agentFile", ARTIFACT_UPLOAD: "artifactUpload", GIT_AI_INTEGRATION: "gitAiIntegration", + BACKGROUND_JOBS: "backgroundJobs", } as const; /** diff --git a/extensions/cli/src/slashCommands.ts b/extensions/cli/src/slashCommands.ts index c28390d0cec..d00b82bfb4f 100644 --- a/extensions/cli/src/slashCommands.ts +++ b/extensions/cli/src/slashCommands.ts @@ -9,6 +9,7 @@ import { import { getAllSlashCommands } from "./commands/commands.js"; import { handleInit } from "./commands/init.js"; import { handleInfoSlashCommand } from "./infoScreen.js"; +import { backgroundJobManager } from "./services/BackgroundJobManager.js"; import { reloadService, SERVICE_NAMES, services } from "./services/index.js"; import { getCurrentSession, updateSessionTitle } from "./session.js"; import { posthogService } from "./telemetry/posthogService.js"; @@ -169,6 +170,36 @@ function handleTitle(args: string[]) { } } +function handleJobs() { + const allJobs = backgroundJobManager.getAllJobs(); + if (allJobs.length === 0) { + return { + exit: false, + output: chalk.dim("No background jobs"), + }; + } + + const lines = [chalk.bold("Background Jobs:")]; + for (const job of allJobs) { + const statusColor = + job.status === "running" || job.status === "pending" + ? chalk.yellow + : job.status === "completed" + ? chalk.green + : chalk.red; + const duration = job.endTime + ? `${Math.round((job.endTime.getTime() - job.startTime.getTime()) / 1000)}s` + : `${Math.round((Date.now() - job.startTime.getTime()) / 1000)}s`; + lines.push( + ` ${statusColor(job.status.padEnd(10))} ${chalk.cyan(job.id)} ${chalk.dim(`(${duration})`)} ${job.command.substring(0, 40)}${job.command.length > 40 ? "..." : ""}`, + ); + } + return { + exit: false, + output: lines.join("\n"), + }; +} + const commandHandlers: Record = { help: handleHelp, clear: () => { @@ -203,6 +234,7 @@ const commandHandlers: Record = { update: () => { return { openUpdateSelector: true }; }, + jobs: handleJobs, }; export async function handleSlashCommands( From 5a479ca427091805ff7f000091d84f1fae421a8c Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:26:09 +0530 Subject: [PATCH 02/13] added checkBackgroundJob tool --- extensions/cli/src/services/index.ts | 2 + .../cli/src/tools/checkBackgroundJob.ts | 64 +++++++++++++++++++ extensions/cli/src/tools/index.tsx | 2 + 3 files changed, 68 insertions(+) create mode 100644 extensions/cli/src/tools/checkBackgroundJob.ts diff --git a/extensions/cli/src/services/index.ts b/extensions/cli/src/services/index.ts index 56a5d4e938a..6ca30e4a040 100644 --- a/extensions/cli/src/services/index.ts +++ b/extensions/cli/src/services/index.ts @@ -10,6 +10,7 @@ import { AgentFileService } from "./AgentFileService.js"; import { ApiClientService } from "./ApiClientService.js"; import { ArtifactUploadService } from "./ArtifactUploadService.js"; import { AuthService } from "./AuthService.js"; +import { backgroundJobManager } from "./BackgroundJobManager.js"; import { ChatHistoryService } from "./ChatHistoryService.js"; import { ConfigService } from "./ConfigService.js"; import { FileIndexService } from "./FileIndexService.js"; @@ -387,6 +388,7 @@ export const services = { toolPermissions: toolPermissionService, artifactUpload: artifactUploadService, gitAiIntegration: gitAiIntegrationService, + backgroundJobs: backgroundJobManager, } as const; export type ServicesType = typeof services; diff --git a/extensions/cli/src/tools/checkBackgroundJob.ts b/extensions/cli/src/tools/checkBackgroundJob.ts new file mode 100644 index 00000000000..08968b91fb8 --- /dev/null +++ b/extensions/cli/src/tools/checkBackgroundJob.ts @@ -0,0 +1,64 @@ +import { services } from "../services/index.js"; + +import { Tool } from "./types.js"; + +export const checkBackgroundJobTool: Tool = { + name: "CheckBackgroundJob", + displayName: "Check Background Job", + description: `Check the status and output of a background job. +Returns the current status, exit code (if finished), and all available output. +If the job is still running, returns partial output with "running" status.`, + parameters: { + type: "object", + required: ["job_id"], + properties: { + job_id: { + type: "string", + description: + "The ID of the background job to check (e.g., bg-1-1234567890)", + }, + }, + }, + readonly: true, + isBuiltIn: true, + run: async ({ job_id }: { job_id: string }): Promise => { + const job = services.backgroundJobs.getJob(job_id); + + if (!job) { + return JSON.stringify({ + error: `Job ${job_id} not found`, + available_jobs: services.backgroundJobs.getAllJobs().map((j) => ({ + id: j.id, + status: j.status, + command: + j.command.substring(0, 50) + (j.command.length > 50 ? "..." : ""), + })), + }); + } + + const result: Record = { + job_id: job.id, + status: job.status, + command: job.command, + output: job.output, + started_at: job.startTime.toISOString(), + }; + + if (job.endTime) { + result.ended_at = job.endTime.toISOString(); + result.duration_seconds = Math.round( + (job.endTime.getTime() - job.startTime.getTime()) / 1000, + ); + } + + if (job.exitCode !== null) { + result.exit_code = job.exitCode; + } + + if (job.error) { + result.error = job.error; + } + + return JSON.stringify(result, null, 2); + }, +}; diff --git a/extensions/cli/src/tools/index.tsx b/extensions/cli/src/tools/index.tsx index 20d37d6d365..eb10d870d62 100644 --- a/extensions/cli/src/tools/index.tsx +++ b/extensions/cli/src/tools/index.tsx @@ -19,6 +19,7 @@ import { telemetryService } from "../telemetry/telemetryService.js"; import { logger } from "../util/logger.js"; import { ALL_BUILT_IN_TOOLS } from "./allBuiltIns.js"; +import { checkBackgroundJobTool } from "./checkBackgroundJob.js"; import { editTool } from "./edit.js"; import { exitTool } from "./exit.js"; import { fetchTool } from "./fetch.js"; @@ -68,6 +69,7 @@ const BASE_BUILTIN_TOOLS: Tool[] = [ runTerminalCommandTool, fetchTool, writeChecklistTool, + checkBackgroundJobTool, ]; const BUILTIN_SEARCH_TOOLS: Tool[] = [searchCodeTool]; From 561f85e95caecc08c80cab1859b35f573c5678c5 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:36:45 +0530 Subject: [PATCH 03/13] move process to background with ctrl+b --- .../src/services/BackgroundSignalManager.ts | 9 +++ .../cli/src/tools/runTerminalCommand.ts | 67 ++++++++++++++++++- extensions/cli/src/ui/hooks/useUserInput.ts | 7 ++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 extensions/cli/src/services/BackgroundSignalManager.ts diff --git a/extensions/cli/src/services/BackgroundSignalManager.ts b/extensions/cli/src/services/BackgroundSignalManager.ts new file mode 100644 index 00000000000..b183ea30ba4 --- /dev/null +++ b/extensions/cli/src/services/BackgroundSignalManager.ts @@ -0,0 +1,9 @@ +import { EventEmitter } from "events"; + +export class BackgroundSignalManager extends EventEmitter { + signalBackground(): void { + this.emit("backgroundRequested"); + } +} + +export const backgroundSignalManager = new BackgroundSignalManager(); diff --git a/extensions/cli/src/tools/runTerminalCommand.ts b/extensions/cli/src/tools/runTerminalCommand.ts index 5b7f08b30fc..57d1d0e5291 100644 --- a/extensions/cli/src/tools/runTerminalCommand.ts +++ b/extensions/cli/src/tools/runTerminalCommand.ts @@ -1,4 +1,4 @@ -import { spawn } from "child_process"; +import { ChildProcess, spawn } from "child_process"; import fs from "fs"; import { @@ -6,6 +6,8 @@ import { type ToolPolicy, } from "@continuedev/terminal-security"; +import { backgroundJobManager } from "../services/BackgroundJobManager.js"; +import { backgroundSignalManager } from "../services/BackgroundSignalManager.js"; import { telemetryService } from "../telemetry/telemetryService.js"; import { isGitCommitCommand, @@ -82,6 +84,35 @@ function getShellCommand(command: string): { shell: string; args: string[] } { return { shell: userShell, args: ["-l", "-c", command] }; } +export function runCommandInBackground(command: string): { + success: boolean; + jobId?: string; + error?: string; +} { + const job = backgroundJobManager.createJob(command); + if (!job) { + return { + success: false, + error: "Cannot create background job: limit of 5 concurrent jobs reached", + }; + } + + const { shell, args } = getShellCommand(command); + const child = backgroundJobManager.startJob(job.id, shell, args); + + if (!child) { + return { + success: false, + error: `Failed to start background job ${job.id}`, + }; + } + + return { + success: true, + jobId: job.id, + }; +} + export const runTerminalCommandTool: Tool = { name: "Bash", displayName: "Bash", @@ -187,6 +218,34 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed return output; }; + const moveToBackground = () => { + if (isResolved) return; + isResolved = true; + + if (timeoutId) { + clearTimeout(timeoutId); + } + backgroundSignalManager.off("backgroundRequested", moveToBackground); + + const job = backgroundJobManager.createJobWithProcess( + command, + child as ChildProcess, + stdout, + ); + + if (job) { + resolve( + `Command moved to background. Job ID: ${job.id}\nOutput so far:\n${stdout}\nUse CheckBackgroundJob("${job.id}") to check status.`, + ); + } else { + resolve( + `Failed to move to background (job limit reached). Command continues in foreground.\nOutput so far: ${stdout}`, + ); + } + }; + + backgroundSignalManager.on("backgroundRequested", moveToBackground); + const resetTimeout = () => { if (timeoutId) { clearTimeout(timeoutId); @@ -230,6 +289,11 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed clearTimeout(timeoutId); } + backgroundSignalManager.removeListener( + "backgroundRequested", + moveToBackground, + ); + // Only reject on non-zero exit code if there's also stderr if (code !== 0 && stderr) { reject(`Error (exit code ${code}): ${stderr}`); @@ -267,6 +331,7 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed if (timeoutId) { clearTimeout(timeoutId); } + backgroundSignalManager.off("backgroundRequested", moveToBackground); reject(`Error: ${error.message}`); }); }); diff --git a/extensions/cli/src/ui/hooks/useUserInput.ts b/extensions/cli/src/ui/hooks/useUserInput.ts index 334d23fc304..366522ff15e 100644 --- a/extensions/cli/src/ui/hooks/useUserInput.ts +++ b/extensions/cli/src/ui/hooks/useUserInput.ts @@ -1,4 +1,5 @@ import type { PermissionMode } from "../../permissions/types.js"; +import { backgroundSignalManager } from "../../services/BackgroundSignalManager.js"; import { checkClipboardForImage, getClipboardImage, @@ -85,6 +86,12 @@ export function handleControlKeys(options: ControlKeysOptions): boolean { return true; } + // Handle Ctrl+B to send current tool execution to background + if (key.ctrl && input === "b") { + backgroundSignalManager.signalBackground(); + return true; + } + // Handle Shift+Tab to cycle through modes if (key.tab && key.shift && !showSlashCommands && !showFileSearch) { cycleModes().catch((error) => { From 60bef4b4d4aca88f033866962af4d8b784b71467 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:39:17 +0530 Subject: [PATCH 04/13] add the jobs slash command --- extensions/cli/src/commands/commands.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extensions/cli/src/commands/commands.ts b/extensions/cli/src/commands/commands.ts index e04849c587d..0c85f2228c1 100644 --- a/extensions/cli/src/commands/commands.ts +++ b/extensions/cli/src/commands/commands.ts @@ -2,11 +2,11 @@ import { type AssistantConfig } from "@continuedev/sdk"; // Export command functions export { chat } from "./chat.js"; -export { review } from "./review.js"; export { login } from "./login.js"; export { logout } from "./logout.js"; export { listSessionsCommand } from "./ls.js"; export { remote } from "./remote.js"; +export { review } from "./review.js"; export { serve } from "./serve.js"; export interface SlashCommand { @@ -101,6 +101,11 @@ export const SYSTEM_SLASH_COMMANDS: SystemCommand[] = [ description: "Exit the chat", category: "system", }, + { + name: "jobs", + description: "List background jobs", + category: "system", + }, ]; // Remote mode specific commands From 007094609f8d077b194b0c17943e77e75b68d6df Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:48:18 +0530 Subject: [PATCH 05/13] kill all background jobs on exit --- extensions/cli/src/util/exit.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/extensions/cli/src/util/exit.ts b/extensions/cli/src/util/exit.ts index a378cd8de64..3642669974e 100644 --- a/extensions/cli/src/util/exit.ts +++ b/extensions/cli/src/util/exit.ts @@ -1,6 +1,7 @@ import type { ChatHistoryItem } from "core/index.js"; import { sentryService } from "../sentry.js"; +import { backgroundJobManager } from "../services/BackgroundJobManager.js"; import { getSessionUsage } from "../session.js"; import { telemetryService } from "../telemetry/telemetryService.js"; @@ -194,6 +195,17 @@ export async function gracefulExit(code: number = 0): Promise { // Display session usage breakdown in verbose mode displaySessionUsage(); + try { + // todo: rename backgroundJobManager to backgroundCommandService + const runningJobs = backgroundJobManager.getRunningJobCount(); + if (runningJobs > 0) { + logger.debug(`Killing ${runningJobs} background job(s) on exit`); + backgroundJobManager.killAllJobs(); + } + } catch (err) { + logger.debug("Background job cleanup error (ignored)", err as any); + } + try { // Flush metrics (forceFlush + shutdown inside service) await telemetryService.shutdown(); From 149a27c988737981f39c58070a7a70f65db8e0b3 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:19:12 +0530 Subject: [PATCH 06/13] show ctrl+b to move to background --- .../cli/src/tools/runTerminalCommand.ts | 9 ++++++++- extensions/cli/src/ui/TUIChat.tsx | 20 +++++++++++++++++++ .../cli/src/ui/components/ActionStatus.tsx | 6 +++++- extensions/cli/src/util/cli.ts | 10 ++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/extensions/cli/src/tools/runTerminalCommand.ts b/extensions/cli/src/tools/runTerminalCommand.ts index 57d1d0e5291..3826ffcf36d 100644 --- a/extensions/cli/src/tools/runTerminalCommand.ts +++ b/extensions/cli/src/tools/runTerminalCommand.ts @@ -13,6 +13,7 @@ import { isGitCommitCommand, isPullRequestCommand, } from "../telemetry/utils.js"; +import { emitBashToolEnded, emitBashToolStarted } from "../util/cli.js"; import { parseEnvNumber, truncateOutputFromStart, @@ -182,7 +183,9 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed const maxChars = Math.floor(baseMaxChars / parallelCount); const maxLines = Math.floor(baseMaxLines / parallelCount); - return new Promise((resolve, reject) => { + emitBashToolStarted(); + + const terminalOutput: string = await new Promise((resolve, reject) => { // Use same shell logic as core implementation const { shell, args } = getShellCommand(command); const child = spawn(shell, args); @@ -335,5 +338,9 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed reject(`Error: ${error.message}`); }); }); + + emitBashToolEnded(); + + return terminalOutput; }, }; diff --git a/extensions/cli/src/ui/TUIChat.tsx b/extensions/cli/src/ui/TUIChat.tsx index b20d4db122d..f28b20a5bc1 100644 --- a/extensions/cli/src/ui/TUIChat.tsx +++ b/extensions/cli/src/ui/TUIChat.tsx @@ -20,6 +20,7 @@ import { UpdateServiceState, } from "../services/types.js"; import { getTotalSessionCost } from "../session.js"; +import { bashToolEvents } from "../util/cli.js"; import { logger } from "../util/logger.js"; import { ActionStatus } from "./components/ActionStatus.js"; @@ -341,6 +342,22 @@ const TUIChat: React.FC = ({ // Fetch organization name based on auth state const organizationName = useOrganizationName(services.auth?.organizationId); + // Track if a Bash tool is currently running via events + const [isBashToolRunning, setIsBashToolRunning] = useState(false); + + useEffect(() => { + const handleStarted = () => setIsBashToolRunning(true); + const handleEnded = () => setIsBashToolRunning(false); + + bashToolEvents.on("started", handleStarted); + bashToolEvents.on("ended", handleEnded); + + return () => { + bashToolEvents.off("started", handleStarted); + bashToolEvents.off("ended", handleEnded); + }; + }, []); + return ( {/* Chat history - takes up all available space above input */} @@ -379,6 +396,9 @@ const TUIChat: React.FC = ({ startTime={responseStartTime || 0} message="" showSpinner={true} + additionalHint={ + isBashToolRunning ? "ctrl+b to background" : undefined + } /> {/* Compaction Status */} diff --git a/extensions/cli/src/ui/components/ActionStatus.tsx b/extensions/cli/src/ui/components/ActionStatus.tsx index a12b3d6497c..84f17b313bc 100644 --- a/extensions/cli/src/ui/components/ActionStatus.tsx +++ b/extensions/cli/src/ui/components/ActionStatus.tsx @@ -11,6 +11,7 @@ interface ActionStatusProps { showSpinner?: boolean; color?: string; loadingColor?: string; + additionalHint?: string; } const ActionStatus: React.FC = ({ @@ -20,6 +21,7 @@ const ActionStatus: React.FC = ({ showSpinner = false, color = "dim", loadingColor = "green", + additionalHint, }) => { if (!visible) return null; @@ -29,7 +31,9 @@ const ActionStatus: React.FC = ({ {message} ( - • esc to interrupt ) + • esc to interrupt + {additionalHint && • {additionalHint}} + ) ); }; diff --git a/extensions/cli/src/util/cli.ts b/extensions/cli/src/util/cli.ts index b8e359a1d7f..563b9405777 100644 --- a/extensions/cli/src/util/cli.ts +++ b/extensions/cli/src/util/cli.ts @@ -94,3 +94,13 @@ export function hasSuppliedPrompt(): boolean { } export const escapeEvents = new EventEmitter(); + +export const bashToolEvents = new EventEmitter(); + +export function emitBashToolStarted(): void { + bashToolEvents.emit("started"); +} + +export function emitBashToolEnded(): void { + bashToolEvents.emit("ended"); +} From a87d44cf52710d228e4006ed0b00b462559cff0a Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:52:25 +0530 Subject: [PATCH 07/13] show terminal output during streaming --- extensions/cli/src/tools/runTerminalCommand.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/extensions/cli/src/tools/runTerminalCommand.ts b/extensions/cli/src/tools/runTerminalCommand.ts index 3826ffcf36d..d9a2d8fce4f 100644 --- a/extensions/cli/src/tools/runTerminalCommand.ts +++ b/extensions/cli/src/tools/runTerminalCommand.ts @@ -8,6 +8,7 @@ import { import { backgroundJobManager } from "../services/BackgroundJobManager.js"; import { backgroundSignalManager } from "../services/BackgroundSignalManager.js"; +import { services } from "../services/index.js"; import { telemetryService } from "../telemetry/telemetryService.js"; import { isGitCommitCommand, @@ -271,17 +272,33 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed }, TIMEOUT_MS); }; + const showCurrentOutput = () => { + if (!context?.toolCallId) return; + try { + const currentOutput = stdout + (stderr ? `\nStderr: ${stderr}` : ""); + services.chatHistory.addToolResult( + context.toolCallId, + currentOutput, + "calling", + ); + } catch { + // Ignore errors during streaming updates + } + }; + // Start the initial timeout resetTimeout(); child.stdout.on("data", (data) => { stdout += data.toString(); resetTimeout(); + showCurrentOutput(); }); child.stderr.on("data", (data) => { stderr += data.toString(); resetTimeout(); + showCurrentOutput(); }); child.on("close", (code) => { From c1e79cd89c526f22b6355964fc32e5226f0ac5bd Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:55:42 +0530 Subject: [PATCH 08/13] remove unused events in backgroundjobmanager --- extensions/cli/src/services/BackgroundJobManager.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/extensions/cli/src/services/BackgroundJobManager.ts b/extensions/cli/src/services/BackgroundJobManager.ts index 83816362c6c..dddcf722df2 100644 --- a/extensions/cli/src/services/BackgroundJobManager.ts +++ b/extensions/cli/src/services/BackgroundJobManager.ts @@ -1,5 +1,4 @@ import { ChildProcess, spawn } from "child_process"; -import { EventEmitter } from "events"; import { logger } from "../util/logger.js"; @@ -23,7 +22,7 @@ export interface BackgroundJob { const MAX_CONCURRENT_JOBS = 5; -export class BackgroundJobManager extends EventEmitter { +export class BackgroundJobManager { private jobs: Map = new Map(); private processes: Map = new Map(); private jobCounter = 0; @@ -49,7 +48,6 @@ export class BackgroundJobManager extends EventEmitter { }; this.jobs.set(id, job); - this.emit("jobCreated", job); return job; } @@ -61,7 +59,6 @@ export class BackgroundJobManager extends EventEmitter { } job.status = "running"; - this.emit("jobStarted", job); const child = spawn(shell, args, { stdio: "pipe" }); this.processes.set(jobId, child); @@ -111,7 +108,6 @@ export class BackgroundJobManager extends EventEmitter { this.jobs.set(id, job); this.processes.set(id, child); - this.emit("jobCreated", job); child.stdout?.on("data", (data: Buffer) => { this.appendOutput(id, data.toString()); @@ -136,7 +132,6 @@ export class BackgroundJobManager extends EventEmitter { const job = this.jobs.get(jobId); if (job) { job.output += data; - this.emit("jobOutput", job, data); } } @@ -147,7 +142,6 @@ export class BackgroundJobManager extends EventEmitter { job.exitCode = exitCode; job.endTime = new Date(); this.processes.delete(jobId); - this.emit("jobCompleted", job); } } @@ -158,7 +152,6 @@ export class BackgroundJobManager extends EventEmitter { job.error = error; job.endTime = new Date(); this.processes.delete(jobId); - this.emit("jobFailed", job); } } @@ -175,7 +168,6 @@ export class BackgroundJobManager extends EventEmitter { job.status = "cancelled"; job.endTime = new Date(); - this.emit("jobCancelled", job); return true; } @@ -207,7 +199,6 @@ export class BackgroundJobManager extends EventEmitter { } } this.processes.clear(); - this.emit("allJobsKilled"); } reset(): void { From f89de1b7b2efd0cd1bc5dc4ebf9a3c743f7cbe2a Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:00:19 +0530 Subject: [PATCH 09/13] keep background job output upto 1000 lines --- extensions/cli/src/services/BackgroundJobManager.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/extensions/cli/src/services/BackgroundJobManager.ts b/extensions/cli/src/services/BackgroundJobManager.ts index dddcf722df2..637d4d13f6a 100644 --- a/extensions/cli/src/services/BackgroundJobManager.ts +++ b/extensions/cli/src/services/BackgroundJobManager.ts @@ -1,6 +1,7 @@ import { ChildProcess, spawn } from "child_process"; import { logger } from "../util/logger.js"; +import { truncateOutputFromStart } from "../util/truncateOutput.js"; export type BackgroundJobStatus = | "pending" @@ -21,6 +22,8 @@ export interface BackgroundJob { } const MAX_CONCURRENT_JOBS = 5; +const MAX_OUTPUT_CHARS = 50_000; +const MAX_OUTPUT_LINES = 1000; export class BackgroundJobManager { private jobs: Map = new Map(); @@ -132,6 +135,11 @@ export class BackgroundJobManager { const job = this.jobs.get(jobId); if (job) { job.output += data; + const { output } = truncateOutputFromStart(job.output, { + maxChars: MAX_OUTPUT_CHARS, + maxLines: MAX_OUTPUT_LINES, + }); + job.output = output; } } From 5a5a90826da7832057ee1cc663465659ca86b11b Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:10:42 +0530 Subject: [PATCH 10/13] truncate job output to 1000 lines --- extensions/cli/src/services/BackgroundJobManager.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/extensions/cli/src/services/BackgroundJobManager.ts b/extensions/cli/src/services/BackgroundJobManager.ts index 637d4d13f6a..72073774671 100644 --- a/extensions/cli/src/services/BackgroundJobManager.ts +++ b/extensions/cli/src/services/BackgroundJobManager.ts @@ -1,7 +1,6 @@ import { ChildProcess, spawn } from "child_process"; import { logger } from "../util/logger.js"; -import { truncateOutputFromStart } from "../util/truncateOutput.js"; export type BackgroundJobStatus = | "pending" @@ -22,7 +21,6 @@ export interface BackgroundJob { } const MAX_CONCURRENT_JOBS = 5; -const MAX_OUTPUT_CHARS = 50_000; const MAX_OUTPUT_LINES = 1000; export class BackgroundJobManager { @@ -131,15 +129,15 @@ export class BackgroundJobManager { return job; } + // todo: improve write efficiency with ring buffer or similar appendOutput(jobId: string, data: string): void { const job = this.jobs.get(jobId); if (job) { job.output += data; - const { output } = truncateOutputFromStart(job.output, { - maxChars: MAX_OUTPUT_CHARS, - maxLines: MAX_OUTPUT_LINES, - }); - job.output = output; + const lines = job.output.split("\n"); + if (lines.length > MAX_OUTPUT_LINES) { + job.output = lines.slice(-MAX_OUTPUT_LINES).join("\n"); + } } } From c7d6a73df1de2c7830db23e76480e71331f3bbde Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:58:11 +0530 Subject: [PATCH 11/13] show separate screen for background jobs --- .../cli/src/services/BackgroundJobManager.ts | 3 + extensions/cli/src/slashCommands.ts | 29 +-- extensions/cli/src/ui/JobsSelector.tsx | 201 ++++++++++++++++++ extensions/cli/src/ui/TUIChat.tsx | 1 + .../cli/src/ui/components/ScreenContent.tsx | 6 + .../cli/src/ui/context/NavigationContext.tsx | 1 + .../cli/src/ui/hooks/useChat.helpers.ts | 7 + extensions/cli/src/ui/hooks/useChat.ts | 2 + extensions/cli/src/ui/hooks/useChat.types.ts | 2 + 9 files changed, 224 insertions(+), 28 deletions(-) create mode 100644 extensions/cli/src/ui/JobsSelector.tsx diff --git a/extensions/cli/src/services/BackgroundJobManager.ts b/extensions/cli/src/services/BackgroundJobManager.ts index 72073774671..fd0ea7c421f 100644 --- a/extensions/cli/src/services/BackgroundJobManager.ts +++ b/extensions/cli/src/services/BackgroundJobManager.ts @@ -144,6 +144,9 @@ export class BackgroundJobManager { completeJob(jobId: string, exitCode: number): void { const job = this.jobs.get(jobId); if (job) { + if (job.status === "cancelled") { + return; + } job.status = exitCode === 0 ? "completed" : "failed"; job.exitCode = exitCode; job.endTime = new Date(); diff --git a/extensions/cli/src/slashCommands.ts b/extensions/cli/src/slashCommands.ts index d00b82bfb4f..07c659d5939 100644 --- a/extensions/cli/src/slashCommands.ts +++ b/extensions/cli/src/slashCommands.ts @@ -9,7 +9,6 @@ import { import { getAllSlashCommands } from "./commands/commands.js"; import { handleInit } from "./commands/init.js"; import { handleInfoSlashCommand } from "./infoScreen.js"; -import { backgroundJobManager } from "./services/BackgroundJobManager.js"; import { reloadService, SERVICE_NAMES, services } from "./services/index.js"; import { getCurrentSession, updateSessionTitle } from "./session.js"; import { posthogService } from "./telemetry/posthogService.js"; @@ -171,33 +170,7 @@ function handleTitle(args: string[]) { } function handleJobs() { - const allJobs = backgroundJobManager.getAllJobs(); - if (allJobs.length === 0) { - return { - exit: false, - output: chalk.dim("No background jobs"), - }; - } - - const lines = [chalk.bold("Background Jobs:")]; - for (const job of allJobs) { - const statusColor = - job.status === "running" || job.status === "pending" - ? chalk.yellow - : job.status === "completed" - ? chalk.green - : chalk.red; - const duration = job.endTime - ? `${Math.round((job.endTime.getTime() - job.startTime.getTime()) / 1000)}s` - : `${Math.round((Date.now() - job.startTime.getTime()) / 1000)}s`; - lines.push( - ` ${statusColor(job.status.padEnd(10))} ${chalk.cyan(job.id)} ${chalk.dim(`(${duration})`)} ${job.command.substring(0, 40)}${job.command.length > 40 ? "..." : ""}`, - ); - } - return { - exit: false, - output: lines.join("\n"), - }; + return { openJobsSelector: true }; } const commandHandlers: Record = { diff --git a/extensions/cli/src/ui/JobsSelector.tsx b/extensions/cli/src/ui/JobsSelector.tsx new file mode 100644 index 00000000000..c0ddfbca643 --- /dev/null +++ b/extensions/cli/src/ui/JobsSelector.tsx @@ -0,0 +1,201 @@ +import { Box, Text, useInput } from "ink"; +import React, { useEffect, useState } from "react"; + +import { + BackgroundJob, + backgroundJobManager, +} from "../services/BackgroundJobManager.js"; +import { truncateOutputFromStart } from "../util/truncateOutput.js"; + +import { defaultBoxStyles } from "./styles.js"; + +interface JobsSelectorProps { + onCancel: () => void; +} + +type ViewMode = "list" | "detail"; + +function getStatusColor(status: BackgroundJob["status"]): string { + if (status === "running" || status === "pending") return "yellow"; + if (status === "completed") return "green"; + return "red"; +} + +function formatDuration(job: BackgroundJob): string { + const endTime = job.endTime ? job.endTime.getTime() : Date.now(); + return `${Math.round((endTime - job.startTime.getTime()) / 1000)}s`; +} + +export function JobsSelector({ onCancel }: JobsSelectorProps) { + const [selectedIndex, setSelectedIndex] = useState(0); + const [viewMode, setViewMode] = useState("list"); + const [selectedJob, setSelectedJob] = useState(null); + const [jobs, setJobs] = useState(() => backgroundJobManager.getAllJobs()); + + // Refetch the job details every 1 second + useEffect(() => { + const interval = setInterval(() => { + if (viewMode === "detail" && selectedJob) { + const freshJob = backgroundJobManager.getJob(selectedJob.id); + if (freshJob) { + setSelectedJob(freshJob); + } + } else { + setJobs(backgroundJobManager.getAllJobs()); + } + }, 1000); + + return () => clearInterval(interval); + }, []); + + useInput((input, key) => { + // Handle key input when viewing job details + if (viewMode === "detail") { + // Go back to list view + if (key.escape || input === "b") { + setViewMode("list"); + setSelectedJob(null); + return; + } + // Cancel the selected job if it's still active + if (input === "x" && selectedJob) { + const job = backgroundJobManager.getJob(selectedJob.id); + if (job && (job.status === "running" || job.status === "pending")) { + backgroundJobManager.cancelJob(selectedJob.id); + } + return; + } + return; + } + + // Handle key input when viewing job list + if (key.escape || (key.ctrl && input === "c")) { + onCancel(); + return; + } + + // Navigate up and down the job list + if (key.upArrow) { + setSelectedIndex((prev) => (prev > 0 ? prev - 1 : jobs.length - 1)); + return; + } + if (key.downArrow) { + setSelectedIndex((prev) => (prev < jobs.length - 1 ? prev + 1 : 0)); + return; + } + + // Select a job to view its details + if (key.return && jobs[selectedIndex]) { + setSelectedJob(jobs[selectedIndex]); + setViewMode("detail"); + return; + } + }); + + if (jobs.length === 0) { + return ( + + + Background Jobs + + + No background jobs + + Press Esc to go back + + ); + } + + if (viewMode === "detail" && selectedJob) { + const freshJob = selectedJob; + const { output: lastOutput } = truncateOutputFromStart(freshJob.output, { + maxLines: 10, + maxChars: 2000, + }); + + return ( + + + Job Details + + Press Esc/b to go back, x to cancel job + + + + ID: + {freshJob.id} + + + + Status: + {freshJob.status} + + + + Duration: + {formatDuration(freshJob)} + + + + Command: + {freshJob.command} + + + {freshJob.exitCode !== null && ( + + Exit Code: + + {freshJob.exitCode} + + + )} + + {freshJob.error && ( + + Error: + {freshJob.error} + + )} + + + Last 10 lines of output: + + {lastOutput || "(no output)"} + + + ); + } + + // List screen + return ( + + + Background Jobs ({jobs.length}) + + + ↑/↓ to navigate, Enter to view details, Esc to exit + + + + {jobs.map((job, index) => { + const isSelected = index === selectedIndex; + const prefix = isSelected ? "❯ " : " "; + + return ( + + {prefix} + + {job.status.padEnd(10)} + + {job.id} + ({formatDuration(job)}) + + {job.command.substring(0, 30)} + {job.command.length > 30 ? "..." : ""} + + + ); + })} + + ); +} diff --git a/extensions/cli/src/ui/TUIChat.tsx b/extensions/cli/src/ui/TUIChat.tsx index f28b20a5bc1..bedad8b3874 100644 --- a/extensions/cli/src/ui/TUIChat.tsx +++ b/extensions/cli/src/ui/TUIChat.tsx @@ -283,6 +283,7 @@ const TUIChat: React.FC = ({ onShowMCPSelector: () => navigateTo("mcp"), onShowUpdateSelector: () => navigateTo("update"), onShowSessionSelector: () => navigateTo("session"), + onShowJobsSelector: () => navigateTo("jobs"), onReload: handleReload, onClear: handleClear, onRefreshStatic: () => setStaticRefreshTrigger((prev) => prev + 1), diff --git a/extensions/cli/src/ui/components/ScreenContent.tsx b/extensions/cli/src/ui/components/ScreenContent.tsx index 694c8f215c6..aff68f1ceb0 100644 --- a/extensions/cli/src/ui/components/ScreenContent.tsx +++ b/extensions/cli/src/ui/components/ScreenContent.tsx @@ -10,6 +10,7 @@ import { DiffViewer } from "../DiffViewer.js"; import { EditMessageSelector } from "../EditMessageSelector.js"; import { FreeTrialTransitionUI } from "../FreeTrialTransitionUI.js"; import type { ActivePermissionRequest } from "../hooks/useChat.types.js"; +import { JobsSelector } from "../JobsSelector.js"; import { MCPSelector } from "../MCPSelector.js"; import { ModelSelector } from "../ModelSelector.js"; import type { ConfigOption, ModelOption } from "../types/selectorTypes.js"; @@ -156,6 +157,11 @@ export const ScreenContent: React.FC = ({ ); } + // Jobs selector + if (isScreenActive("jobs")) { + return ; + } + // Free trial transition UI if (isScreenActive("free-trial")) { return ; diff --git a/extensions/cli/src/ui/context/NavigationContext.tsx b/extensions/cli/src/ui/context/NavigationContext.tsx index 6d43414dd93..04c1689f50d 100644 --- a/extensions/cli/src/ui/context/NavigationContext.tsx +++ b/extensions/cli/src/ui/context/NavigationContext.tsx @@ -21,6 +21,7 @@ export type NavigationScreen = | "diff" // Full-screen diff overlay | "update" // Update selector | "edit" // Edit message selector + | "jobs" // Background Jobs selector | "session"; // Session selector interface NavigationState { diff --git a/extensions/cli/src/ui/hooks/useChat.helpers.ts b/extensions/cli/src/ui/hooks/useChat.helpers.ts index a453b70668f..6eb49924d7e 100644 --- a/extensions/cli/src/ui/hooks/useChat.helpers.ts +++ b/extensions/cli/src/ui/hooks/useChat.helpers.ts @@ -51,6 +51,7 @@ interface ProcessSlashCommandResultOptions { onShowUpdateSelector?: () => void; onShowMCPSelector?: () => void; onShowSessionSelector?: () => void; + onShowJobsSelector?: () => void; onClear?: () => void; } @@ -66,6 +67,7 @@ export function processSlashCommandResult({ onShowModelSelector, onShowMCPSelector, onShowSessionSelector, + onShowJobsSelector, onClear, }: ProcessSlashCommandResultOptions): string | null { if (result.exit) { @@ -97,6 +99,11 @@ export function processSlashCommandResult({ return null; } + if (result.openJobsSelector && onShowJobsSelector) { + onShowJobsSelector(); + return null; + } + if (result.clear) { const systemMessage = chatHistory.find( (item) => item.message.role === "system", diff --git a/extensions/cli/src/ui/hooks/useChat.ts b/extensions/cli/src/ui/hooks/useChat.ts index fe3dc8d05b8..8147906840e 100644 --- a/extensions/cli/src/ui/hooks/useChat.ts +++ b/extensions/cli/src/ui/hooks/useChat.ts @@ -58,6 +58,7 @@ export function useChat({ onShowUpdateSelector, onShowMCPSelector, onShowSessionSelector, + onShowJobsSelector, onClear, onRefreshStatic, isRemoteMode = false, @@ -426,6 +427,7 @@ export function useChat({ onShowModelSelector, onShowMCPSelector, onShowSessionSelector, + onShowJobsSelector, onShowUpdateSelector, onClear, }); diff --git a/extensions/cli/src/ui/hooks/useChat.types.ts b/extensions/cli/src/ui/hooks/useChat.types.ts index 2fe0b0b81dc..ac2ecc5c657 100644 --- a/extensions/cli/src/ui/hooks/useChat.types.ts +++ b/extensions/cli/src/ui/hooks/useChat.types.ts @@ -18,6 +18,7 @@ export interface UseChatProps { onShowUpdateSelector: () => void; onShowModelSelector?: () => void; onShowSessionSelector?: () => void; + onShowJobsSelector?: () => void; onReload?: () => Promise; onClear?: () => void; onRefreshStatic?: () => void; @@ -63,6 +64,7 @@ export interface SlashCommandResult { openMcpSelector?: boolean; openUpdateSelector?: boolean; openSessionSelector?: boolean; + openJobsSelector?: boolean; compact?: boolean; diffContent?: string; } From 001e5537227541ebf1ea3a70148d26b6f4f46fed Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:08:24 +0530 Subject: [PATCH 12/13] refactor backgroundsignalmanger --- extensions/cli/src/tools/runTerminalCommand.ts | 2 +- extensions/cli/src/ui/hooks/useUserInput.ts | 2 +- .../backgroundSignalManager.ts} | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) rename extensions/cli/src/{services/BackgroundSignalManager.ts => util/backgroundSignalManager.ts} (56%) diff --git a/extensions/cli/src/tools/runTerminalCommand.ts b/extensions/cli/src/tools/runTerminalCommand.ts index d9a2d8fce4f..dcdd1b421bf 100644 --- a/extensions/cli/src/tools/runTerminalCommand.ts +++ b/extensions/cli/src/tools/runTerminalCommand.ts @@ -7,13 +7,13 @@ import { } from "@continuedev/terminal-security"; import { backgroundJobManager } from "../services/BackgroundJobManager.js"; -import { backgroundSignalManager } from "../services/BackgroundSignalManager.js"; import { services } from "../services/index.js"; import { telemetryService } from "../telemetry/telemetryService.js"; import { isGitCommitCommand, isPullRequestCommand, } from "../telemetry/utils.js"; +import { backgroundSignalManager } from "../util/backgroundSignalManager.js"; import { emitBashToolEnded, emitBashToolStarted } from "../util/cli.js"; import { parseEnvNumber, diff --git a/extensions/cli/src/ui/hooks/useUserInput.ts b/extensions/cli/src/ui/hooks/useUserInput.ts index 366522ff15e..229ac44f153 100644 --- a/extensions/cli/src/ui/hooks/useUserInput.ts +++ b/extensions/cli/src/ui/hooks/useUserInput.ts @@ -1,5 +1,5 @@ import type { PermissionMode } from "../../permissions/types.js"; -import { backgroundSignalManager } from "../../services/BackgroundSignalManager.js"; +import { backgroundSignalManager } from "../../util/backgroundSignalManager.js"; import { checkClipboardForImage, getClipboardImage, diff --git a/extensions/cli/src/services/BackgroundSignalManager.ts b/extensions/cli/src/util/backgroundSignalManager.ts similarity index 56% rename from extensions/cli/src/services/BackgroundSignalManager.ts rename to extensions/cli/src/util/backgroundSignalManager.ts index b183ea30ba4..1bfd4233374 100644 --- a/extensions/cli/src/services/BackgroundSignalManager.ts +++ b/extensions/cli/src/util/backgroundSignalManager.ts @@ -1,6 +1,7 @@ import { EventEmitter } from "events"; -export class BackgroundSignalManager extends EventEmitter { +// Event emitter to notify that running terminal command should be moved to background +class BackgroundSignalManager extends EventEmitter { signalBackground(): void { this.emit("backgroundRequested"); } From 1d2cba889ccf77aada9f8cf1770c21450efa6dcd Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:13:05 +0530 Subject: [PATCH 13/13] rename background job manager to background job service --- ...groundJobManager.ts => BackgroundJobService.ts} | 14 ++++++-------- extensions/cli/src/services/index.ts | 3 +-- extensions/cli/src/services/types.ts | 2 +- extensions/cli/src/tools/runTerminalCommand.ts | 8 ++++---- extensions/cli/src/ui/JobsSelector.tsx | 14 +++++++------- extensions/cli/src/util/exit.ts | 7 +++---- 6 files changed, 22 insertions(+), 26 deletions(-) rename extensions/cli/src/services/{BackgroundJobManager.ts => BackgroundJobService.ts} (95%) diff --git a/extensions/cli/src/services/BackgroundJobManager.ts b/extensions/cli/src/services/BackgroundJobService.ts similarity index 95% rename from extensions/cli/src/services/BackgroundJobManager.ts rename to extensions/cli/src/services/BackgroundJobService.ts index fd0ea7c421f..e4350231e04 100644 --- a/extensions/cli/src/services/BackgroundJobManager.ts +++ b/extensions/cli/src/services/BackgroundJobService.ts @@ -23,7 +23,11 @@ export interface BackgroundJob { const MAX_CONCURRENT_JOBS = 5; const MAX_OUTPUT_LINES = 1000; -export class BackgroundJobManager { +/** + * Service for managing background job execution and lifecycle + * Handles spawning, tracking, and cleanup of background processes + */ +export class BackgroundJobService { private jobs: Map = new Map(); private processes: Map = new Map(); private jobCounter = 0; @@ -209,12 +213,6 @@ export class BackgroundJobManager { } this.processes.clear(); } - - reset(): void { - this.killAllJobs(); - this.jobs.clear(); - this.jobCounter = 0; - } } -export const backgroundJobManager = new BackgroundJobManager(); +export const backgroundJobService = new BackgroundJobService(); diff --git a/extensions/cli/src/services/index.ts b/extensions/cli/src/services/index.ts index 6ca30e4a040..778c8625a94 100644 --- a/extensions/cli/src/services/index.ts +++ b/extensions/cli/src/services/index.ts @@ -10,7 +10,6 @@ import { AgentFileService } from "./AgentFileService.js"; import { ApiClientService } from "./ApiClientService.js"; import { ArtifactUploadService } from "./ArtifactUploadService.js"; import { AuthService } from "./AuthService.js"; -import { backgroundJobManager } from "./BackgroundJobManager.js"; import { ChatHistoryService } from "./ChatHistoryService.js"; import { ConfigService } from "./ConfigService.js"; import { FileIndexService } from "./FileIndexService.js"; @@ -388,7 +387,7 @@ export const services = { toolPermissions: toolPermissionService, artifactUpload: artifactUploadService, gitAiIntegration: gitAiIntegrationService, - backgroundJobs: backgroundJobManager, + backgroundJobs: backgroundJobService, } as const; export type ServicesType = typeof services; diff --git a/extensions/cli/src/services/types.ts b/extensions/cli/src/services/types.ts index 9965a8fabec..6011bbd5c34 100644 --- a/extensions/cli/src/services/types.ts +++ b/extensions/cli/src/services/types.ts @@ -133,7 +133,7 @@ export interface ArtifactUploadServiceState { export type { BackgroundJob, BackgroundJobStatus, -} from "./BackgroundJobManager.js"; +} from "./BackgroundJobService.js"; export type { ChatHistoryState } from "./ChatHistoryService.js"; export type { FileIndexServiceState } from "./FileIndexService.js"; export type { GitAiIntegrationServiceState } from "./GitAiIntegrationService.js"; diff --git a/extensions/cli/src/tools/runTerminalCommand.ts b/extensions/cli/src/tools/runTerminalCommand.ts index dcdd1b421bf..b6d8bb10f7f 100644 --- a/extensions/cli/src/tools/runTerminalCommand.ts +++ b/extensions/cli/src/tools/runTerminalCommand.ts @@ -6,7 +6,7 @@ import { type ToolPolicy, } from "@continuedev/terminal-security"; -import { backgroundJobManager } from "../services/BackgroundJobManager.js"; +import { backgroundJobService } from "../services/BackgroundJobService.js"; import { services } from "../services/index.js"; import { telemetryService } from "../telemetry/telemetryService.js"; import { @@ -91,7 +91,7 @@ export function runCommandInBackground(command: string): { jobId?: string; error?: string; } { - const job = backgroundJobManager.createJob(command); + const job = backgroundJobService.createJob(command); if (!job) { return { success: false, @@ -100,7 +100,7 @@ export function runCommandInBackground(command: string): { } const { shell, args } = getShellCommand(command); - const child = backgroundJobManager.startJob(job.id, shell, args); + const child = backgroundJobService.startJob(job.id, shell, args); if (!child) { return { @@ -231,7 +231,7 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed } backgroundSignalManager.off("backgroundRequested", moveToBackground); - const job = backgroundJobManager.createJobWithProcess( + const job = backgroundJobService.createJobWithProcess( command, child as ChildProcess, stdout, diff --git a/extensions/cli/src/ui/JobsSelector.tsx b/extensions/cli/src/ui/JobsSelector.tsx index c0ddfbca643..f39136dde8f 100644 --- a/extensions/cli/src/ui/JobsSelector.tsx +++ b/extensions/cli/src/ui/JobsSelector.tsx @@ -3,8 +3,8 @@ import React, { useEffect, useState } from "react"; import { BackgroundJob, - backgroundJobManager, -} from "../services/BackgroundJobManager.js"; + backgroundJobService, +} from "../services/BackgroundJobService.js"; import { truncateOutputFromStart } from "../util/truncateOutput.js"; import { defaultBoxStyles } from "./styles.js"; @@ -30,18 +30,18 @@ export function JobsSelector({ onCancel }: JobsSelectorProps) { const [selectedIndex, setSelectedIndex] = useState(0); const [viewMode, setViewMode] = useState("list"); const [selectedJob, setSelectedJob] = useState(null); - const [jobs, setJobs] = useState(() => backgroundJobManager.getAllJobs()); + const [jobs, setJobs] = useState(() => backgroundJobService.getAllJobs()); // Refetch the job details every 1 second useEffect(() => { const interval = setInterval(() => { if (viewMode === "detail" && selectedJob) { - const freshJob = backgroundJobManager.getJob(selectedJob.id); + const freshJob = backgroundJobService.getJob(selectedJob.id); if (freshJob) { setSelectedJob(freshJob); } } else { - setJobs(backgroundJobManager.getAllJobs()); + setJobs(backgroundJobService.getAllJobs()); } }, 1000); @@ -59,9 +59,9 @@ export function JobsSelector({ onCancel }: JobsSelectorProps) { } // Cancel the selected job if it's still active if (input === "x" && selectedJob) { - const job = backgroundJobManager.getJob(selectedJob.id); + const job = backgroundJobService.getJob(selectedJob.id); if (job && (job.status === "running" || job.status === "pending")) { - backgroundJobManager.cancelJob(selectedJob.id); + backgroundJobService.cancelJob(selectedJob.id); } return; } diff --git a/extensions/cli/src/util/exit.ts b/extensions/cli/src/util/exit.ts index 3642669974e..17012d8228e 100644 --- a/extensions/cli/src/util/exit.ts +++ b/extensions/cli/src/util/exit.ts @@ -1,7 +1,7 @@ import type { ChatHistoryItem } from "core/index.js"; import { sentryService } from "../sentry.js"; -import { backgroundJobManager } from "../services/BackgroundJobManager.js"; +import { backgroundJobService } from "../services/BackgroundJobService.js"; import { getSessionUsage } from "../session.js"; import { telemetryService } from "../telemetry/telemetryService.js"; @@ -196,11 +196,10 @@ export async function gracefulExit(code: number = 0): Promise { displaySessionUsage(); try { - // todo: rename backgroundJobManager to backgroundCommandService - const runningJobs = backgroundJobManager.getRunningJobCount(); + const runningJobs = backgroundJobService.getRunningJobCount(); if (runningJobs > 0) { logger.debug(`Killing ${runningJobs} background job(s) on exit`); - backgroundJobManager.killAllJobs(); + backgroundJobService.killAllJobs(); } } catch (err) { logger.debug("Background job cleanup error (ignored)", err as any);