Skip to content
20 changes: 11 additions & 9 deletions src/terminal/palette.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Mayros palette tokens for CLI/UI theming. "mayros seam" == use this palette.
// Keep in sync with docs/cli/index.md (CLI palette section).
// Values are derived from the TUI dark palette (src/tui/theme/palettes.ts) so
// the CLI and the terminal UI share the same identity. Keep in sync with
// docs/cli/index.md (CLI palette section).
export const MAYROS_PALETTE = {
accent: "#FF5A2D",
accentBright: "#FF7A3D",
accentDim: "#D14A22",
info: "#FF8A5B",
success: "#2FBF71",
warn: "#FFB020",
error: "#E23D2D",
muted: "#8B7F77",
accent: "#F6C453",
accentBright: "#F2A65A",
accentDim: "#C49A3E",
info: "#F2A65A",
success: "#7DD3A5",
warn: "#F0C987",
error: "#F97066",
muted: "#7B7F87",
} as const;
50 changes: 42 additions & 8 deletions src/tui/a11y-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import process from "node:process";
import readline from "node:readline";
import { randomUUID } from "node:crypto";
import { GatewayChatClient, resolveGatewayConnection, type GatewayEvent } from "./gateway-chat.js";
import { handleSharedCommand } from "./shared-commands.js";
import { TuiStreamAssembler } from "./tui-stream-assembler.js";
import { A11yRenderer } from "./a11y-renderer.js";
import type { TuiOptions } from "./tui-types.js";
Expand All @@ -22,7 +23,7 @@ export async function runA11yTui(opts: TuiOptions): Promise<void> {
password: connection.password,
});

const sessionKey = opts.session ?? "main";
let sessionKey = opts.session ?? "main";
const assembler = new TuiStreamAssembler();
const showThinking = opts.thinking === "on" || opts.thinking === "verbose";

Expand Down Expand Up @@ -95,6 +96,15 @@ export async function runA11yTui(opts: TuiOptions): Promise<void> {
}
};

// Declare the readline interface before wiring client disconnect handling,
// so a disconnect between client.start() and the readline creation cannot
// reference `rl` in its temporal dead zone.
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: "> ",
});

client.onDisconnected = (reason: string) => {
renderer.emit({ type: "system", text: `Disconnected: ${reason}` });
rl.close();
Expand Down Expand Up @@ -125,15 +135,9 @@ export async function runA11yTui(opts: TuiOptions): Promise<void> {
});
}

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: "> ",
});

rl.prompt();

