From ac31d69e3a3ea0562249e0e62dacadb1dd78e893 Mon Sep 17 00:00:00 2001 From: bendtherules Date: Mon, 23 Mar 2026 19:16:12 +0530 Subject: [PATCH] feat: add abortSignal to all commands - Extend `listArchiveTags`, `getAvailableRemotes`, `deleteLocalTag`, `deleteRemoteTag`, and `cleanupMultiModel` to accept `mode` and `abortSignal` for better control and cancellation. - Update internal calls to pass the new parameters throughout the cleanup workflow. - Add detailed JSDoc comments for the new arguments. - Modify `closeMultiModel` to destructure `mode` and `abortSignal` from options and forward them to `runCommand` calls, including spinner messages for session checks, killing sessions, and worktree removal. --- src/core/cleanup.ts | 38 +++++++++++++++++++++++++++++--------- src/core/close.ts | 10 ++++++---- src/core/open.ts | 40 +++++++++++++++++++++++++++++++--------- src/core/utils.ts | 1 + src/tools/open.ts | 9 +-------- src/types.ts | 8 ++++++++ 6 files changed, 76 insertions(+), 30 deletions(-) diff --git a/src/core/cleanup.ts b/src/core/cleanup.ts index 85a4665..5985ed7 100644 --- a/src/core/cleanup.ts +++ b/src/core/cleanup.ts @@ -5,16 +5,32 @@ import { promptUser, runCommand } from "./utils"; /** * Lists all archive tags matching the pattern archive/*. */ -export async function listArchiveTags(): Promise { - const { stdout } = await runCommand(["git", "tag", "-l", "archive/*"]); +export async function listArchiveTags( + mode?: "cli" | "tool", + abortSignal?: AbortSignal, +): Promise { + const { stdout } = await runCommand(["git", "tag", "-l", "archive/*"], { + mode, + abortSignal, + }); return stdout.split("\n").filter(Boolean); } /** * Gets the list of available git remotes. */ -export async function getAvailableRemotes(): Promise { - const { stdout } = await runCommand(["git", "remote"]); +/** + * Retrieves the list of git remotes. + * Accepts optional mode and abort signal for cancellation support. + */ +export async function getAvailableRemotes( + mode?: "cli" | "tool", + abortSignal?: AbortSignal, +): Promise { + const { stdout } = await runCommand(["git", "remote"], { + mode, + abortSignal, + }); return stdout.split("\n").filter(Boolean); } @@ -24,10 +40,12 @@ export async function getAvailableRemotes(): Promise { async function deleteLocalTag( tagName: string, mode?: "cli" | "tool", + abortSignal?: AbortSignal, ): Promise { const result = await runCommand(["git", "tag", "-d", tagName], { mode, spinnerMsg: `Deleting local tag ${tagName}...`, + abortSignal, }); return result.ok; } @@ -39,12 +57,14 @@ async function deleteRemoteTag( tagName: string, remoteName: string, mode?: "cli" | "tool", + abortSignal?: AbortSignal, ): Promise<{ ok: boolean; error: string }> { const result = await runCommand( ["git", "push", remoteName, "--delete", tagName], { mode, spinnerMsg: `Deleting remote tag ${tagName} from ${remoteName}...`, + abortSignal, }, ); return { @@ -65,11 +85,11 @@ async function deleteRemoteTag( export async function cleanupMultiModel( options: CleanupOptions, ): Promise { - const { force = false, remote, mode } = options; + const { force = false, remote, mode, abortSignal } = options; // Get available remotes and archive tags first - const availableRemotes = await getAvailableRemotes(); - const archiveTags = await listArchiveTags(); + const availableRemotes = await getAvailableRemotes(mode, abortSignal); + const archiveTags = await listArchiveTags(mode, abortSignal); // Determine whether to push to remote and which remote to use: // - remote === false: --no-remote flag, local-only mode @@ -155,7 +175,7 @@ export async function cleanupMultiModel( // Delete local tags for (const tag of archiveTags) { - const deleted = await deleteLocalTag(tag, mode); + const deleted = await deleteLocalTag(tag, mode, abortSignal); if (deleted) { localTagNames.push(tag); } @@ -164,7 +184,7 @@ export async function cleanupMultiModel( // Delete from remote (if remote is specified and exists) if (shouldPushToRemote && remoteToDeleteFrom) { for (const tag of localTagNames) { - const result = await deleteRemoteTag(tag, remoteToDeleteFrom, mode); + const result = await deleteRemoteTag(tag, remoteToDeleteFrom, mode, abortSignal); if (result.ok) { remoteTagNames.push(tag); } else { diff --git a/src/core/close.ts b/src/core/close.ts index e9fc967..c9d834a 100644 --- a/src/core/close.ts +++ b/src/core/close.ts @@ -36,7 +36,7 @@ export async function closeMultiModel( const instructionsArr: string[] = []; const branchesDeleted: string[] = []; const tagsCreated: string[] = []; - const mode = options.mode; + const {mode , abortSignal} = options; const safeSessionName = sanitizeName(options.sessionName); try { @@ -46,11 +46,11 @@ export async function closeMultiModel( "has-session", "-t", options.sessionName, - ]); + ], { mode, spinnerMsg: `Checking if session '${options.sessionName}' exists...`, abortSignal }); // Kill tmux session if it exists if (sessionExists) { - await runCommand(["tmux", "kill-session", "-t", options.sessionName]); + await runCommand(["tmux", "kill-session", "-t", options.sessionName], { mode, spinnerMsg: `Killing session '${options.sessionName}'...`, abortSignal }); instructionsArr.push( chalk.green(`Closed session '${options.sessionName}'`), ); @@ -96,6 +96,7 @@ export async function closeMultiModel( await runCommand(["git", "worktree", "remove", "-f", worktree.path], { mode, spinnerMsg: `Removing worktree ${worktree.path}...`, + abortSignal, }); worktreesRemoved.push(worktree.path); @@ -121,6 +122,7 @@ export async function closeMultiModel( await runCommand(["git", "branch", "-D", worktree.branch], { mode, spinnerMsg: `Deleting branch ${worktree.branch}...`, + abortSignal, }); branchesDeleted.push(worktree.branch); } catch (err) { @@ -133,7 +135,7 @@ export async function closeMultiModel( // Also remove the base directory const worktreeBase = getSessionPath(safeSessionName); try { - await runCommand(["rm", "-rf", worktreeBase]); + await runCommand(["rm", "-rf", worktreeBase], { mode, spinnerMsg: `Removing worktree base directory: ${worktreeBase}...`, abortSignal }); } catch { // Ignore errors } diff --git a/src/core/open.ts b/src/core/open.ts index ddd28a9..5b67ac4 100644 --- a/src/core/open.ts +++ b/src/core/open.ts @@ -42,7 +42,7 @@ export async function openMultiModel( const safeSessionName = sanitizeName(sessionName); const models = normalizeModels(options.models); const binaryName = options.binaryName || "opencode"; - const mode = options.mode; + const {mode, abortSignal} = options; if (!sessionName) { return { @@ -73,7 +73,11 @@ export async function openMultiModel( "git", "rev-parse", "--is-inside-work-tree", - ]); + ], { + mode, + spinnerMsg: "Checking if inside git repository...", + abortSignal, + }); if (!gitRepoCheck.ok) { return { success: false, @@ -82,7 +86,11 @@ export async function openMultiModel( }; } - const tmuxExists = await runCommand(["command", "-v", "tmux"]); + const tmuxExists = await runCommand(["command", "-v", "tmux"], { + mode, + spinnerMsg: "Checking if tmux is installed...", + abortSignal, + }); if (!tmuxExists.ok) { return { success: false, @@ -91,7 +99,12 @@ export async function openMultiModel( }; } - const binaryExists = await runCommand(["command", "-v", binaryName]); + + const binaryExists = await runCommand(["command", "-v", binaryName], { + mode, + spinnerMsg: `Checking if ${binaryName} is installed...`, + abortSignal, + }); if (!binaryExists.ok) { return { success: false, @@ -103,6 +116,7 @@ export async function openMultiModel( const modelListResult = await runCommand([binaryName, "models"], { mode, spinnerMsg: `Fetching available models using \`${binaryName} models\`...`, + abortSignal, }); if (!modelListResult.ok) { return { @@ -131,7 +145,11 @@ export async function openMultiModel( "has-session", "-t", sessionName, - ]); + ], { + mode, + spinnerMsg: `Checking if tmux session '${sessionName}' already exists...`, + abortSignal, + }); if (sessionExists.ok) { return { success: false, @@ -172,7 +190,11 @@ export async function openMultiModel( "--verify", "--quiet", `refs/heads/${branchName}`, - ]); + ], { + mode, + spinnerMsg: `Checking if branch '${branchName}' already exists...`, + abortSignal, + }); if (branchExists.ok) { if (!isFirst) { failedModels.push( @@ -190,7 +212,7 @@ export async function openMultiModel( const worktreeResult = await runCommand( ["git", "worktree", "add", "-b", branchName, worktreePath], - { mode, spinnerMsg: `Creating worktree for ${plan.model}...` }, + { mode, spinnerMsg: `Creating worktree for ${plan.model}...`, abortSignal }, ); if (!worktreeResult.ok) { if (!isFirst) { @@ -220,7 +242,7 @@ export async function openMultiModel( "-c", worktreePath, ], - { mode, spinnerMsg: `Starting tmux session '${sessionName}'...` }, + { mode, spinnerMsg: `Starting tmux session '${sessionName}'...`, abortSignal }, ); if (!sessionCreateResult.ok) { @@ -244,7 +266,7 @@ export async function openMultiModel( "-c", worktreePath, ], - { mode, spinnerMsg: `Creating tmux window '${plan.windowName}'...` }, + { mode, spinnerMsg: `Creating tmux window '${plan.windowName}'...`, abortSignal }, ); if (!windowCreateResult.ok) { diff --git a/src/core/utils.ts b/src/core/utils.ts index 48a2780..1fd1c7d 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -51,6 +51,7 @@ export async function runCommand( spinner = sharedSpinner; } + options?.abortSignal?.throwIfAborted(); const result = await Bun.$`${parts}`.quiet().nothrow(); if (spinner) { diff --git a/src/tools/open.ts b/src/tools/open.ts index 0952c96..7df357f 100644 --- a/src/tools/open.ts +++ b/src/tools/open.ts @@ -32,14 +32,7 @@ Arguments: models: args.models, binaryName, mode: "tool", - }); - - context.metadata({ - title: `multi-model ${args.sessionName}`, - metadata: { - safeSessionName: result.sessionName, - modelCount: args.models.length, - }, + abortSignal: context.abort, }); if (!result.success) { diff --git a/src/types.ts b/src/types.ts index 78644c6..9f6c06f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,8 @@ export interface OpenSessionOptions { mode: "cli" | "tool"; /** Save models to settings file on successful launch (CLI only). */ saveToSettings?: boolean; + /** Signal to abort the operation. */ + abortSignal?: AbortSignal; } /** @@ -51,6 +53,8 @@ export interface CloseSessionOptions { force?: boolean; /** Caller context: 'cli' shows shell commands, 'tool' shows tool instructions. */ mode: "cli" | "tool"; + /** Signal to abort the operation. */ + abortSignal?: AbortSignal; } /** @@ -100,6 +104,8 @@ export interface CleanupOptions { remote?: string | false; /** Caller context: 'cli' shows shell commands, 'tool' shows tool instructions. */ mode: "cli" | "tool"; + /** Optional abort signal to cancel the operation. */ + abortSignal?: AbortSignal; } /** @@ -146,6 +152,8 @@ export interface RunCommandOptions { mode?: "cli" | "tool"; /** Message to display in the spinner while the command runs. */ spinnerMsg?: string; + /** Signal to abort the operation. Check it before running the command. */ + abortSignal?: AbortSignal; } /**