diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 index 853408f..f3ca6e5 100644 --- a/scripts/setup.ps1 +++ b/scripts/setup.ps1 @@ -1,6 +1,7 @@ param( [switch]$SkipInstall, [switch]$SkipBuild, + [switch]$SkipBun, [switch]$ForceConfig, [switch]$Uninstall, [switch]$Overseas, @@ -104,6 +105,97 @@ function Find-Chrome { return $null } +function Resolve-BunBin { + if ($env:STEP_BUN_BIN -and (Test-Path $env:STEP_BUN_BIN)) { + return $env:STEP_BUN_BIN + } + $cmd = Get-Command bun -ErrorAction SilentlyContinue + if ($cmd) { + return $cmd.Source + } + return $null +} + +function Install-Bun { + <# + .SYNOPSIS + Ensures Bun is available on PATH. Strategy: + 1. Already on PATH -> use it + 2. winget -> install Oven-sh.Bun + 3. Direct download -> github.com/oven-sh/bun/releases/latest + Returns the path to bun.exe, or $null if all attempts failed. + #> + $existing = Resolve-BunBin + if ($existing) { + Write-Ok "Bun already available: $existing" + return $existing + } + + Write-Section "[bun] Installing Bun (needed for OpenTUI TUI on Windows)" + + if (Test-Command "winget") { + Write-Info "Trying winget install Oven-sh.Bun ..." + & winget install Oven-sh.Bun --accept-source-agreements --accept-package-agreements --silent + if ($LASTEXITCODE -eq 0) { + $bunPath = Join-Path $HOME ".bun\bin\bun.exe" + if ($env:Path -notlike "*$HOME\.bun\bin*") { + $env:Path = "$HOME\.bun\bin;$env:Path" + } + $cmd = Get-Command bun -ErrorAction SilentlyContinue + $candidate = if ($cmd) { $cmd.Source } elseif (Test-Path $bunPath) { $bunPath } else { $null } + if ($candidate) { + $versionOutput = & $candidate --version 2>$null + if ($LASTEXITCODE -eq 0 -and $versionOutput) { + Write-Ok "Bun installed via winget: $candidate ($($versionOutput.Trim()))" + return $candidate + } + Write-Warn "Bun at $candidate did not respond to --version; continuing to next strategy" + } + } + Write-Warn "winget install exited $LASTEXITCODE; falling back to direct download" + } + + $installDir = Join-Path $HOME ".bun" + $bunExe = Join-Path $installDir "bin\bun.exe" + if (Test-Path $bunExe) { + Write-Ok "Bun already downloaded at $bunExe" + return $bunExe + } + + Write-Info "Downloading Bun for Windows x64 from github.com/oven-sh/bun ..." + $zipUrl = "https://github.com/oven-sh/bun/releases/latest/download/bun-windows-x64.zip" + $tmpZip = Join-Path $env:TEMP "bun-windows-x64.zip" + try { + Invoke-WebRequest -Uri $zipUrl -OutFile $tmpZip -UseBasicParsing + Expand-Archive -Path $tmpZip -DestinationPath $installDir -Force + # The zip extracts as `bun-windows-x64\bun.exe`; flatten into bin\ + $nested = Join-Path $installDir "bun-windows-x64\bun.exe" + if (Test-Path $nested) { + New-Item -ItemType Directory -Force (Join-Path $installDir "bin") | Out-Null + Move-Item -Force $nested $bunExe + Remove-Item -Recurse -Force (Join-Path $installDir "bun-windows-x64") -ErrorAction SilentlyContinue + } + if (Test-Path $bunExe) { + $versionOutput = & $bunExe --version 2>$null + if ($LASTEXITCODE -eq 0 -and $versionOutput) { + Add-UserPath (Join-Path $installDir "bin") + Write-Ok "Bun installed via direct download: $bunExe ($($versionOutput.Trim()))" + return $bunExe + } + Write-Warn "Downloaded Bun at $bunExe did not respond to --version; install incomplete" + } + } catch { + Write-Warn "Direct download failed: $($_.Exception.Message)" + } finally { + Remove-Item -Force $tmpZip -ErrorAction SilentlyContinue + } + + Write-Err "Could not install Bun. OpenTUI TUI will not work." + Write-Info "Manual install: download from https://bun.com/docs/installation/windows" + Write-Info "Or re-run this script with -SkipBun to install with Node only (text CLI fallback)." + return $null +} + Set-Location (Join-Path $PSScriptRoot "..") $RepoRoot = (Get-Location).Path $InstallDir = Join-Path $HOME ".step-cli\bin" @@ -115,11 +207,12 @@ if ($Uninstall) { Remove-UserPath $InstallDir Write-Ok "Removed $InstallDir and its user PATH entry" Write-Info "Config and session history under $HOME\.step-cli are preserved." + Write-Info "Bun installation (under $HOME\.bun) is preserved; uninstall via winget or by deleting the directory." exit 0 } if (-not (Test-Command "pnpm")) { - Write-Section "[0/7] Bootstrapping pnpm via corepack" + Write-Section "[0/8] Bootstrapping pnpm via corepack" if (-not (Test-Command "corepack")) { Write-Err "Neither pnpm nor corepack was found in PATH." Write-Info "Install Node.js 20+ and then re-run this script." @@ -135,7 +228,7 @@ if (-not (Test-Command "pnpm")) { Write-Ok "pnpm $(pnpm --version) ready" } -Write-Section "[1/7] Installing workspace dependencies" +Write-Section "[1/8] Installing workspace dependencies" if ($SkipInstall) { Write-Info "Skipped (-SkipInstall)" } else { @@ -143,7 +236,35 @@ if ($SkipInstall) { Write-Ok "Workspace dependencies installed" } -Write-Section "[2/7] Initializing user config" +$BunBin = $null +if ($SkipBun) { + Write-Section "[2/8] Bun (skipped via -SkipBun)" + Write-Warn "OpenTUI TUI will be unavailable; step will fall back to text CLI." +} else { + Write-Section "[2/8] Bun runtime (for OpenTUI TUI)" + $BunBin = Install-Bun + if ($BunBin) { + # WinGet installs Bun as an "app execution alias" — a zero-byte reparse + # point under %LOCALAPPDATA%\Microsoft\WinGet\Links\bun.exe. PowerShell + # and cmd.exe resolve it via shell APIs, but Node's child_process.spawn + # uses CreateProcess directly and gets ENOENT. Setting STEP_BUN_BIN to + # such a shim breaks Invoke-StepCli (which spawns via Node). When we + # detect a shim, leave STEP_BUN_BIN unset: Node spawns fall back to tsx + # and the installed step.cmd launcher resolves Bun via `where bun`. + if ($BunBin -match "WinGet\\Links\\") { + Write-Ok "Bun available at $BunBin (WinGet app execution alias)" + Write-Info "STEP_BUN_BIN not set — Node spawn cannot use WinGet aliases directly." + Write-Info "step.cmd launcher will resolve Bun via PATH (cmd handles aliases)." + } else { + $env:STEP_BUN_BIN = $BunBin + Write-Ok "STEP_BUN_BIN set to $BunBin" + } + } else { + Write-Warn "Proceeding without Bun. OpenTUI TUI will use Node fallback." + } +} + +Write-Section "[3/8] Initializing user config" if ((Test-Path $UserConfig) -and (-not $ForceConfig)) { Write-Ok "Config already exists at $UserConfig (use -ForceConfig to overwrite)" } else { @@ -155,7 +276,7 @@ if ((Test-Path $UserConfig) -and (-not $ForceConfig)) { Write-Ok "Config written to $UserConfig" } -Write-Section "[3/7] Silero VAD" +Write-Section "[4/8] Silero VAD" pnpm setup:silero if ($LASTEXITCODE -ne 0) { throw "setup:silero failed with exit code $LASTEXITCODE" @@ -164,7 +285,7 @@ Invoke-StepCli vad set silero Invoke-StepCli vad status Write-Ok "Silero enabled (voice.defaults.vad = silero)" -Write-Section "[4/7] Browser audio / AEC" +Write-Section "[5/8] Browser audio / AEC" $Chrome = Find-Chrome if ($Chrome) { Write-Ok "Chrome/Chromium found: $Chrome" @@ -196,7 +317,7 @@ if ($Overseas) { Write-Ok "Patched $UserConfig for api.stepfun.ai" } -Write-Section "[5/7] Building production bundle" +Write-Section "[6/8] Building production bundle" if ($SkipBuild) { Write-Warn "Skipped (-SkipBuild). Will reuse existing dist." } else { @@ -204,7 +325,7 @@ Write-Section "[5/7] Building production bundle" Write-Ok "dist built" } -Write-Section "[6/7] Preparing Windows launcher" +Write-Section "[7/8] Preparing Windows launcher" if ($SkipBuild) { if (-not (Test-Path "dist\index.js")) { Write-Err "No existing dist\index.js found; cannot continue with -SkipBuild." @@ -215,7 +336,7 @@ if ($SkipBuild) { Write-Info "Using a Node-based step.cmd launcher; Bun native compilation is not required on Windows." } -Write-Section "[7/7] Installing to $InstallDir" +Write-Section "[8/8] Installing to $InstallDir" New-Item -ItemType Directory -Force $InstallDir | Out-Null foreach ($dir in @("package.json", "bin", "dist", "packages", "extensions", "skills", "node_modules")) { @@ -233,8 +354,20 @@ $Launcher = Join-Path $InstallDir "step.cmd" $LauncherContent = @" @echo off setlocal +pushd "%~dp0" || exit /b 1 +if defined STEP_BUN_BIN ( + "%STEP_BUN_BIN%" "%~dp0bin\step-cli.js" %* + goto :exit +) +where bun >nul 2>&1 && ( + bun "%~dp0bin\step-cli.js" %* + goto :exit +) node "%~dp0bin\step-cli.js" %* -exit /b %ERRORLEVEL% +:exit +set "STEP_EXIT_CODE=%ERRORLEVEL%" +popd +exit /b %STEP_EXIT_CODE% "@ Set-Content -Encoding ASCII -Path $Launcher -Value $LauncherContent Write-Ok "Installed launcher: $Launcher" diff --git a/scripts/setup.test.ts b/scripts/setup.test.ts index c59b701..3862173 100644 --- a/scripts/setup.test.ts +++ b/scripts/setup.test.ts @@ -117,3 +117,16 @@ describe("scripts/setup.sh", () => { }, ); }); + +describe("scripts/setup.ps1", () => { + it("runs the launcher from its install directory before resolving runtime imports", () => { + const script = readFileSync( + join(process.cwd(), "scripts", "setup.ps1"), + "utf8", + ); + + expect(script).toContain('pushd "%~dp0" || exit /b 1'); + expect(script).toContain('set "STEP_EXIT_CODE=%ERRORLEVEL%"'); + expect(script).toContain("popd"); + }); +}); diff --git a/src/commands/resume-command.ts b/src/commands/resume-command.ts index e860ffb..81e78bf 100644 --- a/src/commands/resume-command.ts +++ b/src/commands/resume-command.ts @@ -12,7 +12,9 @@ import { resolveStepCliRuntimeConfig } from "../runtime/runtime-config.js"; import { createLocalCliClientApp } from "../runtime/local-cli-app.js"; import { isOpenTuiEnabledInCurrentBuild, + isOpenTuiRuntimeSupported, loadOpenTuiClientAppFactoryAtRuntime, + warnWhenOpenTuiRuntimeUnsupported, } from "../runtime/open-tui-capability.js"; // Keep this build flag in the command module so rolldown can fold the bundle's @@ -42,12 +44,15 @@ export async function runResumeCommand(argv: string[]): Promise { actionCommand: Command, ) => { const cliOptionSources = readSharedRuntimeCliOptionSources(actionCommand); - const shouldUseTui = + const isTuiOtherwiseEligible = OPEN_TUI_COMPILE_TIME_ENABLED && isOpenTuiEnabledInCurrentBuild() && !options.json && process.stdin.isTTY === true && process.stdout.isTTY === true; + const shouldUseTui = + isTuiOtherwiseEligible && isOpenTuiRuntimeSupported(); + warnWhenOpenTuiRuntimeUnsupported(isTuiOtherwiseEligible); const { stepCliConfig } = await resolveStepCliRuntimeConfig({ options, cliOptionSources, diff --git a/src/commands/root-command.test.ts b/src/commands/root-command.test.ts new file mode 100644 index 0000000..dd06863 --- /dev/null +++ b/src/commands/root-command.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { shouldUseTui } from "./root-command.js"; + +const originalBunVersion = process.versions.bun; + +beforeEach(() => { + delete (process.versions as { bun?: string }).bun; +}); + +afterEach(() => { + if (originalBunVersion === undefined) { + delete (process.versions as { bun?: string }).bun; + } else { + (process.versions as { bun?: string }).bun = originalBunVersion; + } +}); + +describe("shouldUseTui", () => { + it("returns false on Node.js even when all other conditions hold", () => { + const result = shouldUseTui({ + options: {}, + prompt: undefined, + attachments: undefined, + stdinIsTTY: true, + stdoutIsTTY: true, + }); + expect(result).toBe(false); + }); + + it("returns true on Bun when stdin/stdout are TTY and no prompt/attachments/json", () => { + (process.versions as { bun?: string }).bun = "1.1.0"; + const result = shouldUseTui({ + options: {}, + prompt: undefined, + attachments: undefined, + stdinIsTTY: true, + stdoutIsTTY: true, + }); + expect(result).toBe(true); + }); + + it("returns false when --json is passed (even on Bun)", () => { + (process.versions as { bun?: string }).bun = "1.1.0"; + const result = shouldUseTui({ + options: { json: true }, + prompt: undefined, + attachments: undefined, + stdinIsTTY: true, + stdoutIsTTY: true, + }); + expect(result).toBe(false); + }); + + it("returns false when a prompt is provided (one-shot mode)", () => { + (process.versions as { bun?: string }).bun = "1.1.0"; + const result = shouldUseTui({ + options: {}, + prompt: "summarize src/index.ts", + attachments: undefined, + stdinIsTTY: true, + stdoutIsTTY: true, + }); + expect(result).toBe(false); + }); + + it("returns false when stdin is not a TTY (piped input)", () => { + (process.versions as { bun?: string }).bun = "1.1.0"; + const result = shouldUseTui({ + options: {}, + prompt: undefined, + attachments: undefined, + stdinIsTTY: false, + stdoutIsTTY: true, + }); + expect(result).toBe(false); + }); +}); diff --git a/src/commands/root-command.ts b/src/commands/root-command.ts index 6abc139..812a6aa 100644 --- a/src/commands/root-command.ts +++ b/src/commands/root-command.ts @@ -14,7 +14,9 @@ import { resolveStepCliRuntimeConfig } from "../runtime/runtime-config.js"; import { createLocalCliClientApp } from "../runtime/local-cli-app.js"; import { isOpenTuiEnabledInCurrentBuild, + isOpenTuiRuntimeSupported, loadOpenTuiClientAppFactoryAtRuntime, + warnWhenOpenTuiRuntimeUnsupported, } from "../runtime/open-tui-capability.js"; // Keep this build flag in the command module so rolldown can fold the bundle's @@ -22,6 +24,34 @@ import { const OPEN_TUI_COMPILE_TIME_ENABLED = process.env.STEP_CLI_ENABLE_OPENTUI !== "0"; +export interface ShouldUseTuiInputs { + options: { json?: boolean }; + prompt: string | undefined; + attachments: ReadonlyArray | undefined; + stdinIsTTY: boolean; + stdoutIsTTY: boolean; +} + +/** + * Decide whether the root command should launch the OpenTUI TUI. Pure + * function so it can be unit-tested without spawning commander. + */ +export function shouldUseTui(inputs: ShouldUseTuiInputs): boolean { + return isTuiOtherwiseEligible(inputs) && isOpenTuiRuntimeSupported(); +} + +function isTuiOtherwiseEligible(inputs: ShouldUseTuiInputs): boolean { + return ( + OPEN_TUI_COMPILE_TIME_ENABLED && + isOpenTuiEnabledInCurrentBuild() && + !inputs.options.json && + (inputs.prompt?.trim().length ?? 0) === 0 && + (inputs.attachments?.length ?? 0) === 0 && + inputs.stdinIsTTY === true && + inputs.stdoutIsTTY === true + ); +} + export async function runRootCommand(argv: string[]): Promise { const program = configureCommanderProgram(new Command()); @@ -76,22 +106,23 @@ export async function runRootCommand(argv: string[]): Promise { }); const cliOptionSources = readSharedRuntimeCliOptionSources(actionCommand); - const shouldUseTui = - OPEN_TUI_COMPILE_TIME_ENABLED && - isOpenTuiEnabledInCurrentBuild() && - !options.json && - (prompt?.trim().length ?? 0) === 0 && - (attachments?.length ?? 0) === 0 && - process.stdin.isTTY === true && - process.stdout.isTTY === true; + const tuiInputs = { + options, + prompt, + attachments, + stdinIsTTY: process.stdin.isTTY === true, + stdoutIsTTY: process.stdout.isTTY === true, + } satisfies ShouldUseTuiInputs; + const shouldUseTuiResult = shouldUseTui(tuiInputs); + warnWhenOpenTuiRuntimeUnsupported(isTuiOtherwiseEligible(tuiInputs)); const { stepCliConfig } = await resolveStepCliRuntimeConfig({ options, cliOptionSources, resumeSession: Boolean(options.resume), - useAlternateScreen: shouldUseTui ? options.altScreen : false, - interactionSurface: shouldUseTui ? "interactive" : undefined, + useAlternateScreen: shouldUseTuiResult ? options.altScreen : false, + interactionSurface: shouldUseTuiResult ? "interactive" : undefined, }); - if (shouldUseTui) { + if (shouldUseTuiResult) { const createLocalTuiClientApp = await loadOpenTuiClientAppFactoryAtRuntime(); const app = await createLocalTuiClientApp(stepCliConfig); diff --git a/src/commands/voice-command.test.ts b/src/commands/voice-command.test.ts new file mode 100644 index 0000000..391806a --- /dev/null +++ b/src/commands/voice-command.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { getVoiceRuntimeError } from "./voice-command.js"; + +const originalBunVersion = process.versions.bun; + +beforeEach(() => { + delete (process.versions as { bun?: string }).bun; +}); + +afterEach(() => { + if (originalBunVersion === undefined) { + delete (process.versions as { bun?: string }).bun; + } else { + (process.versions as { bun?: string }).bun = originalBunVersion; + } +}); + +describe("getVoiceRuntimeError", () => { + it("returns null on Bun runtime", () => { + (process.versions as { bun?: string }).bun = "1.1.0"; + expect(getVoiceRuntimeError()).toBeNull(); + }); + + it("returns a non-empty error string on Node runtime", () => { + expect(getVoiceRuntimeError()).not.toBeNull(); + expect(getVoiceRuntimeError()).toMatch(/Bun runtime/); + }); + + it("error message mentions STEP_BUN_BIN for actionable recovery", () => { + expect(getVoiceRuntimeError()).toMatch(/STEP_BUN_BIN/); + }); + + it("error message includes install instructions", () => { + const message = getVoiceRuntimeError(); + expect(message).not.toBeNull(); + // Either winget (Windows) or bun.sh URL (other platforms). + expect(message).toMatch(/(winget|bun\.sh)/); + }); +}); diff --git a/src/commands/voice-command.ts b/src/commands/voice-command.ts index f0cd402..785faa1 100644 --- a/src/commands/voice-command.ts +++ b/src/commands/voice-command.ts @@ -9,7 +9,10 @@ import { type SharedRuntimeCliOptions, } from "./shared-runtime-options.js"; import { resolveStepCliRuntimeConfig } from "../runtime/runtime-config.js"; -import { loadOpenTuiClientAppFactoryAtRuntime } from "../runtime/open-tui-capability.js"; +import { + isOpenTuiRuntimeSupported, + loadOpenTuiClientAppFactoryAtRuntime, +} from "../runtime/open-tui-capability.js"; import type { VoiceBootstrapConfig } from "../runtime/voice-bootstrap-config.js"; import { loadVoiceConfigFile, @@ -186,6 +189,11 @@ default.`, }, }; + const voiceRuntimeError = getVoiceRuntimeError(); + if (voiceRuntimeError !== null) { + throw new Error(voiceRuntimeError); + } + const createLocalTuiClientApp = await loadOpenTuiClientAppFactoryAtRuntime(); const app = await createLocalTuiClientApp(stepCliConfig, voice); @@ -199,6 +207,21 @@ default.`, await parseCommanderProgram(voiceProgram, ["node", "step voice", ...argv]); } +/** + * Returns null when voice mode may proceed, or an error message string when + * the runtime cannot support OpenTUI. Pure function for unit testing. + */ +export function getVoiceRuntimeError(): string | null { + if (isOpenTuiRuntimeSupported()) { + return null; + } + const installHint = + process.platform === "win32" + ? "Install Bun via `winget install Oven-sh.Bun`" + : "Install Bun from https://bun.sh"; + return `step voice requires Bun runtime on this platform. ${installHint}, or set STEP_BUN_BIN, then re-run.`; +} + function normalizeInputMode( value: string | undefined, ): VoiceBootstrapConfig["inputMode"] { diff --git a/src/runtime/open-tui-capability.test.ts b/src/runtime/open-tui-capability.test.ts new file mode 100644 index 0000000..76e9081 --- /dev/null +++ b/src/runtime/open-tui-capability.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { + isOpenTuiEnabledInCurrentBuild, + isOpenTuiRuntimeSupported, + parseOpenTuiEnabledValue, + warnWhenOpenTuiRuntimeUnsupported, +} from "./open-tui-capability.js"; + +const originalBunVersion = process.versions.bun; + +afterEach(() => { + // Restore process.versions.bun between tests so mocks don't leak. + if (originalBunVersion === undefined) { + delete (process.versions as { bun?: string }).bun; + } else { + (process.versions as { bun?: string }).bun = originalBunVersion; + } +}); + +describe("parseOpenTuiEnabledValue", () => { + it("returns true when value is undefined", () => { + expect(parseOpenTuiEnabledValue(undefined)).toBe(true); + }); + + it("returns false for '0' and 'false' (case-insensitive)", () => { + expect(parseOpenTuiEnabledValue("0")).toBe(false); + expect(parseOpenTuiEnabledValue("false")).toBe(false); + expect(parseOpenTuiEnabledValue("FALSE")).toBe(false); + expect(parseOpenTuiEnabledValue(" false ")).toBe(false); + }); + + it("returns true for any other value", () => { + expect(parseOpenTuiEnabledValue("1")).toBe(true); + expect(parseOpenTuiEnabledValue("true")).toBe(true); + }); +}); + +describe("isOpenTuiEnabledInCurrentBuild", () => { + it("reflects the STEP_CLI_ENABLE_OPENTUI env var parsed at module load", () => { + // Module was loaded with whatever env was active at import time; we just + // assert the function returns a boolean and does not throw. + expect(typeof isOpenTuiEnabledInCurrentBuild()).toBe("boolean"); + }); +}); + +describe("isOpenTuiRuntimeSupported", () => { + it("returns true when process.versions.bun is a version string", () => { + (process.versions as { bun?: string }).bun = "1.1.0"; + expect(isOpenTuiRuntimeSupported()).toBe(true); + }); + + it("returns false when process.versions.bun is undefined (Node.js)", () => { + delete (process.versions as { bun?: string }).bun; + expect(isOpenTuiRuntimeSupported()).toBe(false); + }); + + it("returns false when process.versions.bun is an empty string", () => { + (process.versions as { bun?: string }).bun = ""; + expect(isOpenTuiRuntimeSupported()).toBe(false); + }); +}); + +describe("warnWhenOpenTuiRuntimeUnsupported", () => { + it("warns only when the runtime is the sole blocker for an interactive TUI", () => { + delete (process.versions as { bun?: string }).bun; + const messages: string[] = []; + const stderr = { + isTTY: true, + write: (message: string) => { + messages.push(message); + return true; + }, + }; + + warnWhenOpenTuiRuntimeUnsupported(true, stderr); + warnWhenOpenTuiRuntimeUnsupported(false, stderr); + + expect(messages).toEqual([ + "warning: OpenTUI TUI requires Bun runtime; falling back to text CLI. Install Bun or use a Bun-based launcher.\n", + ]); + }); + + it("does not warn when Bun supports the TUI runtime", () => { + (process.versions as { bun?: string }).bun = "1.1.0"; + const messages: string[] = []; + + warnWhenOpenTuiRuntimeUnsupported(true, { + isTTY: true, + write: (message: string) => { + messages.push(message); + return true; + }, + }); + + expect(messages).toEqual([]); + }); +}); diff --git a/src/runtime/open-tui-capability.ts b/src/runtime/open-tui-capability.ts index 4866a9d..59439af 100644 --- a/src/runtime/open-tui-capability.ts +++ b/src/runtime/open-tui-capability.ts @@ -33,3 +33,43 @@ export async function loadOpenTuiClientAppFactoryAtRuntime(): Promise