Repository navigation
feat(tui): trial an anchored composer in fullscreen mode on nightly #4221
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
Merged
me2seeks
merged 12 commits into
apache:main
from
abhinav-phi:feat/tui-fullscreen-anchored-composer
Oct 13, 2026
Merged
Changes from 8 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
285c603
feat(tui): trial an anchored composer in fullscreen mode on nightly (…
abhinav-phi ce164e6
fix(tui): harden the fullscreen link opener against model-authored hrefs
abhinav-phi ad38322
style(tui): keep the ScrollView import on its own line
abhinav-phi aa07757
fix(tui): address automated review and keep the fullscreen trial conf…
abhinav-phi 6127908
style(tui): format the review-fix files with the current biome
abhinav-phi b28432f
test(tui): make the real spawn-failure case deterministic on any host
abhinav-phi 4376bbe
fix(tui): anchor the buildVersion spread in a gap upstream does not r…
abhinav-phi ee5328d
Merge remote-tracking branch 'upstream/main' into feat/tui-fullscreen…
abhinav-phi 9f24387
fix(tui): render Host questions and the Todo row in the fullscreen tree
abhinav-phi 4d34f5e
fix(tui): count the live-activity separator in the fullscreen chrome …
abhinav-phi d8d59fc
fix(tui): let the unread banner and question margin yield on short sc…
abhinav-phi e5c4ea5
fix(tui): keep fullscreen chrome visible and locale-aware
abhinav-phi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
|
|
||
| import { spawn } from 'node:child_process'; | ||
|
|
||
| /** | ||
| * Nightly trial switch for the fullscreen (alternate-screen) TUI, issue #4136. | ||
| * | ||
| * The fullscreen path swaps `TuiMainScreen` for `TuiAltScreen`: the composer | ||
| * and status line stay anchored to the bottom of the screen while the | ||
| * transcript scrolls in an application-owned viewport. This is a scoped | ||
| * product experiment, not a stable mode: it defaults on only for nightly | ||
| * builds and must not grow a permanent user-facing toggle until the nightly | ||
| * evidence is in (issue non-goals). | ||
| * | ||
| * Precedence, highest first: | ||
| * 1. `setting` — an explicit caller decision (`MakaPiTuiInput.tuiFullscreen`), | ||
| * used by embeddings and tests. | ||
| * 2. `override` — the `MAKA_TUI_FULLSCREEN` environment variable. `1`/`true` | ||
| * opts a release build in; `0`/`false` opts a nightly build out. | ||
| * 3. `packageVersion` — nightly default: on for `-dev.` versions (the | ||
| * Product Nightly identity in scripts/product-nightly.mjs), off otherwise. | ||
| */ | ||
| export const TUI_FULLSCREEN_ENV = 'MAKA_TUI_FULLSCREEN'; | ||
|
|
||
| export interface TuiFullscreenResolution { | ||
| readonly setting?: boolean; | ||
| readonly override?: string; | ||
| readonly packageVersion?: string; | ||
| } | ||
|
|
||
| export function isNightlyPackageVersion(version: string | undefined): boolean { | ||
| if (!version) return false; | ||
| // Product Nightly versions look like `0.2.0-dev.<runNumber>.<YYYYMMDD>`; | ||
| // formal releases are always a stable product version. | ||
| return /-dev\.[1-9]\d*\.\d{8}$/u.test(version); | ||
| } | ||
|
|
||
| export function resolveTuiFullscreen(resolution: TuiFullscreenResolution = {}): boolean { | ||
| if (typeof resolution.setting === 'boolean') return resolution.setting; | ||
| const override = resolution.override?.trim().toLowerCase(); | ||
| if (override === '1' || override === 'true') return true; | ||
| if (override === '0' || override === 'false') return false; | ||
| return isNightlyPackageVersion(resolution.packageVersion); | ||
| } | ||
|
|
||
| /** | ||
| * Per-frame snapshot of the transcript scroll viewport. The chrome reads it | ||
| * once per render to drive the unread indicator; the document line count comes | ||
| * from the transcript document wrapper that renders inside the scroll view. | ||
| */ | ||
| export interface TranscriptWindowSnapshot { | ||
| /** True while the scroll view is pinned to the newest content. */ | ||
| readonly followingEnd: boolean; | ||
| /** Total rendered transcript document lines this frame. */ | ||
| readonly documentLines: number; | ||
| } | ||
|
|
||
| /** | ||
| * Bridges the scroll view (which sees fresh scroll state at each frame's | ||
| * layout pass) and the anchored chrome (which the layout engine measures | ||
| * before the scroll view is laid out, so its view of scroll state lags one | ||
| * frame). The scroll view computes the unread count and compares it against | ||
| * what the chrome actually rendered, requesting one catch-up frame after any | ||
| * change so the indicator settles deterministically. | ||
| */ | ||
| export interface UnreadOutputFeed { | ||
| /** Lines appended since the user left the bottom, as of the latest layout. */ | ||
| readonly current: () => number; | ||
| /** Called by the chrome each frame with the count it rendered. */ | ||
| readonly present: (unreadLines: number) => void; | ||
| } | ||
|
|
||
| /** | ||
| * Counts transcript lines appended while the user is scrolled away from the | ||
| * bottom — the "unread / new output" signal for the anchored-composer trial. | ||
| * | ||
| * Updated once per frame with the current window snapshot: growth accumulates | ||
| * while the user is away, arriving at the bottom clears the count. Shrinks | ||
| * (collapsing tool output, re-wraps) never manufacture unread lines; the | ||
| * counter is approximate by design — it is an attention hint, not an exact | ||
| * diff. | ||
| */ | ||
| export class UnreadOutputCounter { | ||
| private lastDocumentLines: number | undefined; | ||
| private unreadLines = 0; | ||
|
|
||
| update(window: TranscriptWindowSnapshot): number { | ||
| if ( | ||
| !window.followingEnd && | ||
| this.lastDocumentLines !== undefined && | ||
| window.documentLines > this.lastDocumentLines | ||
| ) { | ||
| this.unreadLines += window.documentLines - this.lastDocumentLines; | ||
| } | ||
| if (window.followingEnd) this.unreadLines = 0; | ||
| this.lastDocumentLines = window.documentLines; | ||
| return this.unreadLines; | ||
| } | ||
| } | ||
|
|
||
| /** The rendered unread line: accent-colored, one row, empty when nothing is new. */ | ||
| export function renderUnreadIndicator( | ||
| unreadLines: number, | ||
| accent: (text: string) => string, | ||
| ): string[] { | ||
| if (unreadLines <= 0) return []; | ||
| const noun = unreadLines === 1 ? 'line' : 'lines'; | ||
| return [accent(`↓ ${unreadLines} new ${noun} — End to jump to latest`)]; | ||
| } | ||
|
|
||
| /** | ||
| * URL schemes a model-authored OSC 8 link may be opened with, mirroring the | ||
| * desktop's external-link guard (apps/desktop/src/main/external-link-guard.ts): | ||
| * web and mail only. Assistant Markdown is rendered with the raw href, so | ||
| * everything else — `file:`, `javascript:`, unknown handlers, UNC paths — | ||
| * must never reach an OS opener from a click. | ||
| */ | ||
| const OPENABLE_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']); | ||
|
|
||
| export function isOpenableExternalUrl(url: string): boolean { | ||
| try { | ||
| return OPENABLE_URL_PROTOCOLS.has(new URL(url).protocol); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Opens an OSC 8 hyperlink activated by a primary-button click in the | ||
| * fullscreen viewport. Model-authored hrefs are untrusted input, so the | ||
| * opener is deliberately narrow: | ||
| * | ||
| * - Only `http:`, `https:`, and `mailto:` targets are handed off at all. | ||
| * - Windows never routes the URL through cmd.exe — `spawn`'s argument | ||
| * quoting does not escape shell metacharacters (`&` would start a second | ||
| * command under `cmd /c start`), so the opener is `rundll32 | ||
| * url.dll,FileProtocolHandler`, which receives the URL as a single argv | ||
| * element and hands it to ShellExecute. The DLL/entrypoint half of the | ||
| * command line is a compile-time constant, so a hostile URL cannot | ||
| * redirect it. | ||
| * - macOS/Linux openers take the URL as a plain argv element (no shell). | ||
| * | ||
| * Failures are swallowed — a dead link must never take the TUI down. | ||
| */ | ||
| /** | ||
| * Spawns a detached, fire-and-forget opener process. `spawn` reports a | ||
| * missing binary (and other spawn failures) asynchronously via the child's | ||
| * `error` event — with no listener attached, Node re-emits it as an | ||
| * uncaughtException, which the TUI's handler treats as fatal and begins | ||
| * session teardown. The child is therefore kept and its error swallowed: a | ||
| * dead link must degrade to "nothing opened", never end the session. | ||
| * Synchronous throws (invalid arguments) are swallowed here as well. | ||
| */ | ||
| function spawnDetached( | ||
| spawnProcess: typeof spawn, | ||
| command: string, | ||
| args: string[], | ||
| windowsHide = false, | ||
| ): void { | ||
| try { | ||
| const child = spawnProcess(command, args, { | ||
| detached: true, | ||
| stdio: 'ignore', | ||
| ...(windowsHide ? { windowsHide: true } : {}), | ||
| }); | ||
| child.on('error', () => {}); | ||
| child.unref(); | ||
| } catch { | ||
| // Best-effort only; the terminal may also offer its own link handling. | ||
| } | ||
| } | ||
|
|
||
| export function openExternalUrl( | ||
| url: string, | ||
| platform: NodeJS.Platform = process.platform, | ||
| spawnProcess: typeof spawn = spawn, | ||
| ): void { | ||
| if (!isOpenableExternalUrl(url)) return; | ||
| if (platform === 'darwin') { | ||
| spawnDetached(spawnProcess, 'open', [url]); | ||
| return; | ||
| } | ||
| if (platform === 'win32') { | ||
| // Never cmd.exe: `spawn`'s argument quoting does not escape shell | ||
| // metacharacters, and `cmd /c start` would let a model-authored `&` | ||
| // start a second command. rundll32 receives the URL as a single argv | ||
| // element and hands it to ShellExecute; the DLL/entrypoint half is a | ||
| // compile-time constant, so a hostile URL cannot redirect what runs. | ||
| spawnDetached(spawnProcess, 'rundll32', ['url.dll,FileProtocolHandler', url], true); | ||
| return; | ||
| } | ||
| spawnDetached(spawnProcess, 'xdg-open', [url]); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.