Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,8 @@ The interactive TUI uses regular scrollback output by default. Set
`ui.tuiMode` to `"fullscreen"` to use the terminal's alternate screen with an
independently scrollable transcript, a fixed composer, and mouse-wheel/
scrollbar navigation. The composer remains available while older transcript
content is being reviewed.
content is being reviewed. The scrollbar is hidden when the transcript fits,
then appears briefly while scrolling and follows the active dark/light theme.

```json
{
Expand Down
7 changes: 5 additions & 2 deletions packages/zcode-tui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1070,7 +1070,10 @@ class ZCodeTui {
follow: "end",
primary: true,
overscroll: "chain",
scrollbar: "always"
// Keep the chrome quiet until the transcript actually overflows and
// the user scrolls. This is the default behavior used by pi-agent.
scrollbar: "auto",
scrollbarStyle: this.theme.scrollbarThumb
});
this.fullscreenLayout ??= new VStack([
{ component: this.fullscreenHeader, basis: 1, shrink: 0, minSize: 1 },
Expand Down Expand Up @@ -3908,7 +3911,7 @@ class ZCodeTui {
{
value: "fullscreen",
label: "Fullscreen",
description: "Alternate screen with scrollable transcript and scrollbars"
description: "Alternate screen with a fixed composer and transient themed scrollbar"
}
],
selectedIndex: this.tuiMode === "fullscreen" ? 1 : 0
Expand Down
6 changes: 6 additions & 0 deletions packages/zcode-tui/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface ThemePalette {
quote: string;
toolPendingSurface: string;
toolErrorSurface: string;
scrollbarThumb: string;
diffAddedLine: string;
diffRemovedLine: string;
diffHunkLine: string;
Expand All @@ -47,6 +48,7 @@ const palettes: Record<ZCodeColorScheme, ThemePalette> = {
quote: "38;5;109",
toolPendingSurface: "38;5;252;48;5;236",
toolErrorSurface: "38;5;252;48;5;52",
scrollbarThumb: "48;5;238",
diffAddedLine: "38;5;120;48;5;22",
diffRemovedLine: "38;5;210;48;5;52",
diffHunkLine: "38;5;159;48;5;24",
Expand All @@ -64,6 +66,7 @@ const palettes: Record<ZCodeColorScheme, ThemePalette> = {
quote: "38;5;24",
toolPendingSurface: "38;5;58;48;5;230",
toolErrorSurface: "38;5;236;48;5;224",
scrollbarThumb: "48;5;252",
diffAddedLine: "38;5;22;48;5;194",
diffRemovedLine: "38;5;88;48;5;224",
diffHunkLine: "38;5;24;48;5;189",
Expand Down Expand Up @@ -91,6 +94,7 @@ export interface ZCodeTheme {
bold: (text: string) => string;
toolPendingBackground: (text: string) => string;
toolErrorBackground: (text: string) => string;
scrollbarThumb: (text: string) => string;
diffAddedLine: (text: string) => string;
diffRemovedLine: (text: string) => string;
diffHunkLine: (text: string) => string;
Expand Down Expand Up @@ -120,6 +124,7 @@ export function createTheme(enabled: boolean, initialColorScheme: ZCodeColorSche
const quote = paletteStyle(state, "quote", enabled);
const toolPendingBackground = paletteStyle(state, "toolPendingSurface", enabled);
const toolErrorBackground = paletteStyle(state, "toolErrorSurface", enabled);
const scrollbarThumb = paletteStyle(state, "scrollbarThumb", enabled);
const diffAddedLine = paletteStyle(state, "diffAddedLine", enabled);
const diffRemovedLine = paletteStyle(state, "diffRemovedLine", enabled);
const diffHunkLine = paletteStyle(state, "diffHunkLine", enabled);
Expand Down Expand Up @@ -154,6 +159,7 @@ export function createTheme(enabled: boolean, initialColorScheme: ZCodeColorSche
bold,
toolPendingBackground,
toolErrorBackground,
scrollbarThumb,
diffAddedLine,
diffRemovedLine,
diffHunkLine,
Expand Down
8 changes: 8 additions & 0 deletions scripts/smoke-tui-fullscreen-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,12 @@ async function runPhase(options: { copyOnSelect: boolean }): Promise<void> {
const timeout = setTimeout(() => child.kill("SIGKILL"), 20_000);
let failure: unknown;
try {
const startupOutputStart = output.length;
await waitFor(/alpha\/model/i);
const startupRows = screenRows();
if (/\x1b\[(?:100|48;5;(?:238|252))m/u.test(output.slice(startupOutputStart))) {
throw new Error(`Fullscreen startup rendered a scrollbar thumb before scrolling.\n${startupRows.join("\n")}`);
}
if (startupRows.some((row) => row.includes("SYSTEM INITIATED"))) {
throw new Error(`Fullscreen header used the wide banner unexpectedly.\n${startupRows.join("\n")}`);
}
Expand Down Expand Up @@ -126,9 +130,13 @@ async function runPhase(options: { copyOnSelect: boolean }): Promise<void> {
terminal.write("long transcript\r");
await waitFor(/transcript line 80/i, turnStart);
const beforeRows = screenRows();
const scrollOutputStart = output.length;
terminal.write("\x1b[5~");
await Bun.sleep(100);
const rows = screenRows();
if (!process.env.NO_COLOR && !/\x1b\[48;5;(?:238|252)m/u.test(output.slice(scrollOutputStart))) {
throw new Error(`Fullscreen transcript scroll did not render the themed scrollbar thumb.\n${rows.join("\n")}`);
}
const firstTranscript = (lines: string[]): number => {
const line = lines.find((value) => /^ transcript line \d+$/u.test(value));
return line ? Number(line.match(/\d+/u)?.[0] ?? 0) : 0;
Expand Down
10 changes: 10 additions & 0 deletions test/theme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ describe("TUI terminal theme", () => {
expect(theme.select.selectedText("selected")).toContain("\x1b[38;5;25m");
});

test("keeps fullscreen scrollbar thumbs aligned with the active theme", () => {
const theme = createTheme(true, "dark");
expect(theme.scrollbarThumb(" ")).toStartWith("\x1b[48;5;238m");

theme.setColorScheme("light");
expect(theme.scrollbarThumb(" ")).toStartWith("\x1b[48;5;252m");

expect(createTheme(false).scrollbarThumb(" ")).toBe(" ");
});

test("never relies on the terminal default foreground for strong text", () => {
const theme = createTheme(true, "dark");
expect(theme.bold("strong")).toStartWith("\x1b[1;38;5;252m");
Expand Down