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); + }); +});