rl.on("line", (line) => {
rl.on("line", async (line) => {
const text = line.trim();
if (!text) {
rl.prompt();
Expand All @@ -147,6 +151,36 @@ export async function runA11yTui(opts: TuiOptions): Promise<void> {
return;
}

// Route slash commands through the shared command core so the accessible
// TUI reaches feature parity with the graphical TUI for the commands that
// do not require a visual picker (abort, reset, compact, status, etc.).
if (text.startsWith("/")) {
const [name, ...rest] = text.slice(1).split(/\s+/);
if (name) {
const sink = {
info: (msg: string) => renderer.emit({ type: "system", text: msg }),
error: (msg: string) => renderer.emit({ type: "system", text: msg }),
};
const handled = await handleSharedCommand(
{
client,
sessionKey,
getActiveRunId: () => currentRunId,
onSessionChanged: (key) => {
sessionKey = key;
},
},
name.toLowerCase(),
rest.join(" ").trim(),
sink,
);
if (handled.handled) {
rl.prompt();
return;
}
}
}

const runId = randomUUID();
currentRunId = runId;
lastOutputLength = 0;
Expand Down
13 changes: 11 additions & 2 deletions src/tui/components/tool-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,18 @@ export class ToolExecutionComponent extends Container {
const isDiff = DIFF_TOOLS.has(this.toolName) && raw && !this.isPartial;

if (isDiff) {
const colored = renderDiff(raw);
const colored = renderDiff(raw, {
add: theme.success,
del: theme.error,
header: (t) => theme.bold(t),
hunk: theme.accent,
context: theme.dim,
});
const stats = parseDiffStats(raw);
const statsLine = formatDiffStatsLine(stats);
const statsLine = formatDiffStatsLine(stats, {
add: theme.success,
del: theme.error,
});
const maxLines = this.expanded ? Infinity : DIFF_PREVIEW_LINES;
const display =
colored.length > maxLines
Expand Down
47 changes: 38 additions & 9 deletions src/tui/diff-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@ export type DiffStats = {
deletions: number;
};

/**
* Color functions for diff rendering, each taking text and returning it styled.
* Matches the shape of the TUI theme color functions so the active palette can
* be passed straight through, keeping diff colors consistent with the theme
* (correct under light and high-contrast presets).
*/
export type DiffColors = {
add: (text: string) => string;
del: (text: string) => string;
header: (text: string) => string;
hunk: (text: string) => string;
context: (text: string) => string;
};

/** Default (terminal-default) colors used when no theme is supplied. */
const DEFAULT_DIFF_COLORS: DiffColors = {
add: (t) => chalk.green(t),
del: (t) => chalk.red(t),
header: (t) => chalk.bold(chalk.white(t)),
hunk: (t) => chalk.cyan(t),
context: (t) => chalk.dim(t),
};

export function parseDiffLines(raw: string): DiffLine[] {
const lines: DiffLine[] = [];
for (const text of raw.split("\n")) {
Expand All @@ -36,20 +59,21 @@ export function parseDiffLines(raw: string): DiffLine[] {
return lines;
}

export function renderDiff(raw: string): string[] {
export function renderDiff(raw: string, colors?: Partial<DiffColors>): string[] {
const c = { ...DEFAULT_DIFF_COLORS, ...colors };
const parsed = parseDiffLines(raw);
return parsed.map((line) => {
switch (line.type) {
case "add":
return chalk.green(line.text);
return c.add(line.text);
case "del":
return chalk.red(line.text);
return c.del(line.text);
case "header":
return chalk.bold(chalk.white(line.text));
return c.header(line.text);
case "hunk":
return chalk.cyan(line.text);
return c.hunk(line.text);
default:
return chalk.dim(line.text);
return c.context(line.text);
}
});
}
Expand Down Expand Up @@ -80,10 +104,15 @@ export function renderDiffStats(raw: string): DiffStats {
* Format a single-line summary of diff stats with colors.
* Example: "+5 -3 (2 files)"
*/
export function formatDiffStatsLine(stats: DiffStats): string {
export function formatDiffStatsLine(
stats: DiffStats,
colors?: { add?: (text: string) => string; del?: (text: string) => string },
): string {
const add = colors?.add ?? ((t: string) => chalk.green(t));
const del = colors?.del ?? ((t: string) => chalk.red(t));
const parts: string[] = [];
if (stats.additions > 0) parts.push(chalk.green(`+${stats.additions}`));
if (stats.deletions > 0) parts.push(chalk.red(`-${stats.deletions}`));
if (stats.additions > 0) parts.push(add(`+${stats.additions}`));
if (stats.deletions > 0) parts.push(del(`-${stats.deletions}`));
const fileLabel = stats.files === 1 ? "1 file" : `${stats.files} files`;
return parts.length > 0 ? `${parts.join(" ")} (${fileLabel})` : `(${fileLabel})`;
}
Expand Down
6 changes: 5 additions & 1 deletion src/tui/file-mention.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ describe("file-mention", () => {
expect(result.mediaAttachments).toHaveLength(0);
});

it("follows symlinks to regular files", async () => {
// Creating symlinks on Windows requires Developer Mode or admin rights
// (EPERM otherwise), so skip this case there. CI runs on Linux where
// unprivileged symlink creation works.
const itSymlink = process.platform === "win32" ? it.skip : it;
itSymlink("follows symlinks to regular files", async () => {
const realPath = path.join(tmpDir, "real.txt");
const linkPath = path.join(tmpDir, "link.txt");
await fs.writeFile(realPath, "linked content");
Expand Down
48 changes: 0 additions & 48 deletions src/tui/input-history-store.test.ts

This file was deleted.

47 changes: 0 additions & 47 deletions src/tui/input-history-store.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/tui/session-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe("SessionManager", () => {
const client = createMockClient();
const mgr = new SessionManager({ client: client as never, currentAgentId: "default" });
await mgr.renameSession("s-abc", "New Name");
expect(client.patchSession).toHaveBeenCalledWith({ key: "s-abc", displayName: "New Name" });
expect(client.patchSession).toHaveBeenCalledWith({ key: "s-abc", label: "New Name" });
});

it("deletes a session via reset", async () => {
Expand Down
Loading
Loading