From 9a72c4e33769a05ce35b2d12390aff712b3305e3 Mon Sep 17 00:00:00 2001 From: Sebastian Herrlinger Date: Tue, 30 Jun 2026 11:52:09 +0200 Subject: [PATCH 01/15] debug: add Windows OpenTUI standalone repro --- .github/workflows/test.yml | 34 +++++ script/repro-windows-opentui-crash.ps1 | 168 +++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 script/repro-windows-opentui-crash.ps1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c69de1d93b..fcacd79fa9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,12 @@ on: - dev pull_request: workflow_dispatch: + inputs: + windows_opentui_repro: + description: "Run targeted Windows OpenTUI standalone crash repro" + required: false + type: boolean + default: false concurrency: # Keep every run on dev so cancelled checks do not pollute the default branch @@ -22,6 +28,7 @@ env: jobs: unit: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.windows_opentui_repro != true }} name: unit (${{ matrix.settings.name }}) strategy: fail-fast: false @@ -80,6 +87,7 @@ jobs: run: bun run test:httpapi e2e: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.windows_opentui_repro != true }} name: e2e (${{ matrix.settings.name }}) strategy: fail-fast: false @@ -149,3 +157,29 @@ jobs: path: | packages/app/e2e/test-results packages/app/e2e/playwright-report + + windows-opentui-repro: + if: ${{ github.event_name == 'workflow_dispatch' && inputs.windows_opentui_repro == true }} + name: windows opentui repro (${{ matrix.version }}) + strategy: + fail-fast: false + matrix: + version: ["1.17.9", "1.17.10", "1.17.11"] + runs-on: blacksmith-4vcpu-windows-2025 + defaults: + run: + shell: pwsh + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "24.11.1" + + - name: Run Windows standalone repro + timeout-minutes: 12 + run: ./script/repro-windows-opentui-crash.ps1 -Version "${{ matrix.version }}" diff --git a/script/repro-windows-opentui-crash.ps1 b/script/repro-windows-opentui-crash.ps1 new file mode 100644 index 0000000000..37488a9a59 --- /dev/null +++ b/script/repro-windows-opentui-crash.ps1 @@ -0,0 +1,168 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Version, + [int]$McpLoops = 30, + [int]$McpServers = 12, + [int]$StartupSeconds = 130 +) + +$ErrorActionPreference = "Stop" + +function Write-Section([string]$Name) { + Write-Host "" + Write-Host "==== $Name ====" +} + +function Read-TextFile([string]$Path) { + if (!(Test-Path $Path)) { + return "" + } + return [System.IO.File]::ReadAllText($Path) +} + +function Test-CrashText([string]$Text) { + if ($Text -match "Segmentation fault") { return $true } + if ($Text -match "Bun has crashed") { return $true } + if ($Text -match "ACCESS_VIOLATION") { return $true } + if ($Text -match "0xC0000005") { return $true } + if ($Text -match "322122") { return $true } + if ($Text -match "panic\(main thread\)") { return $true } + return $false +} + +function Invoke-Opencode { + param( + [Parameter(Mandatory = $true)] + [string]$Label, + [Parameter(Mandatory = $true)] + [string]$Exe, + [string[]]$Args = @(), + [Parameter(Mandatory = $true)] + [string]$WorkingDirectory, + [int]$TimeoutSeconds = 60, + [switch]$AllowNonZero, + [switch]$AllowTimeout + ) + + Write-Section $Label + Write-Host "cwd=$WorkingDirectory" + Write-Host "cmd=$Exe $($Args -join ' ')" + + $safe = ($Label -replace "[^A-Za-z0-9_.-]", "_") + $stdout = Join-Path $env:RUNNER_TEMP "$safe.out.log" + $stderr = Join-Path $env:RUNNER_TEMP "$safe.err.log" + Remove-Item -ErrorAction SilentlyContinue $stdout, $stderr + + $process = Start-Process -FilePath $Exe -ArgumentList $Args -WorkingDirectory $WorkingDirectory -NoNewWindow -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr + $exited = $process.WaitForExit($TimeoutSeconds * 1000) + if (!$exited) { + Write-Host "timeout after ${TimeoutSeconds}s; terminating process id $($process.Id)" + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 1 + } + + $outText = Read-TextFile $stdout + $errText = Read-TextFile $stderr + if ($outText.Length -gt 0) { + Write-Host "--- stdout ---" + Write-Host $outText + } + if ($errText.Length -gt 0) { + Write-Host "--- stderr ---" + Write-Host $errText + } + + $combined = $outText + "`n" + $errText + if (Test-CrashText $combined) { + throw "native crash signature detected in $Label" + } + + if (!$exited) { + if ($AllowTimeout) { + Write-Host "timeout accepted for long-running TUI startup check" + return + } + throw "timeout in $Label" + } + + $exitCode = [int64]$process.ExitCode + Write-Host "exitCode=$exitCode" + if (@(3, -1073741819, -1073740791, -1073741571) -contains $exitCode) { + throw "native crash exit code $exitCode in $Label" + } + if (!$AllowNonZero -and $process.ExitCode -ne 0) { + throw "unexpected exit code $($process.ExitCode) in $Label" + } +} + +Write-Section "Environment" +node --version +npm --version +Write-Host "RUNNER_OS=$env:RUNNER_OS RUNNER_ARCH=$env:RUNNER_ARCH" +Write-Host "PROCESSOR_ARCHITECTURE=$env:PROCESSOR_ARCHITECTURE" + +Write-Section "Install opencode-ai@$Version" +npm uninstall -g opencode-ai opencode-windows-x64 2>$null | Out-Host +npm install -g "opencode-ai@$Version" + +$npmRoot = (npm root -g).Trim() +$exe = Join-Path $npmRoot "opencode-ai\bin\opencode.exe" +if (!(Test-Path $exe)) { + $cmd = Get-Command opencode -ErrorAction Stop + $exe = $cmd.Source +} +Write-Host "opencode executable=$exe" +if (Test-Path $exe) { + $info = (Get-Item $exe).VersionInfo + Write-Host "FileDescription=$($info.FileDescription) FileVersion=$($info.FileVersion) ProductVersion=$($info.ProductVersion) CompanyName=$($info.CompanyName)" +} + +$root = Join-Path $env:RUNNER_TEMP "opencode-windows-opentui-$Version" +Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $root +New-Item -ItemType Directory -Force $root | Out-Null +$env:XDG_CONFIG_HOME = Join-Path $root "xdg-config" +$env:XDG_DATA_HOME = Join-Path $root "xdg-data" +$env:XDG_CACHE_HOME = Join-Path $root "xdg-cache" +$env:XDG_STATE_HOME = Join-Path $root "xdg-state" +$env:APPDATA = Join-Path $root "appdata" +$env:LOCALAPPDATA = Join-Path $root "localappdata" +$env:OPENCODE_DISABLE_AUTOUPDATE = "1" +$env:OPENCODE_DISABLE_EXTERNAL_SKILLS = "1" +$env:OPENCODE_DISABLE_LSP_DOWNLOAD = "1" +$env:OPENCODE_PURE = "1" +New-Item -ItemType Directory -Force $env:XDG_CONFIG_HOME, $env:XDG_DATA_HOME, $env:XDG_CACHE_HOME, $env:XDG_STATE_HOME, $env:APPDATA, $env:LOCALAPPDATA | Out-Null + +$emptyProject = Join-Path $root "empty-project" +$mcpProject = Join-Path $root "mcp-project" +New-Item -ItemType Directory -Force $emptyProject, $mcpProject | Out-Null + +Invoke-Opencode -Label "version" -Exe $exe -Args @("--version") -WorkingDirectory $emptyProject -TimeoutSeconds 30 +Invoke-Opencode -Label "help" -Exe $exe -Args @("--help") -WorkingDirectory $emptyProject -TimeoutSeconds 30 + +Write-Section "Write MCP spawn-storm config" +$servers = [ordered]@{} +for ($i = 1; $i -le $McpServers; $i++) { + $name = "server$($i.ToString('00'))" + $servers[$name] = [ordered]@{ + type = "local" + command = @("node", "-e", "setTimeout(() => {}, 30000)") + timeout = 350 + } +} +$config = [ordered]@{ + autoupdate = $false + mcp = $servers +} +($config | ConvertTo-Json -Depth 10) | Set-Content -Encoding UTF8 (Join-Path $mcpProject "opencode.json") +Get-Content (Join-Path $mcpProject "opencode.json") | Write-Host + +for ($i = 1; $i -le $McpLoops; $i++) { + Invoke-Opencode -Label "mcp-list-$i" -Exe $exe -Args @("mcp", "list") -WorkingDirectory $mcpProject -TimeoutSeconds 45 -AllowNonZero +} + +Invoke-Opencode -Label "empty-startup-tui" -Exe $exe -Args @() -WorkingDirectory $emptyProject -TimeoutSeconds $StartupSeconds -AllowTimeout -AllowNonZero +Invoke-Opencode -Label "mini-demo-question" -Exe $exe -Args @("--mini", "--demo", "/question", "single") -WorkingDirectory $emptyProject -TimeoutSeconds 30 -AllowNonZero +Invoke-Opencode -Label "mini-demo-permission" -Exe $exe -Args @("--mini", "--demo", "/permission", "bash") -WorkingDirectory $emptyProject -TimeoutSeconds 30 -AllowNonZero + +Write-Section "Result" +Write-Host "No native crash signature detected for opencode-ai@$Version" From 5030097b553980285971e6923724efe62b67e6e4 Mon Sep 17 00:00:00 2001 From: Sebastian Herrlinger Date: Tue, 30 Jun 2026 11:54:43 +0200 Subject: [PATCH 02/15] fix(tui): satisfy strict dialog prompt types --- packages/tui/src/routes/session/index.tsx | 2 +- packages/tui/src/ui/dialog-prompt.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index f832174aa4..47dc19dbdd 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1528,7 +1528,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las customBorderChars={SplitBorder.customBorderChars} borderColor={theme.error} > - {props.message.error?.data.message} + {String(props.message.error?.data.message ?? "")} diff --git a/packages/tui/src/ui/dialog-prompt.tsx b/packages/tui/src/ui/dialog-prompt.tsx index d627ea2970..63dc2ca6dd 100644 --- a/packages/tui/src/ui/dialog-prompt.tsx +++ b/packages/tui/src/ui/dialog-prompt.tsx @@ -8,7 +8,7 @@ import { useBindings, useCommandShortcut } from "../keymap" export type DialogPromptProps = { title: string - description?: () => JSX.Element + description?: JSX.Element | (() => JSX.Element) placeholder?: string value?: string busy?: boolean @@ -25,6 +25,11 @@ export function DialogPrompt(props: DialogPromptProps) { const [textareaTarget, setTextareaTarget] = createSignal() let textarea: TextareaRenderable + const description = () => { + if (typeof props.description === "function") return props.description() + return props.description + } + function confirm() { if (props.busy) return props.onConfirm?.(textarea.plainText) @@ -83,7 +88,7 @@ export function DialogPrompt(props: DialogPromptProps) { - {props.description} + {description()}