diff --git a/.opencode/package.json b/.opencode/package.json index 31a74b2..1ed6b82 100644 --- a/.opencode/package.json +++ b/.opencode/package.json @@ -12,4 +12,4 @@ "@types/bun": "^1.3.10", "@types/node": "^25.5.0" } -} \ No newline at end of file +} diff --git a/bun.lock b/bun.lock index ba2a9f4..553039e 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@opencode-ai/plugin": "^1.2.27", "chalk": "^5.3.0", "commander": "^12.0.0", + "ora": "^9.3.0", }, "devDependencies": { "@biomejs/biome": "^2.4.8", @@ -44,16 +45,48 @@ "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "ora": ["ora@9.3.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.1", "string-width": "^8.1.0" } }, "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "stdin-discarder": ["stdin-discarder@0.3.1", "", {}, "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA=="], + + "string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], } } diff --git a/package.json b/package.json index 1cab7b5..0de50e0 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,10 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "chalk": "^5.3.0", "@opencode-ai/plugin": "^1.2.27", - "commander": "^12.0.0" + "chalk": "^5.3.0", + "commander": "^12.0.0", + "ora": "^9.3.0" }, "devDependencies": { "@biomejs/biome": "^2.4.8", diff --git a/src/core/cleanup.ts b/src/core/cleanup.ts index 3099c90..d63f933 100644 --- a/src/core/cleanup.ts +++ b/src/core/cleanup.ts @@ -38,8 +38,14 @@ export async function getAvailableRemotes(): Promise { /** * Deletes a local git tag. */ -async function deleteLocalTag(tagName: string): Promise { - const result = await runCommand(["git", "tag", "-d", tagName]); +async function deleteLocalTag( + tagName: string, + mode?: "cli" | "tool", +): Promise { + const result = await runCommand(["git", "tag", "-d", tagName], { + mode, + spinnerMsg: `Deleting local tag ${tagName}...`, + }); return result.ok; } @@ -49,14 +55,15 @@ async function deleteLocalTag(tagName: string): Promise { async function deleteRemoteTag( tagName: string, remoteName: string, + mode?: "cli" | "tool", ): Promise<{ ok: boolean; error: string }> { - const result = await runCommand([ - "git", - "push", - remoteName, - "--delete", - tagName, - ]); + const result = await runCommand( + ["git", "push", remoteName, "--delete", tagName], + { + mode, + spinnerMsg: `Deleting remote tag ${tagName} from ${remoteName}...`, + }, + ); return { ok: result.ok, error: result.ok ? "" : result.stderr, @@ -165,7 +172,7 @@ export async function cleanupMultiModel( // Delete local tags for (const tag of archiveTags) { - const deleted = await deleteLocalTag(tag); + const deleted = await deleteLocalTag(tag, mode); if (deleted) { localTagNames.push(tag); } @@ -174,7 +181,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); + const result = await deleteRemoteTag(tag, remoteToDeleteFrom, mode); if (result.ok) { remoteTagNames.push(tag); } else { diff --git a/src/core/close.ts b/src/core/close.ts index b38f88e..491fb85 100644 --- a/src/core/close.ts +++ b/src/core/close.ts @@ -113,19 +113,20 @@ export async function closeMultiModel( for (const worktree of worktrees) { try { // Remove worktree - await runCommand(["git", "worktree", "remove", "-f", worktree.path]); + await runCommand(["git", "worktree", "remove", "-f", worktree.path], { + mode, + spinnerMsg: `Removing worktree ${worktree.path}...`, + }); worktreesRemoved.push(worktree.path); // Optionally create an archive tag before deleting the branch for safe recovery if (options.createArchiveTags !== false) { const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const archiveTag = `archive/${worktree.branch}-${timestamp}`; - const tagResult = await runCommand([ - "git", - "tag", - archiveTag, - worktree.branch, - ]); + const tagResult = await runCommand( + ["git", "tag", archiveTag, worktree.branch], + { mode, spinnerMsg: `Creating archive tag ${archiveTag}...` }, + ); if (tagResult.ok) { tagsCreated.push(archiveTag); @@ -137,7 +138,10 @@ export async function closeMultiModel( } } - await runCommand(["git", "branch", "-D", worktree.branch]); + await runCommand(["git", "branch", "-D", worktree.branch], { + mode, + spinnerMsg: `Deleting branch ${worktree.branch}...`, + }); branchesDeleted.push(worktree.branch); } catch (err) { instructionsArr.push( diff --git a/src/core/open.ts b/src/core/open.ts index 74baf2a..482c53e 100644 --- a/src/core/open.ts +++ b/src/core/open.ts @@ -99,7 +99,10 @@ export async function openMultiModel( }; } - const modelListResult = await runCommand([binaryName, "models"]); + const modelListResult = await runCommand([binaryName, "models"], { + mode, + spinnerMsg: `Fetching available models using \`${binaryName} models\`...`, + }); if (!modelListResult.ok) { return { success: false, @@ -184,14 +187,10 @@ export async function openMultiModel( } } - const worktreeResult = await runCommand([ - "git", - "worktree", - "add", - "-b", - branchName, - worktreePath, - ]); + const worktreeResult = await runCommand( + ["git", "worktree", "add", "-b", branchName, worktreePath], + { mode, spinnerMsg: `Creating worktree for ${plan.model}...` }, + ); if (!worktreeResult.ok) { if (!isFirst) { failedModels.push( @@ -208,17 +207,20 @@ export async function openMultiModel( } if (isFirst) { - const sessionCreateResult = await runCommand([ - "tmux", - "new-session", - "-d", - "-s", - sessionName, - "-n", - plan.windowName, - "-c", - worktreePath, - ]); + const sessionCreateResult = await runCommand( + [ + "tmux", + "new-session", + "-d", + "-s", + sessionName, + "-n", + plan.windowName, + "-c", + worktreePath, + ], + { mode, spinnerMsg: `Starting tmux session '${sessionName}'...` }, + ); if (!sessionCreateResult.ok) { const undoErrors = await undoWorktree(worktreePath, branchName); @@ -229,17 +231,20 @@ export async function openMultiModel( return { success: false, sessionName, error: errorMsg }; } } else { - const windowCreateResult = await runCommand([ - "tmux", - "new-window", - "-d", - "-t", - sessionName, - "-n", - plan.windowName, - "-c", - worktreePath, - ]); + const windowCreateResult = await runCommand( + [ + "tmux", + "new-window", + "-d", + "-t", + sessionName, + "-n", + plan.windowName, + "-c", + worktreePath, + ], + { mode, spinnerMsg: `Creating tmux window '${plan.windowName}'...` }, + ); if (!windowCreateResult.ok) { failedModels.push( @@ -259,6 +264,7 @@ export async function openMultiModel( sessionName, plan, binaryName, + mode, ); if (launchResult.ok) { succeededModels.push(plan.model); diff --git a/src/core/utils.ts b/src/core/utils.ts index dc3bb9e..7096eaf 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -1,6 +1,12 @@ import * as os from "node:os"; import * as path from "node:path"; -import type { CommandResult, WindowLaunchPlan, WorktreeInfo } from "../types"; +import ora, { type Ora } from "ora"; +import type { + CommandResult, + RunCommandOptions, + WindowLaunchPlan, + WorktreeInfo, +} from "../types"; const MAX_SUGGESTIONS = 3; const WINDOW_NAME_LIMIT = 24; @@ -9,6 +15,7 @@ const WINDOW_NAME_LIMIT = 24; * Runs a command and captures its output without throwing on non-zero exit codes. * * @param parts Command segments to pass to the shell. + * @param options Additional options for running the command (e.g., mode, spinnerMsg). * @returns The exit status plus captured stdout and stderr. * * @example @@ -19,9 +26,28 @@ const WINDOW_NAME_LIMIT = 24; * } * ``` */ -export async function runCommand(parts: string[]): Promise { +export async function runCommand( + parts: string[], + options?: RunCommandOptions, +): Promise { + let spinner: Ora | undefined; + const isTest = + process.env.NODE_ENV === "test" || process.env.BUN_ENV === "test"; + if (options?.mode === "cli" && !isTest) { + const msg = options.spinnerMsg || `Running ${parts[0]}...`; + spinner = ora(msg).start(); + } + const result = await Bun.$`${parts}`.quiet().nothrow(); + if (spinner) { + if (result.exitCode === 0) { + spinner.succeed(); + } else { + spinner.fail(); + } + } + return { ok: result.exitCode === 0, stdout: result.stdout.toString().trim(), @@ -274,17 +300,21 @@ export async function launchModelInWindow( sessionName: string, plan: WindowLaunchPlan, binaryName: string = "opencode", + mode?: "cli" | "tool", ): Promise { const launchCommand = `${binaryName} --model ${shellQuote(plan.model)}`; - return runCommand([ - "tmux", - "send-keys", - "-t", - `${sessionName}:${plan.windowName}`, - launchCommand, - "C-m", - ]); + return runCommand( + [ + "tmux", + "send-keys", + "-t", + `${sessionName}:${plan.windowName}`, + launchCommand, + "C-m", + ], + { mode, spinnerMsg: `Launching model ${plan.model}...` }, + ); } /** diff --git a/src/types.ts b/src/types.ts index 8fb9e5d..eefc576 100644 --- a/src/types.ts +++ b/src/types.ts @@ -129,6 +129,16 @@ export interface CommandResult { stderr: string; } +/** + * Options for running a shell command. + */ +export interface RunCommandOptions { + /** Caller context: 'cli' shows spinner, 'tool' does not. */ + mode?: "cli" | "tool"; + /** Message to display in the spinner while the command runs. */ + spinnerMsg?: string; +} + /** * Information about a git worktree. */ diff --git a/tests/cleanup.test.ts b/tests/cleanup.test.ts index 7b23b07..06da7b9 100644 --- a/tests/cleanup.test.ts +++ b/tests/cleanup.test.ts @@ -70,7 +70,7 @@ describe("cleanupMultiModel", () => { stderr: "", }; - const result = await cleanupMultiModel({ force: true }); + const result = await cleanupMultiModel({ mode: "cli", force: true }); expect(result.success).toBe(true); expect(result.localTagsDeleted).toBe(0); @@ -96,7 +96,7 @@ describe("cleanupMultiModel", () => { } as any; spyOn(readline, "createInterface").mockImplementation(() => mockInterface); - const result = await cleanupMultiModel({ force: false }); + const result = await cleanupMultiModel({ mode: "cli", force: false }); expect(result.success).toBe(false); expect(result.error).toBe("Cleanup cancelled by user"); @@ -126,7 +126,7 @@ describe("cleanupMultiModel", () => { stderr: "", }; - const result = await cleanupMultiModel({ force: true }); + const result = await cleanupMultiModel({ mode: "cli", force: true }); expect(result.success).toBe(true); expect(result.localTagsDeleted).toBe(1); @@ -150,7 +150,11 @@ describe("cleanupMultiModel", () => { stderr: "", }; - const result = await cleanupMultiModel({ force: true, remote: false }); + const result = await cleanupMultiModel({ + mode: "cli", + force: true, + remote: false, + }); expect(result.success).toBe(true); expect(result.localTagsDeleted).toBe(1); @@ -191,7 +195,7 @@ describe("cleanupMultiModel", () => { stderr: "", }; - const result = await cleanupMultiModel({ force: true }); + const result = await cleanupMultiModel({ mode: "cli", force: true }); expect(result.success).toBe(true); expect(result.localTagsDeleted).toBe(2); @@ -222,7 +226,11 @@ describe("cleanupMultiModel", () => { stderr: "", }; - const result = await cleanupMultiModel({ force: true, remote: undefined }); + const result = await cleanupMultiModel({ + mode: "cli", + force: true, + remote: undefined, + }); expect(result.success).toBe(true); expect(executedCommands).toContain( @@ -253,6 +261,7 @@ describe("cleanupMultiModel", () => { }; const result = await cleanupMultiModel({ + mode: "cli", force: true, remote: "upstream", }); @@ -277,6 +286,7 @@ describe("cleanupMultiModel", () => { }; const result = await cleanupMultiModel({ + mode: "cli", force: true, remote: "nonexistent", }); @@ -303,7 +313,7 @@ describe("cleanupMultiModel", () => { stderr: "", }; - const result = await cleanupMultiModel({ force: true }); + const result = await cleanupMultiModel({ mode: "cli", force: true }); expect(result.success).toBe(true); expect(result.availableRemotes).toEqual([]); @@ -339,7 +349,7 @@ describe("cleanupMultiModel", () => { } as any; spyOn(readline, "createInterface").mockImplementation(() => mockInterface); - const result = await cleanupMultiModel({ force: false }); + const result = await cleanupMultiModel({ mode: "cli", force: false }); expect(result.success).toBe(true); expect(result.localTagsDeleted).toBe(1); @@ -392,7 +402,7 @@ describe("cleanupMultiModel", () => { stderr: "", }; - const result = await cleanupMultiModel({ force: true }); + const result = await cleanupMultiModel({ mode: "cli", force: true }); expect(result.success).toBe(true); expect(result.localTagsDeleted).toBe(3); diff --git a/tests/close.test.ts b/tests/close.test.ts index 0e1c251..1bed6ba 100644 --- a/tests/close.test.ts +++ b/tests/close.test.ts @@ -79,6 +79,7 @@ describe("closeMultiModel", () => { }; const result = await closeMultiModel({ + mode: "cli", sessionName: "test-session", cleanupWorktrees: false, force: false, @@ -103,6 +104,7 @@ describe("closeMultiModel", () => { }; const result = await closeMultiModel({ + mode: "cli", sessionName: "test-session", cleanupWorktrees: false, force: false, @@ -161,6 +163,7 @@ describe("closeMultiModel", () => { } as any; const result = await closeMultiModel({ + mode: "cli", sessionName: "test-session", cleanupWorktrees: true, force: true, @@ -206,6 +209,7 @@ describe("closeMultiModel", () => { spyOn(readline, "createInterface").mockImplementation(() => mockInterface); const result = await closeMultiModel({ + mode: "cli", sessionName: "test-session", cleanupWorktrees: true, force: false, @@ -251,6 +255,7 @@ describe("closeMultiModel", () => { } as any; const result = await closeMultiModel({ + mode: "cli", sessionName: "test-session", cleanupWorktrees: true, force: false, @@ -322,6 +327,7 @@ describe("closeMultiModel", () => { }; const result = await closeMultiModel({ + mode: "cli", sessionName: "test-session", cleanupWorktrees: true, force: true, @@ -380,6 +386,7 @@ describe("closeMultiModel", () => { } as any; const result = await closeMultiModel({ + mode: "cli", sessionName: unsanitized, cleanupWorktrees: true, force: true, @@ -422,6 +429,7 @@ describe("closeMultiModel", () => { }; const result = await closeMultiModel({ + mode: "cli", sessionName: unsanitized, cleanupWorktrees: false, force: false, @@ -465,6 +473,7 @@ describe("closeMultiModel", () => { ] = { ok: true, stdout: "", stderr: "" }; const result = await closeMultiModel({ + mode: "cli", sessionName: "test-session", cleanupWorktrees: true, force: true, diff --git a/tests/close_error.test.ts b/tests/close_error.test.ts index 2ec7de3..c5a7e2b 100644 --- a/tests/close_error.test.ts +++ b/tests/close_error.test.ts @@ -62,6 +62,7 @@ describe("closeMultiModel unexpected error handling", () => { test("catches thrown error and returns failure", async () => { const result = await closeMultiModel({ + mode: "cli", sessionName: "any-session", cleanupWorktrees: false, force: false, diff --git a/tests/open.test.ts b/tests/open.test.ts index 38c209a..8f2d332 100644 --- a/tests/open.test.ts +++ b/tests/open.test.ts @@ -94,6 +94,7 @@ describe("multi-model launch", () => { test("fails if session name is empty", async () => { const result = await openMultiModel({ + mode: "cli", sessionName: "", models: ["openai/gpt-5.2"], }); @@ -105,6 +106,7 @@ describe("multi-model launch", () => { test("fails if no models are provided", async () => { const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: [], }); @@ -116,6 +118,7 @@ describe("multi-model launch", () => { test("fails if duplicate models are provided", async () => { const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2", "openai/gpt-5.2"], }); @@ -133,6 +136,7 @@ describe("multi-model launch", () => { }; const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); @@ -152,6 +156,7 @@ describe("multi-model launch", () => { }; const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); @@ -174,6 +179,7 @@ describe("multi-model launch", () => { }; const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); @@ -191,6 +197,7 @@ describe("multi-model launch", () => { test("fails if model name is not in allowlist", async () => { const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["invalid/model"], }); @@ -215,6 +222,7 @@ describe("multi-model launch", () => { }; const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); @@ -232,6 +240,7 @@ describe("multi-model launch", () => { test("successfully plans and executes tmux launch with single model", async () => { const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); @@ -258,6 +267,7 @@ describe("multi-model launch", () => { ] = { ok: false, stdout: "", stderr: "" }; const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2", "anthropic/claude-3-5-sonnet"], }); @@ -289,6 +299,7 @@ describe("multi-model launch", () => { ] = { ok: false, stdout: "", stderr: "" }; const result = await openMultiModel({ + mode: "cli", sessionName: "test session!", models: ["openai/gpt-5.2"], }); @@ -326,6 +337,7 @@ describe("multi-model launch", () => { ] = { ok: true, stdout: "", stderr: "" }; const result = await openMultiModel({ + mode: "cli", sessionName: unsanitized, models: ["openai/gpt-5.2"], }); @@ -373,6 +385,7 @@ describe("multi-model launch", () => { }; const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); @@ -399,6 +412,7 @@ describe("multi-model launch", () => { }); const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); @@ -420,6 +434,7 @@ describe("multi-model launch", () => { ] = { ok: true, stdout: "", stderr: "" }; const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); @@ -444,6 +459,7 @@ describe("multi-model launch", () => { ] = { ok: false, stdout: "", stderr: "git error" }; const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); @@ -475,6 +491,7 @@ describe("multi-model launch", () => { ] = { ok: false, stdout: "", stderr: "" }; const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2", "anthropic/gpt-5.2"], }); @@ -507,6 +524,7 @@ describe("multi-model launch", () => { ] = { ok: false, stdout: "", stderr: "" }; const result = await openMultiModel({ + mode: "cli", sessionName: "test-session", models: [longModel], });