Repository navigation
feat(opentui): Activate the OpenTUI backend behind QWEN_TUI_RENDERER #10739
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
7a7d73a
b7ef05b
c2ae855
2057cbb
66674ae
5b1dd15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1198,6 +1198,33 @@ export async function main() { | |
| // startInteractiveUI) and so the first paint uses the refined theme | ||
| // when the probe finishes in time. | ||
| await themeAutoDetectionComplete; | ||
| // Renderer dispatch for the ink→OpenTUI migration: QWEN_TUI_RENDERER | ||
| // selects the experimental backend only on a runtime that can drive it; | ||
| // every other case — including a failed renderer probe inside the entry — | ||
| // falls through to ink, which stays the default renderer. | ||
| const { selectTuiRenderer } = await import( | ||
| './ui/opentui/renderer-selection.js' | ||
| ); | ||
| if (selectTuiRenderer().renderer === 'opentui') { | ||
| const { startOpenTuiUI } = await import( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 5b1dd15. The dispatch block now wraps the entry import and the boot call in try/catch: a module-evaluation failure (theme.ts |
||
| './ui/opentui/start-opentui-ui.js' | ||
| ); | ||
| const started = await startOpenTuiUI( | ||
| config, | ||
| settings, | ||
| startupWarnings, | ||
| process.cwd(), | ||
| initializationResult!, | ||
| { | ||
| postRenderConnectIde: deferIdeConnection, | ||
| extensionRefreshState, | ||
| }, | ||
| ); | ||
| if (started) { | ||
| clearCorruptionEnvVars(); | ||
| return; | ||
| } | ||
| } | ||
| const { startInteractiveUI } = await import('./ui/startInteractiveUI.js'); | ||
| await startInteractiveUI( | ||
| config, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Qwen | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| /** | ||
| * Outcome-option construction for the tool-confirmation dialog: the | ||
| * always-allow rows depend on the optional `hideAlwaysAllow` flag that only | ||
| * some confirmation-detail union members carry (ask_user_question has no such | ||
| * field), so the gate must narrow before reading it. | ||
| */ | ||
|
|
||
| import { describe, it, expect, vi } from 'vitest'; | ||
|
|
||
| // theme.ts builds a SyntaxStyle at module scope, which needs the OpenTUI | ||
| // native FFI — unavailable in the test runtime. Stub the graphics surface. | ||
| vi.mock('@opentui/core', () => ({ | ||
| SyntaxStyle: { fromStyles: () => ({}) }, | ||
| MouseButton: { LEFT: 0 }, | ||
| })); | ||
|
|
||
| import { | ||
| ToolConfirmationOutcome, | ||
| type ToolCallConfirmationDetails, | ||
| } from '@qwen-code/qwen-code-core'; | ||
| import { buildOutcomeOptions } from './dialogs-confirm.js'; | ||
|
|
||
| const onConfirm = async () => {}; | ||
|
|
||
| const execDetails = ( | ||
| hideAlwaysAllow?: boolean, | ||
| ): ToolCallConfirmationDetails => ({ | ||
| type: 'exec', | ||
| title: 'Run command', | ||
| onConfirm, | ||
| hideAlwaysAllow, | ||
| command: 'ls -la', | ||
| rootCommand: 'ls', | ||
| }); | ||
|
|
||
| const askDetails = (): ToolCallConfirmationDetails => ({ | ||
| type: 'ask_user_question', | ||
| title: 'A question', | ||
| questions: [ | ||
| { | ||
| question: 'Pick one', | ||
| header: 'Choice', | ||
| options: [{ label: 'A', description: 'option a' }], | ||
| }, | ||
| ], | ||
| onConfirm, | ||
| }); | ||
|
|
||
| describe('buildOutcomeOptions', () => { | ||
| it('offers allow-once, both always-allow rows, and cancel by default', () => { | ||
| const values = buildOutcomeOptions(execDetails()).map((o) => o.value); | ||
| expect(values).toEqual([ | ||
| ToolConfirmationOutcome.ProceedOnce, | ||
| ToolConfirmationOutcome.ProceedAlwaysProject, | ||
| ToolConfirmationOutcome.ProceedAlwaysUser, | ||
| ToolConfirmationOutcome.Cancel, | ||
| ]); | ||
| }); | ||
|
|
||
| it('drops the always-allow rows when hideAlwaysAllow is set', () => { | ||
| const values = buildOutcomeOptions(execDetails(true)).map((o) => o.value); | ||
| expect(values).toEqual([ | ||
| ToolConfirmationOutcome.ProceedOnce, | ||
| ToolConfirmationOutcome.Cancel, | ||
| ]); | ||
| }); | ||
|
|
||
| it('handles details without the hideAlwaysAllow field at all', () => { | ||
| // ask_user_question has no hideAlwaysAllow — reading it unguarded is a | ||
| // type error and would misrender the dialog for every question card. | ||
| const values = buildOutcomeOptions(askDetails()).map((o) => o.value); | ||
| expect(values).toContain(ToolConfirmationOutcome.ProceedOnce); | ||
| expect(values).toContain(ToolConfirmationOutcome.Cancel); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[R1-2 · minor] The gate-rejection path — the one every currently-supported runtime takes — says nothing at all, and the PR description has the two fallback paths swapped.
witness: executed (gate probed with 11 inputs at head) + grep for any consumer of selection.reason.selectTuiRenderer()returns areasonwhose own doc says "Human-readable reason, for debug logging. Never shown to the user" (renderer-selection.ts:33), andrenderer-selection.test.ts:101even asserts its wording (toContain('native FFI')). But nothing reads it: this line destructures only.renderer, and there is no otherselectTuiRendererconsumer inpackages/cli/srcoutside the module and its test. So on Node 22/24 — i.e..nvmrc,engines.node >=22.0.0, and every CI runner —QWEN_TUI_RENDERER=opentuiis accepted by the user, rejected by the gate, and produces no output whatsoever: the CLI looks like the flag was ignored, which is the same symptom as the bug the PR's own "Before" section describes ("the flag was inert").The asymmetry is what makes it worth a line, because the code already knows how to do this: the construction-failure path prints —
start-opentui-ui.tsx:282-285writesWarning: OpenTUI renderer unavailable — <msg> (falling back to ink)to stderr. So the loud path is the rare one and the mute path is the universal one, while the Risk section says the opposite ("a failed renderer probe falls back to ink silently"). Both statements can't be right; the silent-vs-loud split is worth deciding on purpose.Cheapest resolution, either direction:
if (selection.renderer !== 'opentui' && process.env.QWEN_TUI_RENDERER) debugLogger.info(selection.reason)at the call site — or drop the "for debug logging" promise from the docblock. Note the currentif (selectTuiRenderer().renderer === 'opentui')form also recomputes the selection rather than holding it, so a named local would let the reason be logged without calling the gate twice.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 5b1dd15: the refusal path now logs
selection.reasonto the debug log (STARTUP tag), and the boot-failure path warns on stderr. The PR description's swapped fallback paths are corrected in the body.