From 4533db309b4bb832282979c02658f8d9fdefba61 Mon Sep 17 00:00:00 2001 From: Nima Naderi Date: Fri, 29 May 2026 10:21:22 +0000 Subject: [PATCH 1/9] style(cli): polish matrix tui launcher --- .../src/cli/tui/views/CommandPalette.tsx | 25 ++++-- .../src/cli/tui/views/HomeView.tsx | 80 +++++++++++++++---- .../sync-client/src/cli/tui/views/Mascot.tsx | 33 +++++++- .../tests/tui/accessibility.test.tsx | 2 +- .../tests/tui/command-palette.test.tsx | 16 +++- .../tests/tui/home-render.test.tsx | 26 ++++-- 6 files changed, 147 insertions(+), 35 deletions(-) diff --git a/packages/sync-client/src/cli/tui/views/CommandPalette.tsx b/packages/sync-client/src/cli/tui/views/CommandPalette.tsx index 72e97dc8f..d7348a527 100644 --- a/packages/sync-client/src/cli/tui/views/CommandPalette.tsx +++ b/packages/sync-client/src/cli/tui/views/CommandPalette.tsx @@ -2,6 +2,14 @@ import React from "react"; import { Box, Text } from "ink"; import type { TuiAction } from "../actions.js"; +function actionDescription(action: TuiAction): string { + return action.intents[0] ?? action.aliases[0] ?? action.group; +} + +function paddedTitle(title: string): string { + return title.length >= 34 ? `${title} ` : title.padEnd(34); +} + export function CommandPalette({ results, query, @@ -14,15 +22,20 @@ export function CommandPalette({ noColor?: boolean; }) { return ( - - Command Palette - {`/${query}`} + + + MATRIX COMMANDS + esc closes + + {`/${query}`} {results.map((action, index) => ( - + - {index === selectedIndex ? "> " : " "}{action.title} + {index === selectedIndex ? "> " : " "}{paddedTitle(action.title)}{action.group} + + + {" "}{actionDescription(action)} - {action.group} ))} {results.length === 0 && No commands found} diff --git a/packages/sync-client/src/cli/tui/views/HomeView.tsx b/packages/sync-client/src/cli/tui/views/HomeView.tsx index ba115752c..dcc93ec8b 100644 --- a/packages/sync-client/src/cli/tui/views/HomeView.tsx +++ b/packages/sync-client/src/cli/tui/views/HomeView.tsx @@ -3,6 +3,14 @@ import { Box, Text } from "ink"; import { Mascot } from "./Mascot.js"; import type { TuiStatusSnapshot } from "../status.js"; +const WORDMARK = [ + "M M A TTTTT RRRR III X X OOO SSS", + "MM MM A A T R R I X X O O S", + "M M M AAAAA T RRRR I X O O SSS", + "M M A A T R R I X X O O S", + "M M A A T R R III X X OOO SSS", +]; + function stateLabel(snapshot: TuiStatusSnapshot): string { if (snapshot.overall === "unauthenticated") { return "login required"; @@ -10,6 +18,12 @@ function stateLabel(snapshot: TuiStatusSnapshot): string { return snapshot.overall; } +function stateColor(snapshot: TuiStatusSnapshot): "green" | "yellow" | "cyan" { + if (snapshot.overall === "healthy") return "green"; + if (snapshot.overall === "degraded" || snapshot.overall === "unauthenticated") return "yellow"; + return "cyan"; +} + export function HomeView({ snapshot, columns = 80, @@ -20,27 +34,63 @@ export function HomeView({ noColor?: boolean; }) { const narrow = columns < 80; + const wide = columns >= 92; + const stageWidth = Math.max(40, Math.min(columns, wide ? 96 : 76)); const sessionLabel = `${snapshot.sessions.count} ${snapshot.sessions.count === 1 ? "session" : "sessions"}`; const status = `${stateLabel(snapshot)} · ${snapshot.profile.name} · ${snapshot.gateway.label} · ${sessionLabel}`; + const color = noColor ? undefined : stateColor(snapshot); return ( - - - Matrix OS - - - {'Ask Hermes... "review my current PR"'} - Build · Hermes Codex Shell + + + {!narrow ? ( + <> + {WORDMARK.map((line) => ( + {line} + ))} + MATRIX OS + + ) : ( + MATRIX OS + )} - - {"/ commands tab agents s sessions"} - {!narrow && } - - - {status} + + + + + + | + + + {'Ask Hermes... "review my current PR"'} + Build Hermes Codex Shell + / commands tab agents s sessions q quit + + + + + {status} + {snapshot.blockingActions.length > 0 && ( + {`Next: /${snapshot.blockingActions[0]}`} + )} + + {!wide && !narrow && ( + + + + )} + + {wide && ( + + + + )} - {snapshot.blockingActions.length > 0 && ( - {`Next: /${snapshot.blockingActions[0]}`} + + {narrow && ( + + + )} ); diff --git a/packages/sync-client/src/cli/tui/views/Mascot.tsx b/packages/sync-client/src/cli/tui/views/Mascot.tsx index 521ca1642..a20aa36df 100644 --- a/packages/sync-client/src/cli/tui/views/Mascot.tsx +++ b/packages/sync-client/src/cli/tui/views/Mascot.tsx @@ -1,8 +1,33 @@ import React from "react"; -import { Text } from "ink"; +import { Box, Text } from "ink"; import type { TuiOverallState } from "../status.js"; -export function Mascot({ state, noColor }: { state: TuiOverallState; noColor?: boolean }) { - const face = state === "healthy" ? "[::]" : state === "unauthenticated" ? "[??]" : "[!!]"; - return {face}; +const RABBIT_ART = [ + " /\\_/\\", + " ( o.o )", + " > ^ <", + " /| MATRIX |\\", + " /_|__OS___|_\\", +]; + +function mascotColor(state: TuiOverallState): "green" | "yellow" | "cyan" { + if (state === "healthy") return "green"; + if (state === "degraded" || state === "unauthenticated") return "yellow"; + return "cyan"; +} + +export function Mascot({ state, noColor, compact = false }: { state: TuiOverallState; noColor?: boolean; compact?: boolean }) { + const color = noColor ? undefined : mascotColor(state); + + if (compact) { + return {"rabbit: /\\_/\\"}; + } + + return ( + + {RABBIT_ART.map((line) => ( + {line} + ))} + + ); } diff --git a/packages/sync-client/tests/tui/accessibility.test.tsx b/packages/sync-client/tests/tui/accessibility.test.tsx index e1c74af0f..cc8165b35 100644 --- a/packages/sync-client/tests/tui/accessibility.test.tsx +++ b/packages/sync-client/tests/tui/accessibility.test.tsx @@ -27,7 +27,7 @@ describe("TUI accessibility rendering", () => { expect(home).toContain("degraded"); expect(home).toContain("gateway degraded"); - expect(palette).toContain("Command Palette"); + expect(palette).toContain("MATRIX COMMANDS"); expect(`${home}${palette}`).not.toContain("\u001B["); }); }); diff --git a/packages/sync-client/tests/tui/command-palette.test.tsx b/packages/sync-client/tests/tui/command-palette.test.tsx index 3e533eeeb..fefac4f40 100644 --- a/packages/sync-client/tests/tui/command-palette.test.tsx +++ b/packages/sync-client/tests/tui/command-palette.test.tsx @@ -15,10 +15,20 @@ describe("command palette", () => { it("renders filtered commands with the selected row highlighted", () => { const results = searchTuiActions(DEFAULT_TUI_ACTIONS, "session", 8); - const output = renderToString(); + const output = renderToString(); - expect(output).toContain("Command Palette"); - expect(output).toContain("Open shell sessions"); + expect(output).toContain("MATRIX COMMANDS"); + expect(output).toContain("> Open shell sessions"); expect(output).toContain("Shell and Remote Run"); + expect(output).toContain("attach to a session"); + }); + + it("keeps no-color palette readable without ANSI escapes", () => { + const results = searchTuiActions(DEFAULT_TUI_ACTIONS, "doctor", 8); + const output = renderToString(); + + expect(output).toContain("/doctor"); + expect(output).toContain("> Run doctor"); + expect(output).not.toContain("\u001B["); }); }); diff --git a/packages/sync-client/tests/tui/home-render.test.tsx b/packages/sync-client/tests/tui/home-render.test.tsx index debdef191..3cb8457d1 100644 --- a/packages/sync-client/tests/tui/home-render.test.tsx +++ b/packages/sync-client/tests/tui/home-render.test.tsx @@ -17,28 +17,42 @@ const baseSnapshot: TuiStatusSnapshot = { }; describe("HomeView", () => { - it("renders prompt-first Matrix OS home with compact status", () => { + it("renders wide Matrix OS launcher with rabbit art, prompt, shortcuts, and status", () => { const output = renderToString(); - expect(output).toContain("Matrix OS"); + expect(output).toContain("MATRIX OS"); + expect(output).toContain("/\\_/\\"); expect(output).toContain("Ask Hermes"); + expect(output).toContain("q quit"); expect(output).toContain("cloud"); expect(output).toContain("2 sessions"); - expect(output).toContain("/ commands"); + expect(output).toContain("healthy · cloud · ok · 2 sessions"); }); - it("keeps no-color output understandable", () => { + it("keeps no-color home output understandable without ANSI escapes", () => { const output = renderToString(); + expect(output).toContain("MATRIX OS"); + expect(output).toContain("/\\_/\\"); expect(output).toContain("healthy"); expect(output).not.toContain("\u001B["); }); - it("hides mascot and preserves critical text in narrow terminals", () => { + it("keeps large rabbit art readable on normal-width terminals", () => { + const output = renderToString(); + + expect(output).toContain("/| MATRIX |\\"); + expect(output).toContain("Ask Hermes"); + expect(output).toContain("healthy · cloud · ok · 2 sessions"); + }); + + it("hides large art and preserves critical prompt/status text in narrow terminals", () => { const output = renderToString(); + expect(output).toContain("MATRIX OS"); expect(output).toContain("Ask Hermes"); expect(output).toContain("cloud"); - expect(output).not.toContain("rabbit"); + expect(output).toContain("rabbit: /\\_/\\"); + expect(output).not.toContain("/| MATRIX |\\"); }); }); From ba1d353ac55ddef196dc5383758eec284c34c1df Mon Sep 17 00:00:00 2001 From: Nima Naderi Date: Fri, 29 May 2026 10:43:00 +0000 Subject: [PATCH 2/9] fix(cli): address tui review findings --- packages/sync-client/src/cli/tui/app.tsx | 10 +++++++++- .../sync-client/src/cli/tui/views/CommandPalette.tsx | 6 +++++- packages/sync-client/src/cli/tui/views/HomeView.tsx | 4 ++-- packages/sync-client/src/cli/tui/views/Mascot.tsx | 4 ++-- .../sync-client/tests/tui/command-palette.test.tsx | 9 +++++++++ 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/sync-client/src/cli/tui/app.tsx b/packages/sync-client/src/cli/tui/app.tsx index 32d3d64e8..2ea837c6c 100644 --- a/packages/sync-client/src/cli/tui/app.tsx +++ b/packages/sync-client/src/cli/tui/app.tsx @@ -101,7 +101,15 @@ export function MatrixTuiApp({ initialSnapshot, noColor = false }: { initialSnap } if (paletteOpen) { - return ; + return ( + + ); } return ; diff --git a/packages/sync-client/src/cli/tui/views/CommandPalette.tsx b/packages/sync-client/src/cli/tui/views/CommandPalette.tsx index d7348a527..61b3a3312 100644 --- a/packages/sync-client/src/cli/tui/views/CommandPalette.tsx +++ b/packages/sync-client/src/cli/tui/views/CommandPalette.tsx @@ -14,15 +14,19 @@ export function CommandPalette({ results, query, selectedIndex = 0, + columns = 80, noColor = false, }: { results: readonly TuiAction[]; query: string; selectedIndex?: number; + columns?: number; noColor?: boolean; }) { + const width = Math.min(Math.max(1, columns), 76); + return ( - + MATRIX COMMANDS esc closes diff --git a/packages/sync-client/src/cli/tui/views/HomeView.tsx b/packages/sync-client/src/cli/tui/views/HomeView.tsx index dcc93ec8b..4a68cab34 100644 --- a/packages/sync-client/src/cli/tui/views/HomeView.tsx +++ b/packages/sync-client/src/cli/tui/views/HomeView.tsx @@ -45,8 +45,8 @@ export function HomeView({ {!narrow ? ( <> - {WORDMARK.map((line) => ( - {line} + {WORDMARK.map((line, index) => ( + {line} ))} MATRIX OS diff --git a/packages/sync-client/src/cli/tui/views/Mascot.tsx b/packages/sync-client/src/cli/tui/views/Mascot.tsx index a20aa36df..4616c8b1f 100644 --- a/packages/sync-client/src/cli/tui/views/Mascot.tsx +++ b/packages/sync-client/src/cli/tui/views/Mascot.tsx @@ -25,8 +25,8 @@ export function Mascot({ state, noColor, compact = false }: { state: TuiOverallS return ( - {RABBIT_ART.map((line) => ( - {line} + {RABBIT_ART.map((line, index) => ( + {line} ))} ); diff --git a/packages/sync-client/tests/tui/command-palette.test.tsx b/packages/sync-client/tests/tui/command-palette.test.tsx index fefac4f40..0e8e057df 100644 --- a/packages/sync-client/tests/tui/command-palette.test.tsx +++ b/packages/sync-client/tests/tui/command-palette.test.tsx @@ -31,4 +31,13 @@ describe("command palette", () => { expect(output).toContain("> Run doctor"); expect(output).not.toContain("\u001B["); }); + + it("caps palette width to the terminal columns", () => { + const results = searchTuiActions(DEFAULT_TUI_ACTIONS, "session", 8); + const output = renderToString(); + + for (const line of output.split("\n")) { + expect(line.length).toBeLessThanOrEqual(32); + } + }); }); From 67f8af437669ff5351fe9caa80b6c281652c4dd5 Mon Sep 17 00:00:00 2001 From: Nima Naderi Date: Fri, 29 May 2026 10:50:03 +0000 Subject: [PATCH 3/9] fix(cli): tune compact palette spacing --- .../src/cli/tui/views/CommandPalette.tsx | 7 ++++--- .../tests/tui/command-palette.test.tsx | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/sync-client/src/cli/tui/views/CommandPalette.tsx b/packages/sync-client/src/cli/tui/views/CommandPalette.tsx index 61b3a3312..5d4bc28e8 100644 --- a/packages/sync-client/src/cli/tui/views/CommandPalette.tsx +++ b/packages/sync-client/src/cli/tui/views/CommandPalette.tsx @@ -6,8 +6,8 @@ function actionDescription(action: TuiAction): string { return action.intents[0] ?? action.aliases[0] ?? action.group; } -function paddedTitle(title: string): string { - return title.length >= 34 ? `${title} ` : title.padEnd(34); +function paddedTitle(title: string, targetWidth: number): string { + return title.length >= targetWidth ? `${title} ` : title.padEnd(targetWidth); } export function CommandPalette({ @@ -24,6 +24,7 @@ export function CommandPalette({ noColor?: boolean; }) { const width = Math.min(Math.max(1, columns), 76); + const titleColumnWidth = Math.min(34, Math.max(1, Math.floor((width - 4) / 2))); return ( @@ -35,7 +36,7 @@ export function CommandPalette({ {results.map((action, index) => ( - {index === selectedIndex ? "> " : " "}{paddedTitle(action.title)}{action.group} + {index === selectedIndex ? "> " : " "}{paddedTitle(action.title, titleColumnWidth)}{action.group} {" "}{actionDescription(action)} diff --git a/packages/sync-client/tests/tui/command-palette.test.tsx b/packages/sync-client/tests/tui/command-palette.test.tsx index 0e8e057df..fd151d7cf 100644 --- a/packages/sync-client/tests/tui/command-palette.test.tsx +++ b/packages/sync-client/tests/tui/command-palette.test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { renderToString } from "ink"; import { describe, expect, it } from "vitest"; -import { DEFAULT_TUI_ACTIONS } from "../../src/cli/tui/actions.js"; +import { DEFAULT_TUI_ACTIONS, type TuiAction } from "../../src/cli/tui/actions.js"; import { searchTuiActions } from "../../src/cli/tui/palette.js"; import { CommandPalette } from "../../src/cli/tui/views/CommandPalette.js"; @@ -40,4 +40,20 @@ describe("command palette", () => { expect(line.length).toBeLessThanOrEqual(32); } }); + + it("uses narrower title padding in compact palettes", () => { + const compactAction: TuiAction = { + id: "test.open", + title: "Open", + group: "Utility", + aliases: ["open"], + intents: ["open command"], + danger: "none", + handler: "view", + }; + + const output = renderToString(); + + expect(output).toContain("> Open Utility"); + }); }); From 11e43dc27b8df9b8eca703e297e4439856342032 Mon Sep 17 00:00:00 2001 From: Nima Naderi Date: Fri, 29 May 2026 14:11:32 +0200 Subject: [PATCH 4/9] fix(cli): refine tui rabbit mascot --- .../sync-client/src/cli/tui/views/Mascot.tsx | 19 +++++++++++++------ .../tests/tui/home-render.test.tsx | 12 +++++++----- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/sync-client/src/cli/tui/views/Mascot.tsx b/packages/sync-client/src/cli/tui/views/Mascot.tsx index 4616c8b1f..399f4535d 100644 --- a/packages/sync-client/src/cli/tui/views/Mascot.tsx +++ b/packages/sync-client/src/cli/tui/views/Mascot.tsx @@ -3,13 +3,20 @@ import { Box, Text } from "ink"; import type { TuiOverallState } from "../status.js"; const RABBIT_ART = [ - " /\\_/\\", - " ( o.o )", - " > ^ <", - " /| MATRIX |\\", - " /_|__OS___|_\\", + " /\\ /\\", + " /##\\______/##\\", + " /##############\\", + " |### [] [] ###|", + " |### __ ###|", + " __|### MATRIX ###|__", + " /##|### OS ###|##\\", + " |###|##############|###|", + " \\##|__[]______[]__|##/", + " /__/ \\__\\", ]; +const COMPACT_RABBIT = "rabbit: /##\\_[]_/##\\"; + function mascotColor(state: TuiOverallState): "green" | "yellow" | "cyan" { if (state === "healthy") return "green"; if (state === "degraded" || state === "unauthenticated") return "yellow"; @@ -20,7 +27,7 @@ export function Mascot({ state, noColor, compact = false }: { state: TuiOverallS const color = noColor ? undefined : mascotColor(state); if (compact) { - return {"rabbit: /\\_/\\"}; + return {COMPACT_RABBIT}; } return ( diff --git a/packages/sync-client/tests/tui/home-render.test.tsx b/packages/sync-client/tests/tui/home-render.test.tsx index 3cb8457d1..592e09e33 100644 --- a/packages/sync-client/tests/tui/home-render.test.tsx +++ b/packages/sync-client/tests/tui/home-render.test.tsx @@ -21,7 +21,9 @@ describe("HomeView", () => { const output = renderToString(); expect(output).toContain("MATRIX OS"); - expect(output).toContain("/\\_/\\"); + expect(output).toContain("/##\\______/##\\"); + expect(output).toContain("MATRIX"); + expect(output).toContain("OS"); expect(output).toContain("Ask Hermes"); expect(output).toContain("q quit"); expect(output).toContain("cloud"); @@ -33,7 +35,7 @@ describe("HomeView", () => { const output = renderToString(); expect(output).toContain("MATRIX OS"); - expect(output).toContain("/\\_/\\"); + expect(output).toContain("/##\\______/##\\"); expect(output).toContain("healthy"); expect(output).not.toContain("\u001B["); }); @@ -41,7 +43,7 @@ describe("HomeView", () => { it("keeps large rabbit art readable on normal-width terminals", () => { const output = renderToString(); - expect(output).toContain("/| MATRIX |\\"); + expect(output).toContain("|### MATRIX ###|"); expect(output).toContain("Ask Hermes"); expect(output).toContain("healthy · cloud · ok · 2 sessions"); }); @@ -52,7 +54,7 @@ describe("HomeView", () => { expect(output).toContain("MATRIX OS"); expect(output).toContain("Ask Hermes"); expect(output).toContain("cloud"); - expect(output).toContain("rabbit: /\\_/\\"); - expect(output).not.toContain("/| MATRIX |\\"); + expect(output).toContain("rabbit: /##\\_[]_/##\\"); + expect(output).not.toContain("|### MATRIX ###|"); }); }); From 0a234ea273acc218a1653c51c602c394b0e1283d Mon Sep 17 00:00:00 2001 From: Nima Naderi Date: Fri, 29 May 2026 14:21:43 +0200 Subject: [PATCH 5/9] style(cli): use dense ascii rabbit mascot --- .../src/cli/tui/views/HomeView.tsx | 16 ++++---- .../sync-client/src/cli/tui/views/Mascot.tsx | 37 +++++++++++++------ .../tests/tui/home-render.test.tsx | 13 +++---- 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/packages/sync-client/src/cli/tui/views/HomeView.tsx b/packages/sync-client/src/cli/tui/views/HomeView.tsx index 4a68cab34..6033287df 100644 --- a/packages/sync-client/src/cli/tui/views/HomeView.tsx +++ b/packages/sync-client/src/cli/tui/views/HomeView.tsx @@ -35,7 +35,8 @@ export function HomeView({ }) { const narrow = columns < 80; const wide = columns >= 92; - const stageWidth = Math.max(40, Math.min(columns, wide ? 96 : 76)); + const extraWide = columns >= 124; + const stageWidth = Math.max(40, Math.min(columns, extraWide ? 120 : wide ? 96 : 76)); const sessionLabel = `${snapshot.sessions.count} ${snapshot.sessions.count === 1 ? "session" : "sessions"}`; const status = `${stateLabel(snapshot)} · ${snapshot.profile.name} · ${snapshot.gateway.label} · ${sessionLabel}`; const color = noColor ? undefined : stateColor(snapshot); @@ -55,6 +56,12 @@ export function HomeView({ )} + {!narrow && !extraWide && ( + + + + )} + @@ -74,13 +81,8 @@ export function HomeView({ {`Next: /${snapshot.blockingActions[0]}`} )} - {!wide && !narrow && ( - - - - )} - {wide && ( + {extraWide && ( diff --git a/packages/sync-client/src/cli/tui/views/Mascot.tsx b/packages/sync-client/src/cli/tui/views/Mascot.tsx index 399f4535d..08e33e602 100644 --- a/packages/sync-client/src/cli/tui/views/Mascot.tsx +++ b/packages/sync-client/src/cli/tui/views/Mascot.tsx @@ -3,19 +3,34 @@ import { Box, Text } from "ink"; import type { TuiOverallState } from "../status.js"; const RABBIT_ART = [ - " /\\ /\\", - " /##\\______/##\\", - " /##############\\", - " |### [] [] ###|", - " |### __ ###|", - " __|### MATRIX ###|__", - " /##|### OS ###|##\\", - " |###|##############|###|", - " \\##|__[]______[]__|##/", - " /__/ \\__\\", + " .@@.", + " .@@@@.", + " @@@@@", + " .@@. @@@@@.", + " .@@@@oo. @@@@.", + " @@@@@@@o. @@@'", + " '@@@@@@@@o. @@.", + " 'o@@@@@o. @@'", + " '@@@o .@@@@.", + " .@@@@@@@@.", + " @@@@@@@@@@", + " '@@@@@@@'", + "", + " .@@@@oo.o@@@. ..", + " .@@@@o. @@@@@@@@@@@@. .@@.", + " @@@@@@@@@@@@@@@' '' .@@@@@.", + " '@@@@'' '@@@' '' '''''", + "", + " .@@@@@@@@@@o. .@@@. .@@@@@@@o.", + " @@@@@@@@@@@@@@o.@@@@@o@@@@@@@@@@@@.", + " '@@@' '@@@' '@@' '@@' '@@@'", + "", + " .@@@@.", + " @@@@@@@. .@@@@@. .o@@@@@@@@@o. .@@@. .@@@.", + " '@@@@' @@@@@@@.@@@@@@@@@@@@@@.'@@@' '@@@'", ]; -const COMPACT_RABBIT = "rabbit: /##\\_[]_/##\\"; +const COMPACT_RABBIT = "rabbit: .@@. @@@"; function mascotColor(state: TuiOverallState): "green" | "yellow" | "cyan" { if (state === "healthy") return "green"; diff --git a/packages/sync-client/tests/tui/home-render.test.tsx b/packages/sync-client/tests/tui/home-render.test.tsx index 592e09e33..4c235187d 100644 --- a/packages/sync-client/tests/tui/home-render.test.tsx +++ b/packages/sync-client/tests/tui/home-render.test.tsx @@ -21,9 +21,8 @@ describe("HomeView", () => { const output = renderToString(); expect(output).toContain("MATRIX OS"); - expect(output).toContain("/##\\______/##\\"); - expect(output).toContain("MATRIX"); - expect(output).toContain("OS"); + expect(output).toContain(".@@@@oo.o@@@."); + expect(output).toContain("@@@@@@@@@@@@@@@"); expect(output).toContain("Ask Hermes"); expect(output).toContain("q quit"); expect(output).toContain("cloud"); @@ -35,7 +34,7 @@ describe("HomeView", () => { const output = renderToString(); expect(output).toContain("MATRIX OS"); - expect(output).toContain("/##\\______/##\\"); + expect(output).toContain(".@@@@@@@@@@o."); expect(output).toContain("healthy"); expect(output).not.toContain("\u001B["); }); @@ -43,7 +42,7 @@ describe("HomeView", () => { it("keeps large rabbit art readable on normal-width terminals", () => { const output = renderToString(); - expect(output).toContain("|### MATRIX ###|"); + expect(output).toContain(".@@@@oo.o@@@."); expect(output).toContain("Ask Hermes"); expect(output).toContain("healthy · cloud · ok · 2 sessions"); }); @@ -54,7 +53,7 @@ describe("HomeView", () => { expect(output).toContain("MATRIX OS"); expect(output).toContain("Ask Hermes"); expect(output).toContain("cloud"); - expect(output).toContain("rabbit: /##\\_[]_/##\\"); - expect(output).not.toContain("|### MATRIX ###|"); + expect(output).toContain("rabbit: .@@. @@@"); + expect(output).not.toContain(".@@@@oo.o@@@."); }); }); From bebfda3ae7de55f5784f8e5d6320817b300f2b1d Mon Sep 17 00:00:00 2001 From: Nima Naderi Date: Fri, 29 May 2026 14:33:06 +0200 Subject: [PATCH 6/9] style(cli): remove matrix tui wordmark --- .../src/cli/tui/views/HomeView.tsx | 19 +------------------ .../tests/tui/home-render.test.tsx | 1 + 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/sync-client/src/cli/tui/views/HomeView.tsx b/packages/sync-client/src/cli/tui/views/HomeView.tsx index 6033287df..731bb352f 100644 --- a/packages/sync-client/src/cli/tui/views/HomeView.tsx +++ b/packages/sync-client/src/cli/tui/views/HomeView.tsx @@ -3,14 +3,6 @@ import { Box, Text } from "ink"; import { Mascot } from "./Mascot.js"; import type { TuiStatusSnapshot } from "../status.js"; -const WORDMARK = [ - "M M A TTTTT RRRR III X X OOO SSS", - "MM MM A A T R R I X X O O S", - "M M M AAAAA T RRRR I X O O SSS", - "M M A A T R R I X X O O S", - "M M A A T R R III X X OOO SSS", -]; - function stateLabel(snapshot: TuiStatusSnapshot): string { if (snapshot.overall === "unauthenticated") { return "login required"; @@ -44,16 +36,7 @@ export function HomeView({ return ( - {!narrow ? ( - <> - {WORDMARK.map((line, index) => ( - {line} - ))} - MATRIX OS - - ) : ( - MATRIX OS - )} + MATRIX OS {!narrow && !extraWide && ( diff --git a/packages/sync-client/tests/tui/home-render.test.tsx b/packages/sync-client/tests/tui/home-render.test.tsx index 4c235187d..df70597d9 100644 --- a/packages/sync-client/tests/tui/home-render.test.tsx +++ b/packages/sync-client/tests/tui/home-render.test.tsx @@ -21,6 +21,7 @@ describe("HomeView", () => { const output = renderToString(); expect(output).toContain("MATRIX OS"); + expect(output).not.toContain("M M A TTTTT"); expect(output).toContain(".@@@@oo.o@@@."); expect(output).toContain("@@@@@@@@@@@@@@@"); expect(output).toContain("Ask Hermes"); From 059d4f292245689335337190c4d39887584c2cf0 Mon Sep 17 00:00:00 2001 From: Nima Naderi Date: Fri, 29 May 2026 14:36:20 +0200 Subject: [PATCH 7/9] style(cli): use matrix launcher copy --- packages/sync-client/src/cli/tui/views/HomeView.tsx | 4 ++-- packages/sync-client/tests/tui/home-render.test.tsx | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/sync-client/src/cli/tui/views/HomeView.tsx b/packages/sync-client/src/cli/tui/views/HomeView.tsx index 731bb352f..828c64d75 100644 --- a/packages/sync-client/src/cli/tui/views/HomeView.tsx +++ b/packages/sync-client/src/cli/tui/views/HomeView.tsx @@ -52,8 +52,8 @@ export function HomeView({ | - {'Ask Hermes... "review my current PR"'} - Build Hermes Codex Shell + {'Ask Matrix... "review my current PR"'} + Build Matrix Codex Shell / commands tab agents s sessions q quit diff --git a/packages/sync-client/tests/tui/home-render.test.tsx b/packages/sync-client/tests/tui/home-render.test.tsx index df70597d9..42ad87661 100644 --- a/packages/sync-client/tests/tui/home-render.test.tsx +++ b/packages/sync-client/tests/tui/home-render.test.tsx @@ -24,7 +24,8 @@ describe("HomeView", () => { expect(output).not.toContain("M M A TTTTT"); expect(output).toContain(".@@@@oo.o@@@."); expect(output).toContain("@@@@@@@@@@@@@@@"); - expect(output).toContain("Ask Hermes"); + expect(output).toContain("Ask Matrix"); + expect(output).toContain("Build Matrix"); expect(output).toContain("q quit"); expect(output).toContain("cloud"); expect(output).toContain("2 sessions"); @@ -44,7 +45,7 @@ describe("HomeView", () => { const output = renderToString(); expect(output).toContain(".@@@@oo.o@@@."); - expect(output).toContain("Ask Hermes"); + expect(output).toContain("Ask Matrix"); expect(output).toContain("healthy · cloud · ok · 2 sessions"); }); @@ -52,7 +53,7 @@ describe("HomeView", () => { const output = renderToString(); expect(output).toContain("MATRIX OS"); - expect(output).toContain("Ask Hermes"); + expect(output).toContain("Ask Matrix"); expect(output).toContain("cloud"); expect(output).toContain("rabbit: .@@. @@@"); expect(output).not.toContain(".@@@@oo.o@@@."); From bfb08c2216be57455bbefc814338e064ac2cf7ca Mon Sep 17 00:00:00 2001 From: Nima Naderi Date: Fri, 29 May 2026 14:41:47 +0200 Subject: [PATCH 8/9] style(cli): use matrix green fullscreen tui --- packages/sync-client/src/cli/tui/app.tsx | 36 ++++++++++++++++--- .../src/cli/tui/views/CommandPalette.tsx | 8 +++-- .../src/cli/tui/views/HomeView.tsx | 29 +++++++++------ .../sync-client/src/cli/tui/views/Mascot.tsx | 8 ++--- .../tests/tui/home-render.test.tsx | 7 ++++ packages/sync-client/tests/tui/launch.test.ts | 20 +++++++++++ 6 files changed, 87 insertions(+), 21 deletions(-) diff --git a/packages/sync-client/src/cli/tui/app.tsx b/packages/sync-client/src/cli/tui/app.tsx index 2ea837c6c..c89abdc95 100644 --- a/packages/sync-client/src/cli/tui/app.tsx +++ b/packages/sync-client/src/cli/tui/app.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { render, Text, useInput } from "ink"; +import { render, Text, useApp, useInput } from "ink"; import { DEFAULT_TUI_ACTIONS } from "./actions.js"; import { normalizeTuiError } from "./errors.js"; import { searchTuiActions } from "./palette.js"; @@ -8,6 +8,9 @@ import { getTerminalCapabilities } from "./terminal.js"; import { CommandPalette } from "./views/CommandPalette.js"; import { HomeView } from "./views/HomeView.js"; +const ENTER_ALTERNATE_SCREEN = "\u001B[?1049h\u001B[H\u001B[2J"; +const EXIT_ALTERNATE_SCREEN = "\u001B[?1049l"; + function createSnapshotFailure(error: unknown): TuiStatusSnapshot { const unknownSubsystem = { state: "unknown" as const, label: "unknown" }; return { @@ -25,6 +28,7 @@ function createSnapshotFailure(error: unknown): TuiStatusSnapshot { } export function MatrixTuiApp({ initialSnapshot, noColor = false }: { initialSnapshot?: TuiStatusSnapshot; noColor?: boolean }) { + const { exit } = useApp(); const [snapshot, setSnapshot] = useState(initialSnapshot ?? null); const [paletteOpen, setPaletteOpen] = useState(false); const [paletteQuery, setPaletteQuery] = useState(""); @@ -44,6 +48,10 @@ export function MatrixTuiApp({ initialSnapshot, noColor = false }: { initialSnap setSelectedIndex(0); return; } + if (!paletteOpen && input === "q") { + exit(); + return; + } if (paletteOpen && key.upArrow) { setSelectedIndex((value) => Math.max(0, value - 1)); return; @@ -112,10 +120,30 @@ export function MatrixTuiApp({ initialSnapshot, noColor = false }: { initialSnap ); } - return ; + return ; +} + +export function shouldUseAlternateScreen({ + stdout = process.stdout, + env = process.env, +}: { + stdout?: NodeJS.WriteStream & { isTTY?: boolean }; + env?: NodeJS.ProcessEnv; +} = {}): boolean { + return stdout.isTTY === true && env.TERM !== "dumb" && env.MATRIX_TUI_FULLSCREEN !== "0"; } export async function launchTui(options: { noColor?: boolean } = {}): Promise { - const { waitUntilExit } = render(); - await waitUntilExit(); + const useAlternateScreen = shouldUseAlternateScreen(); + if (useAlternateScreen) { + process.stdout.write(ENTER_ALTERNATE_SCREEN); + } + try { + const { waitUntilExit } = render(); + await waitUntilExit(); + } finally { + if (useAlternateScreen) { + process.stdout.write(EXIT_ALTERNATE_SCREEN); + } + } } diff --git a/packages/sync-client/src/cli/tui/views/CommandPalette.tsx b/packages/sync-client/src/cli/tui/views/CommandPalette.tsx index 5d4bc28e8..739059c16 100644 --- a/packages/sync-client/src/cli/tui/views/CommandPalette.tsx +++ b/packages/sync-client/src/cli/tui/views/CommandPalette.tsx @@ -2,6 +2,8 @@ import React from "react"; import { Box, Text } from "ink"; import type { TuiAction } from "../actions.js"; +const MATRIX_GREEN = "#00ff41"; + function actionDescription(action: TuiAction): string { return action.intents[0] ?? action.aliases[0] ?? action.group; } @@ -27,15 +29,15 @@ export function CommandPalette({ const titleColumnWidth = Math.min(34, Math.max(1, Math.floor((width - 4) / 2))); return ( - + - MATRIX COMMANDS + MATRIX COMMANDS esc closes {`/${query}`} {results.map((action, index) => ( - + {index === selectedIndex ? "> " : " "}{paddedTitle(action.title, titleColumnWidth)}{action.group} diff --git a/packages/sync-client/src/cli/tui/views/HomeView.tsx b/packages/sync-client/src/cli/tui/views/HomeView.tsx index 828c64d75..b7b0bdd79 100644 --- a/packages/sync-client/src/cli/tui/views/HomeView.tsx +++ b/packages/sync-client/src/cli/tui/views/HomeView.tsx @@ -3,6 +3,8 @@ import { Box, Text } from "ink"; import { Mascot } from "./Mascot.js"; import type { TuiStatusSnapshot } from "../status.js"; +const MATRIX_GREEN = "#00ff41"; + function stateLabel(snapshot: TuiStatusSnapshot): string { if (snapshot.overall === "unauthenticated") { return "login required"; @@ -10,33 +12,40 @@ function stateLabel(snapshot: TuiStatusSnapshot): string { return snapshot.overall; } -function stateColor(snapshot: TuiStatusSnapshot): "green" | "yellow" | "cyan" { - if (snapshot.overall === "healthy") return "green"; - if (snapshot.overall === "degraded" || snapshot.overall === "unauthenticated") return "yellow"; - return "cyan"; +function stateColor(_snapshot: TuiStatusSnapshot): string { + return MATRIX_GREEN; } export function HomeView({ snapshot, columns = 80, + rows = 24, noColor = false, }: { snapshot: TuiStatusSnapshot; columns?: number; + rows?: number; noColor?: boolean; }) { const narrow = columns < 80; const wide = columns >= 92; const extraWide = columns >= 124; const stageWidth = Math.max(40, Math.min(columns, extraWide ? 120 : wide ? 96 : 76)); + const fullScreenHeight = rows >= 30 ? rows : undefined; const sessionLabel = `${snapshot.sessions.count} ${snapshot.sessions.count === 1 ? "session" : "sessions"}`; const status = `${stateLabel(snapshot)} · ${snapshot.profile.name} · ${snapshot.gateway.label} · ${sessionLabel}`; const color = noColor ? undefined : stateColor(snapshot); return ( - + - MATRIX OS + MATRIX OS {!narrow && !extraWide && ( @@ -48,12 +57,12 @@ export function HomeView({ - - | + + | {'Ask Matrix... "review my current PR"'} - Build Matrix Codex Shell + Build Matrix Codex Shell / commands tab agents s sessions q quit @@ -61,7 +70,7 @@ export function HomeView({ {status} {snapshot.blockingActions.length > 0 && ( - {`Next: /${snapshot.blockingActions[0]}`} + {`Next: /${snapshot.blockingActions[0]}`} )} diff --git a/packages/sync-client/src/cli/tui/views/Mascot.tsx b/packages/sync-client/src/cli/tui/views/Mascot.tsx index 08e33e602..abf7f34e4 100644 --- a/packages/sync-client/src/cli/tui/views/Mascot.tsx +++ b/packages/sync-client/src/cli/tui/views/Mascot.tsx @@ -2,6 +2,8 @@ import React from "react"; import { Box, Text } from "ink"; import type { TuiOverallState } from "../status.js"; +const MATRIX_GREEN = "#00ff41"; + const RABBIT_ART = [ " .@@.", " .@@@@.", @@ -32,10 +34,8 @@ const RABBIT_ART = [ const COMPACT_RABBIT = "rabbit: .@@. @@@"; -function mascotColor(state: TuiOverallState): "green" | "yellow" | "cyan" { - if (state === "healthy") return "green"; - if (state === "degraded" || state === "unauthenticated") return "yellow"; - return "cyan"; +function mascotColor(_state: TuiOverallState): string { + return MATRIX_GREEN; } export function Mascot({ state, noColor, compact = false }: { state: TuiOverallState; noColor?: boolean; compact?: boolean }) { diff --git a/packages/sync-client/tests/tui/home-render.test.tsx b/packages/sync-client/tests/tui/home-render.test.tsx index 42ad87661..0fa83f8ef 100644 --- a/packages/sync-client/tests/tui/home-render.test.tsx +++ b/packages/sync-client/tests/tui/home-render.test.tsx @@ -41,6 +41,13 @@ describe("HomeView", () => { expect(output).not.toContain("\u001B["); }); + it("fills tall terminals as a fullscreen launcher", () => { + const output = renderToString(); + + expect(output.split("\n").length).toBeGreaterThanOrEqual(40); + expect(output).toContain("Ask Matrix"); + }); + it("keeps large rabbit art readable on normal-width terminals", () => { const output = renderToString(); diff --git a/packages/sync-client/tests/tui/launch.test.ts b/packages/sync-client/tests/tui/launch.test.ts index c90e26301..a1d2fe433 100644 --- a/packages/sync-client/tests/tui/launch.test.ts +++ b/packages/sync-client/tests/tui/launch.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { shouldUseAlternateScreen } from "../../src/cli/tui/app.js"; import { resolveTuiLaunchMode } from "../../src/cli/tui/launch.js"; describe("TUI launch routing", () => { @@ -49,4 +50,23 @@ describe("TUI launch routing", () => { expect(resolveTuiLaunchMode({ argv, stdinIsTTY: true, stdoutIsTTY: true }).mode).toBe("direct"); } }); + + it("uses fullscreen alternate screen only for capable TTYs", () => { + expect(shouldUseAlternateScreen({ + stdout: { isTTY: true } as NodeJS.WriteStream & { isTTY: boolean }, + env: { TERM: "xterm-256color" }, + })).toBe(true); + expect(shouldUseAlternateScreen({ + stdout: { isTTY: true } as NodeJS.WriteStream & { isTTY: boolean }, + env: { TERM: "dumb" }, + })).toBe(false); + expect(shouldUseAlternateScreen({ + stdout: { isTTY: true } as NodeJS.WriteStream & { isTTY: boolean }, + env: { TERM: "xterm-256color", MATRIX_TUI_FULLSCREEN: "0" }, + })).toBe(false); + expect(shouldUseAlternateScreen({ + stdout: { isTTY: false } as NodeJS.WriteStream & { isTTY: boolean }, + env: { TERM: "xterm-256color" }, + })).toBe(false); + }); }); From fe4ca8cde5a494cbe23d21d50670510d1d8e4ad9 Mon Sep 17 00:00:00 2001 From: Nima Naderi Date: Fri, 29 May 2026 14:49:15 +0200 Subject: [PATCH 9/9] fix(cli): restore fullscreen tui on process exit --- packages/sync-client/src/cli/tui/app.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/sync-client/src/cli/tui/app.tsx b/packages/sync-client/src/cli/tui/app.tsx index c89abdc95..c65096f2d 100644 --- a/packages/sync-client/src/cli/tui/app.tsx +++ b/packages/sync-client/src/cli/tui/app.tsx @@ -137,13 +137,20 @@ export async function launchTui(options: { noColor?: boolean } = {}): Promise); - await waitUntilExit(); - } finally { - if (useAlternateScreen) { + const restoreAlternateScreen = () => { process.stdout.write(EXIT_ALTERNATE_SCREEN); + }; + process.once("exit", restoreAlternateScreen); + try { + const { waitUntilExit } = render(); + await waitUntilExit(); + } finally { + process.removeListener("exit", restoreAlternateScreen); + restoreAlternateScreen(); } + return; } + + const { waitUntilExit } = render(); + await waitUntilExit(); }