feat(ui): terminal, charts and low-contrast text follow the selected theme

- The lane terminal (xterm) takes its palette from the selected DARK theme —
  it stays dark in light mode, as a terminal should — and repaints live when
  the theme changes (`client/src/lib/terminalTheme.ts`).
- Charts on the Dashboard and Workflows pages draw their grid, axes, labels
  and tooltips from theme tokens instead of hardcoded near-black/white, and
  the activity heatmap ramps from the page surface to the accent, so it reads
  on every theme instead of going near-black on light ones.
- Status/brand text that only worked on dark backgrounds
  (`text-*-300/400`) gets a darker light-mode shade (`text-*-700
  dark:text-*-300`) across sessions, conversations, workflows, config and
  settings. Categorical chart colours are kept.
This commit is contained in:
2026-09-24 09:50:44 +07:00
parent 6353e88c9e
commit 8ea40260ad
36 changed files with 919 additions and 635 deletions
@@ -3,11 +3,12 @@
* @description Tests for the xterm.js-backed terminal view: verifies it opens
* a WS connection to the right URL, writes incoming binary frames to the
* mocked terminal, forwards typed input as outgoing binary frames, and
* re-attaches after the socket drops (server restart) but not after `exit`.
* re-attaches after the socket drops (server restart) but not after `exit`,
* and recolours live on a dark-theme change without re-attaching.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, cleanup } from "@testing-library/react";
import { render, cleanup, act } from "@testing-library/react";
import { TerminalView } from "../TerminalView";
const writeMock = vi.fn();
@@ -26,9 +27,13 @@ const termState = {
rows: 28,
};
const onResizeHandlers: Array<() => void> = [];
const termOptions: { theme?: unknown } = {};
const ctorArgs: Array<{ theme?: unknown }> = [];
vi.mock("@xterm/xterm", () => ({
Terminal: vi.fn().mockImplementation(() => ({
Terminal: vi.fn().mockImplementation((opts: { theme?: unknown }) => ({
...(ctorArgs.push(opts), {}),
options: termOptions,
get modes() {
return termState.modes;
},
@@ -58,6 +63,10 @@ vi.mock("@xterm/xterm", () => ({
focus: focusMock,
})),
}));
// jsdom loads no stylesheet, so stand in for the CSS-variable probe.
vi.mock("../../../lib/terminalTheme", () => ({
terminalThemeFor: (id: string) => ({ background: `bg-${id}` }),
}));
vi.mock("@xterm/addon-fit", () => ({
FitAddon: vi.fn().mockImplementation(() => ({ fit: vi.fn() })),
}));
@@ -103,6 +112,10 @@ describe("TerminalView", () => {
loadAddonMock.mockClear();
openMock.mockClear();
focusMock.mockClear();
disposeMock.mockClear();
ctorArgs.length = 0;
delete termOptions.theme;
localStorage.clear();
});
afterEach(cleanup);
@@ -126,6 +139,21 @@ describe("TerminalView", () => {
expect(MockWebSocket.instances[0]!.url).toBe("ws://localhost:4820/ws-pty/agenthub-lane-1");
});
it("paints the chosen DARK theme and recolours live without re-attaching", () => {
localStorage.setItem("theme", "light"); // app in light mode: terminal stays dark
localStorage.setItem("agenthub.theme.dark", "apple-dark");
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
expect(ctorArgs[0]!.theme).toEqual({ background: "bg-apple-dark" });
act(() => {
localStorage.setItem("agenthub.theme.dark", "darcula");
window.dispatchEvent(new Event("agenthub-theme"));
});
expect(termOptions.theme).toEqual({ background: "bg-darcula" });
expect(MockWebSocket.instances).toHaveLength(1);
expect(disposeMock).not.toHaveBeenCalled();
});
it("writes incoming WS data to the terminal", () => {
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
const ws = MockWebSocket.instances[0]!;