diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c69de1d93b..edfff29ac9 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,85 @@ 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.name }}, ${{ matrix.version }}, ${{ matrix.host }})
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - version: "1.17.9"
+ host: windows-latest
+ name: baseline
+ markdownLoops: 1
+ onlyMarkdown: false
+ - version: "1.17.9"
+ host: windows-latest
+ name: markdown-stress
+ markdownLoops: 10
+ onlyMarkdown: true
+ - version: "1.17.10"
+ host: windows-latest
+ name: baseline
+ markdownLoops: 1
+ onlyMarkdown: false
+ - version: "1.17.10"
+ host: windows-latest
+ name: markdown-stress
+ markdownLoops: 10
+ onlyMarkdown: true
+ - version: "1.17.11"
+ host: windows-latest
+ name: baseline
+ markdownLoops: 1
+ onlyMarkdown: false
+ - version: "1.17.11"
+ host: windows-latest
+ name: markdown-stress
+ markdownLoops: 10
+ onlyMarkdown: true
+ - version: "1.17.10"
+ host: windows-2022
+ name: baseline
+ markdownLoops: 1
+ onlyMarkdown: false
+ runs-on: ${{ matrix.host }}
+ 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: 30
+ run: |
+ $reproArgs = @{
+ Version = "${{ matrix.version }}"
+ MarkdownLoops = ${{ matrix.markdownLoops }}
+ }
+ if ("${{ matrix.onlyMarkdown }}" -eq "true") {
+ $reproArgs.OnlyMarkdown = $true
+ }
+ ./script/repro-windows-opentui-crash.ps1 @reproArgs
+
+ - name: Upload Windows repro artifacts
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: windows-opentui-repro-${{ matrix.name }}-${{ matrix.version }}-${{ matrix.host }}-${{ github.run_attempt }}
+ if-no-files-found: ignore
+ retention-days: 7
+ path: |
+ ${{ runner.temp }}/opencode-windows-opentui-${{ matrix.version }}/**/*.log
+ ${{ runner.temp }}/opencode-windows-opentui-${{ matrix.version }}/**/*.jsonl
+ ${{ runner.temp }}/*.out.log
+ ${{ runner.temp }}/*.err.log
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()}