From befcceba6e47ec28493e8a94076accd61dee9fbb Mon Sep 17 00:00:00 2001 From: Gautham Venkatasubramanian Date: Sat, 1 Aug 2026 15:49:51 -0700 Subject: [PATCH] fix(tui): render correctly under Windows Terminal / WSL Detect WT_SESSION as a truecolor terminal in detectTerminalId so TERMINAL.trueColor agrees with the color-mode and synchronized-output detectors, fixing dull/quantized markdown swatches, LaTeX colors, and the welcome brand gradient under WSL where COLORTERM is not forwarded. Default tui.scrollbackRebuild on for ConPTY hosts (native Windows and WSL) so a scrolled-off live preview no longer leaves a duplicate above its committed final form; an explicit setting still overrides. Suppress SGR 3 (italic) under screen-family terminfo. GNU screen and tmux's screen-256color default lack `sitm`, so the multiplexer substitutes standout/reverse and paints a solid inverse block over the welcome tip, thinking traces, blockquotes, and markdown emphasis. Resolve TERMINAL.italic at runtime (with an explicit user override) and route theme italics through it, emitting plain text where slanted text is unavailable. Signed-off-by: Gautham Venkatasubramanian --- docs/aura/FORK.md | 1 + packages/coding-agent/CHANGELOG.md | 2 + .../src/config/settings-schema.ts | 2 +- .../src/modes/interactive-mode.ts | 11 ++- .../coding-agent/src/modes/theme/theme.ts | 5 ++ packages/tui/CHANGELOG.md | 5 ++ packages/tui/src/terminal-capabilities.ts | 72 +++++++++++++++++++ .../tui/test/terminal-capabilities.test.ts | 54 ++++++++++++++ 8 files changed, 149 insertions(+), 3 deletions(-) diff --git a/docs/aura/FORK.md b/docs/aura/FORK.md index f5c57b0d3f2..a26a8a43f49 100644 --- a/docs/aura/FORK.md +++ b/docs/aura/FORK.md @@ -88,6 +88,7 @@ and after every upstream merge. | `packages/coding-agent/src/cli/commands/init-xdg.ts` | removed the local `APP_NAME = "omp"` shadow; XDG dirs and hint text use the branded constant from pi-utils | | `packages/coding-agent/src/cli/profile-alias.ts` | alias command, fish conf.d filename, `--wraps`, and rc-file block markers all derive from `APP_NAME` (imported from the `/dirs` subpath to avoid eager env load) | | Display name "Oh My Pi" → "Aura" | `packages/tui/src/desktop-notify.ts`, `tui/src/terminal-capabilities.ts` (CMUX + OSC-99 titles), `modes/controllers/event-controller.ts`, `debug/index.ts`, `tools/ask.ts`, `dap/session.ts` (clientName), `modes/acp/acp-agent.ts`, `commands/acp.ts`, `prompts/system/system-prompt.md` ("Aura coding harness"), `live/prompts/live-instructions.md` ("Aura Live"), `session/agent-session.ts` power reason, `packages/ai/src/registry/oauth/oauth.html`, and Rust `crates/pi-natives/src/{power,crash_handler,lib}.rs` (crash logs land under `.aura`) | +| Windows Terminal / WSL / tmux rendering (`packages/tui/src/terminal-capabilities.ts`, `packages/coding-agent/src/modes/{interactive-mode.ts,theme/theme.ts}`, `packages/coding-agent/src/config/settings-schema.ts`) | aura hardening for the common WSL setup: `detectTerminalId` treats `WT_SESSION` as truecolor (Windows Terminal never emits `COLORTERM`; WSLENV forwards only `WT_SESSION`), `tui.scrollbackRebuild` defaults on for ConPTY hosts, and a new `TERMINAL.italic` capability (`shouldEnableItalicByDefault` / `italicUserOverride`, `PI_FORCE_ITALIC`/`PI_NO_ITALIC`) suppresses SGR 3 under screen-family terminfo (no `sitm`) so tmux/screen no longer paint italic as reverse-video blocks. `theme.italic` and the HUD-note marker in `interactive-mode.ts` gate on it | | Release pipeline | `scripts/ci-release-build-binaries.ts` outfiles templated `${APP_NAME}--`; `.github/workflows/ci.yml` matrix/codesign paths and `release_brew` (elide-dev/homebrew-tap, `Formula/aura.rb`); `scripts/ci-update-brew-formula.ts` (repo `elide-dev/BREAKDANCE`, class `Aura`, installs `aura`); `scripts/install.sh` + `scripts/install.ps1` (fork repo, `aura-*` assets, installs `aura`); `packages/metaharness/src/launch-args.ts` prebuilt names. Asset basenames must stay equal to update-cli's `getBinaryName()` output | | `AGENTS.md` | appended the `## Aura fork conventions` section (points contributors at this file, states the runtime naming rule, locates specs/plans) | | `biome.json` | excludes checked-in `capnp-es` runtime protocol bindings from source formatting/lint; generated output is verified byte-for-byte by `scripts/sync-embedded-runtime-protocol.ts --check` instead | diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 02c76b89746..6ff62284989 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -26,6 +26,8 @@ - Fixed Kotlin runtime execution missing the bundled standard library, and made Auto runtime selection try Java/Kotlin in-process before falling back when the embedded library does not support them yet. - Fixed Python path execution through process and embedded runtime adapters omitting `__file__`, script argv identity, and sibling import roots. - Fixed isolated and containerized Aura launches ignoring the requested runtime adapter by adding the validated `AURA_RUNTIME_ADAPTER` process override. +- Fixed duplicated/leftover scrollback rows under Windows Terminal and WSL by defaulting `tui.scrollbackRebuild` on for ConPTY hosts (native Windows and WSL); a scrolled-off live preview otherwise remained in history with the final block appended below. An explicit setting still overrides the per-host default. +- Fixed the `Tip:` line and other italicized UI (thinking traces, blockquotes, markdown emphasis) rendering as reverse-video highlight blocks under tmux/screen inside Windows Terminal. `theme.italic` now emits plain text when the terminal lacks `sitm` (screen-family terminfo, where the multiplexer substitutes standout for SGR 3), keeping the theme colors readable instead of painting solid inverse bars. ### Removed diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 8d8855a49e3..7d06fc94225 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -1077,7 +1077,7 @@ export const SETTINGS_SCHEMA = { group: "Display", label: "Rewrite Scrollback", description: - "Erase and replay terminal scrollback when a block's final form replaces its live preview. When off (default), stale preview copies remain in history and the final content is appended below.", + "Erase and replay terminal scrollback when a block's final form replaces its live preview. Defaults on for Windows/WSL (ConPTY) terminals, where a stale preview copy otherwise remains in history with the final content appended below; off elsewhere. Set explicitly to override the per-host default.", }, }, diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index d5e42ee4291..f6f7866ac75 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -38,6 +38,7 @@ import { visibleWidth, } from "@oh-my-pi/pi-tui"; import type { TerminalAppearanceRequestToken } from "@oh-my-pi/pi-tui/terminal"; +import { isConPTYHosted } from "@oh-my-pi/pi-tui/terminal"; import { isInsideTerminalMultiplexer } from "@oh-my-pi/pi-tui/terminal-capabilities"; import { $env, @@ -319,7 +320,7 @@ function formatHudNoteMarker(count: number): string { .split("") .map(d => HUD_NOTE_SUP_DIGITS[d] ?? d) .join(""); - return theme.fg("dim", chalk.italic(` \u207a${sub}`)); + return theme.fg("dim", theme.italic(` \u207a${sub}`)); } type GoalSubcommand = "set" | "show" | "pause" | "resume" | "drop" | "budget"; @@ -715,7 +716,13 @@ export class InteractiveMode implements InteractiveModeContext { setMarkdownMermaidRendering(settings.get("tui.renderMermaid")); this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor")); this.ui.setMaxInlineImages(settings.get("tui.maxInlineImages")); - this.ui.setScrollbackRebuild(settings.get("tui.scrollbackRebuild")); + // Scrollback rewrite defaults on for ConPTY hosts (native Windows and WSL), + // where a scrolled-off live preview otherwise leaves a duplicate above its + // committed final form (Windows Terminal viewport-follow). Elsewhere it stays + // off. An explicit user setting always wins. + this.ui.setScrollbackRebuild( + settings.isConfigured("tui.scrollbackRebuild") ? settings.get("tui.scrollbackRebuild") : isConPTYHosted(), + ); // OSC 66 text-sizing is Kitty-only; resolve the setting against the terminal's // capability (`TERMINAL.textSizing` defaults on for Kitty) so it stays off // unless the user opts in, and never emits raw escapes on other terminals. diff --git a/packages/coding-agent/src/modes/theme/theme.ts b/packages/coding-agent/src/modes/theme/theme.ts index 18fe03d3649..41e926eebe9 100644 --- a/packages/coding-agent/src/modes/theme/theme.ts +++ b/packages/coding-agent/src/modes/theme/theme.ts @@ -18,6 +18,7 @@ import type { Terminal, TerminalAppearance, } from "@oh-my-pi/pi-tui"; +import { TERMINAL } from "@oh-my-pi/pi-tui"; import { adjustHsv, colorLuma, getCustomThemesDir, isEnoent, logger, relativeLuminance } from "@oh-my-pi/pi-utils"; import { type } from "arktype"; import chalk from "chalk"; @@ -1631,6 +1632,10 @@ export class Theme { } italic(text: string): string { + // Screen-family terminfo (GNU screen, tmux's default screen-256color) + // lacks `sitm`, so the multiplexer substitutes standout/reverse for + // SGR 3 and paints a solid inverse block. Emit plain text there instead. + if (!TERMINAL.italic) return text; return chalk.italic(text); } diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index ee822e5d543..d5498588728 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Fixed + +- Fixed dull, inconsistent colors under Windows Terminal and WSL by detecting `WT_SESSION` as a truecolor terminal in `detectTerminalId`. Windows Terminal never sets `COLORTERM` (and WSLENV forwards only `WT_SESSION`), so `TERMINAL.trueColor` was `false` while the rest of the UI assumed truecolor, quantizing markdown color swatches, LaTeX colors, and the welcome brand gradient to 256-color. +- Fixed italic text (the welcome tip, thinking traces, blockquotes, and markdown emphasis) rendering as a solid reverse-video block under tmux/screen inside Windows Terminal. Screen-family terminfo (`screen-256color`, tmux's historical `default-terminal`) lacks `sitm`, so the multiplexer substitutes the standout attribute for SGR 3. `TERMINAL.italic` (resolved via `shouldEnableItalicByDefault`) is now off for screen-family / GNU screen sessions so italic degrades to plain text; `PI_FORCE_ITALIC=1` / `PI_NO_ITALIC=1` override. + ## [17.2.2] - 2026-07-31 ### Added diff --git a/packages/tui/src/terminal-capabilities.ts b/packages/tui/src/terminal-capabilities.ts index b16f07f0e7c..d56bbd179ab 100644 --- a/packages/tui/src/terminal-capabilities.ts +++ b/packages/tui/src/terminal-capabilities.ts @@ -112,6 +112,13 @@ export class TerminalInfo { * (macOS narrow, otherwise UAX#11). */ public readonly hangulJamoWidth: HangulCompatibilityJamoWidth = "platform", + /** + * Emits SGR 3 (italic) as slanted text rather than a substituted + * attribute. Runtime-resolved: screen-family terminfo lacks `sitm`, so + * tmux/screen paint standout (reverse video) instead — see + * {@link shouldEnableItalicByDefault}. + */ + public readonly italic: boolean = true, ) {} /** @@ -429,6 +436,53 @@ export function shouldEnableHyperlinksByDefault( return true; } +/** + * Resolve an explicit user override for SGR 3 (italic). Returns `false` for an + * opt-out, `true` for a force-on, or `null` when the user has no preference. + * Opt-out beats force-on, mirroring {@link hyperlinksUserOverride}. + */ +export function italicUserOverride(env: NodeJS.ProcessEnv = Bun.env): boolean | null { + if (env.PI_NO_ITALIC === "1") return false; + if (env.PI_FORCE_ITALIC === "1") return true; + return null; +} + +/** + * Whether SGR 3 (italic) should be emitted at all. + * + * Italic reaches the screen only when the active terminfo carries `sitm`. + * GNU `screen` and tmux's historical `screen-256color` default-terminal do + * not: tmux/screen then substitute the standout attribute, so every italic run + * paints as a solid reverse-video block (a purple/gray highlight on the welcome + * tip, thinking traces, and markdown emphasis) instead of slanted text. Under + * WSL + Windows Terminal this is the common tmux default. Degrade to plain text + * in that case so the color still reads. + * + * Policy (highest precedence first): + * 1. Explicit override (`PI_NO_ITALIC=1` off, `PI_FORCE_ITALIC=1` on). + * 2. Static terminal capability (`TerminalInfo.italic`). + * 3. GNU screen marker (`STY`) — screen-family terminfo lacks `sitm`. + * 4. `TERM=screen*` (tmux's historical default or bare screen) — lacks `sitm`. + * 5. Otherwise on: `tmux-256color`, `xterm*`, and modern direct terminals + * all advertise `sitm`. A user who forces italic on a sitm-less TERM can + * use `PI_FORCE_ITALIC=1`. + */ +export function shouldEnableItalicByDefault( + env: NodeJS.ProcessEnv = Bun.env, + terminalId: TerminalId = TERMINAL_ID, +): boolean { + const override = italicUserOverride(env); + if (override !== null) return override; + + if (!getTerminalInfo(terminalId).italic) return false; + + if (env.STY) return false; + const term = env.TERM?.toLowerCase() ?? ""; + if (term.startsWith("screen")) return false; + + return true; +} + function getFallbackImageProtocol(terminalId: TerminalId): ImageProtocol | null { if (!process.stdout.isTTY) return null; if (terminalId === "vscode" || terminalId === "alacritty") return null; @@ -501,6 +555,7 @@ export function detectTerminalId(env: NodeJS.ProcessEnv = Bun.env): TerminalId { TERM_PROGRAM, TERM, COLORTERM, + WT_SESSION, } = env; if (KITTY_WINDOW_ID) return "kitty"; @@ -525,6 +580,12 @@ export function detectTerminalId(env: NodeJS.ProcessEnv = Bun.env): TerminalId { if (COLORTERM) { if (caseEq(COLORTERM, "truecolor") || caseEq(COLORTERM, "24bit")) return "trueColor"; } + // Windows Terminal advertises 24-bit color but sets neither COLORTERM (which + // it never emits) nor a TERM_PROGRAM marker, and under WSL only WT_SESSION is + // forwarded through WSLENV. Treat its presence as truecolor so TERMINAL.trueColor + // agrees with the coding-agent color-mode and synchronized-output detectors, + // which already key off WT_SESSION. + if (WT_SESSION) return "trueColor"; return "base"; } @@ -542,6 +603,7 @@ export interface RuntimeTerminal extends TerminalInfo { deccara: boolean; supportsScreenToScrollback: boolean; textSizing: boolean; + italic: boolean; } export const TERMINAL: RuntimeTerminal = (() => { @@ -570,6 +632,11 @@ export const TERMINAL: RuntimeTerminal = (() => { // ignores DECCARA) exercises the padded-string fallback. Integration tests opt // in explicitly through setTerminalDeccara. resolved.deccara = detectRectangularSgrSupport(resolved.id, Bun.env) && !isBunTestRuntime(); + // Italic (SGR 3). Emitted directly by modern terminals; suppressed under + // screen-family terminfo (GNU screen, tmux's historical screen-256color + // default) where tmux/screen substitute standout/reverse for the missing + // `sitm` and paint a solid inverse block instead of slanted text. + resolved.italic = shouldEnableItalicByDefault(Bun.env, resolved.id); return resolved; })(); @@ -609,6 +676,11 @@ export function setTerminalTextSizing(enabled: boolean): void { TERMINAL.textSizing = enabled; } +/** Override italic (SGR 3) emission at runtime; tests flip it directly. */ +export function setTerminalItalic(enabled: boolean): void { + TERMINAL.italic = enabled; +} + export function getTerminalInfo( terminalId: TerminalId, platform: NodeJS.Platform = process.platform, diff --git a/packages/tui/test/terminal-capabilities.test.ts b/packages/tui/test/terminal-capabilities.test.ts index 2852b7f2d1f..daaecf20331 100644 --- a/packages/tui/test/terminal-capabilities.test.ts +++ b/packages/tui/test/terminal-capabilities.test.ts @@ -4,9 +4,11 @@ import { getTerminalInfo, hyperlinksUserOverride, ImageProtocol, + italicUserOverride, NotifyProtocol, resolveWarpImageProtocol, shouldEnableHyperlinksByDefault, + shouldEnableItalicByDefault, shouldEnableSynchronizedOutputByDefault, synchronizedOutputUserOverride, } from "@oh-my-pi/pi-tui/terminal-capabilities"; @@ -21,6 +23,12 @@ describe("detectTerminalId", () => { expect(detectTerminalId(env)).toBe("trueColor"); }); + + it("treats Windows Terminal / WSL as truecolor via WT_SESSION when COLORTERM is absent", () => { + // Windows Terminal never sets COLORTERM, and WSLENV forwards only WT_SESSION, + // so the true-color fallback must key off WT_SESSION rather than COLORTERM. + expect(detectTerminalId({ WT_SESSION: "abc", TERM: "xterm-256color" })).toBe("trueColor"); + }); }); describe("synchronizedOutputUserOverride", () => { @@ -373,3 +381,49 @@ describe("shouldEnableHyperlinksByDefault", () => { expect(shouldEnableHyperlinksByDefault({ PI_FORCE_HYPERLINKS: "1", STY: "1.pts-0" }, "kitty")).toBe(true); }); }); + +describe("italicUserOverride", () => { + it("returns null when neither override is set", () => { + expect(italicUserOverride({})).toBeNull(); + expect(italicUserOverride({ TERM: "screen-256color" })).toBeNull(); + }); + + it("returns true for the force-on flag and false for the opt-out", () => { + expect(italicUserOverride({ PI_FORCE_ITALIC: "1" })).toBe(true); + expect(italicUserOverride({ PI_NO_ITALIC: "1" })).toBe(false); + }); + + it("resolves opt-out ahead of force-on when both are set", () => { + expect(italicUserOverride({ PI_NO_ITALIC: "1", PI_FORCE_ITALIC: "1" })).toBe(false); + }); +}); + +describe("shouldEnableItalicByDefault", () => { + it("enables italic on modern direct terminals and sitm-carrying TERMs", () => { + expect(shouldEnableItalicByDefault({ TERM: "xterm-256color" }, "kitty")).toBe(true); + expect(shouldEnableItalicByDefault({ TERM: "tmux-256color", TMUX: "1" }, "trueColor")).toBe(true); + expect(shouldEnableItalicByDefault({}, "base")).toBe(true); + }); + + it("disables italic under screen-family terminfo (no sitm -> tmux/screen paint reverse video)", () => { + // tmux's historical default-terminal is screen-256color, so tmux inside + // Windows Terminal substitutes standout for SGR 3 and paints a solid + // inverse block instead of slanted text. + expect(shouldEnableItalicByDefault({ TERM: "screen-256color", TMUX: "1" }, "trueColor")).toBe(false); + expect(shouldEnableItalicByDefault({ TERM: "screen" }, "base")).toBe(false); + }); + + it("disables italic when GNU screen is in the path", () => { + expect(shouldEnableItalicByDefault({ STY: "1234.pts-0.host", TERM: "xterm-256color" }, "kitty")).toBe(false); + }); + + it("lets PI_FORCE_ITALIC override a sitm-less TERM", () => { + expect( + shouldEnableItalicByDefault({ PI_FORCE_ITALIC: "1", TERM: "screen-256color", TMUX: "1" }, "trueColor"), + ).toBe(true); + }); + + it("lets PI_NO_ITALIC force italic off on a capable terminal", () => { + expect(shouldEnableItalicByDefault({ PI_NO_ITALIC: "1", TERM: "xterm-256color" }, "kitty")).toBe(false); + }); +});