From e91d0fc8b7e905b99652bf3ed7b3c808f114178a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 14 Jul 2026 00:07:41 -0700 Subject: [PATCH] refactor(tui): remove dead exports (#107241) --- scripts/deadcode-exports.baseline.mjs | 12 ---- .../components/filterable-select-list.test.ts | 8 +-- src/tui/components/filterable-select-list.ts | 2 +- src/tui/gateway-chat.test.ts | 10 ++- src/tui/gateway-chat.ts | 4 +- src/tui/theme/theme.test.ts | 62 ++++++++++++++++++- src/tui/theme/theme.ts | 9 +-- src/tui/tui-busy-notice.test.ts | 6 +- src/tui/tui-busy-notice.ts | 3 +- src/tui/tui-last-session.test.ts | 8 +-- src/tui/tui-last-session.ts | 4 +- src/tui/tui-plugin-approvals.test.ts | 14 ++--- src/tui/tui-plugin-approvals.ts | 2 +- src/tui/tui-task-suggestions.test.ts | 15 ++--- src/tui/tui-task-suggestions.ts | 2 +- src/tui/tui-waiting.test.ts | 22 ++++--- src/tui/tui-waiting.ts | 2 +- 17 files changed, 117 insertions(+), 68 deletions(-) diff --git a/scripts/deadcode-exports.baseline.mjs b/scripts/deadcode-exports.baseline.mjs index ca1b83992479..60e0d1788587 100644 --- a/scripts/deadcode-exports.baseline.mjs +++ b/scripts/deadcode-exports.baseline.mjs @@ -864,18 +864,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [ "src/tasks/task-registry.ts: setTaskRegistryControlRuntimeForTests", "src/tasks/task-registry.ts: setTaskRegistryDeliveryRuntimeForTests", "src/tasks/task-retention.ts: resolveTaskRetentionMs", - "src/tui/components/filterable-select-list.ts: FilterableSelectListTheme", - "src/tui/gateway-chat.ts: resolveBoundGatewayConnection", - "src/tui/gateway-chat.ts: resolveGatewayConnection", - "src/tui/theme/theme.ts: lightMode", - "src/tui/theme/theme.ts: lightPalette", - "src/tui/theme/theme.ts: palette", - "src/tui/tui-busy-notice.ts: TUI_AGENT_BUSY_MESSAGE", - "src/tui/tui-last-session.ts: isHeartbeatLikeTuiSession", - "src/tui/tui-last-session.ts: resolveTuiLastSessionStatePath", - "src/tui/tui-plugin-approvals.ts: parseTuiPluginApproval", - "src/tui/tui-task-suggestions.ts: parseTuiTaskSuggestion", - "src/tui/tui-waiting.ts: pickWaitingPhrase", "src/wizard/clack-navigation-prompts.ts: formatNavigationFooter", "src/wizard/i18n/index.ts: listWizardI18nKeys", "src/wizard/i18n/index.ts: resolveWizardLocale", diff --git a/src/tui/components/filterable-select-list.test.ts b/src/tui/components/filterable-select-list.test.ts index 24134e54fa2b..390a1a6a410d 100644 --- a/src/tui/components/filterable-select-list.test.ts +++ b/src/tui/components/filterable-select-list.test.ts @@ -1,10 +1,8 @@ // Filterable select list tests cover keyboard filtering and cursor behavior. import { describe, expect, it } from "vitest"; -import { - FilterableSelectList, - type FilterableSelectItem, - type FilterableSelectListTheme, -} from "./filterable-select-list.js"; +import { FilterableSelectList, type FilterableSelectItem } from "./filterable-select-list.js"; + +type FilterableSelectListTheme = ConstructorParameters[2]; const mockTheme: FilterableSelectListTheme = { selectedPrefix: (t) => `[${t}]`, diff --git a/src/tui/components/filterable-select-list.ts b/src/tui/components/filterable-select-list.ts index f52ca20d8284..0b557b74a5be 100644 --- a/src/tui/components/filterable-select-list.ts +++ b/src/tui/components/filterable-select-list.ts @@ -18,7 +18,7 @@ export interface FilterableSelectItem extends SelectItem { searchTextLower?: string; } -export interface FilterableSelectListTheme extends SelectListTheme { +interface FilterableSelectListTheme extends SelectListTheme { filterLabel: (text: string) => string; } diff --git a/src/tui/gateway-chat.test.ts b/src/tui/gateway-chat.test.ts index d23d6b303915..fc39ca005d22 100644 --- a/src/tui/gateway-chat.test.ts +++ b/src/tui/gateway-chat.test.ts @@ -37,10 +37,16 @@ vi.mock("../infra/gateway-lock.js", () => ({ readActiveGatewayLockPort: readActiveGatewayLockPortMock, })); -const { GatewayChatClient, resolveBoundGatewayConnection, resolveGatewayConnection } = - await import("./gateway-chat.js"); +const { GatewayChatClient } = await import("./gateway-chat.js"); const { GatewayClientRequestError } = await import("../gateway/client.js"); +const resolveBoundGatewayConnection = ( + opts: Parameters[0], +) => GatewayChatClient.connectBound(opts).connection; + +const resolveGatewayConnection = async (opts: Parameters[0]) => + (await GatewayChatClient.connect(opts)).connection; + async function fileExists(filePath: string): Promise { try { await fs.access(filePath); diff --git a/src/tui/gateway-chat.ts b/src/tui/gateway-chat.ts index ba04410f8f4d..dc50bae532b1 100644 --- a/src/tui/gateway-chat.ts +++ b/src/tui/gateway-chat.ts @@ -394,7 +394,7 @@ export class GatewayChatClient implements TuiBackend { * deliberately ignores global config and Gateway env overrides, including * credentials, while still applying the normal remote URL safety policy. */ -export function resolveBoundGatewayConnection( +function resolveBoundGatewayConnection( opts: GatewayConnectionOptions & { config: OpenClawConfig; url: string }, ): ResolvedGatewayConnection { const url = buildGatewayConnectionDetails({ @@ -413,7 +413,7 @@ export function resolveBoundGatewayConnection( }; } -export async function resolveGatewayConnection( +async function resolveGatewayConnection( opts: GatewayConnectionOptions, ): Promise { const config = getRuntimeConfig(); diff --git a/src/tui/theme/theme.test.ts b/src/tui/theme/theme.test.ts index 0af8d1f6d5fa..ff85abf2aec6 100644 --- a/src/tui/theme/theme.test.ts +++ b/src/tui/theme/theme.test.ts @@ -1,8 +1,12 @@ // TUI theme tests cover theme defaults and environment-driven variants. import { expectDefined } from "@openclaw/normalization-core"; +import chalk from "chalk"; import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterAll, afterEach, describe, expect, it } from "vitest"; + +const originalChalkLevel = chalk.level; +chalk.level = 3; const { markdownTheme, searchableSelectListTheme, selectListTheme, theme } = await import("./theme.js"); @@ -13,6 +17,10 @@ const stripAnsi = (str: string) => let themeImportCase = 0; const originalEnv = { ...process.env }; +afterAll(() => { + chalk.level = originalChalkLevel; +}); + afterEach(() => { process.env = { ...originalEnv }; }); @@ -22,6 +30,48 @@ type ThemeEnvOverrides = { COLORFGBG?: string | undefined; }; +type ThemeModule = typeof import("./theme.js"); +const ansiRgbPattern = new RegExp( + `${String.fromCharCode(27)}\\[(38|48);2;(\\d+);(\\d+);(\\d+)m`, + "u", +); + +function colorFromStyle(style: (text: string) => string, layer: 38 | 48): string { + const match = style("x").match(ansiRgbPattern); + if (!match || Number(match[1]) !== layer) { + throw new Error(`expected ${layer === 38 ? "foreground" : "background"} RGB style`); + } + return `#${match + .slice(2, 5) + .map((channel) => Number(channel).toString(16).padStart(2, "0")) + .join("")}`.toUpperCase(); +} + +function readActivePalette(mod: ThemeModule) { + return { + text: colorFromStyle(mod.theme.fg, 38), + dim: colorFromStyle(mod.theme.dim, 38), + accent: colorFromStyle(mod.theme.accent, 38), + accentSoft: colorFromStyle(mod.theme.accentSoft, 38), + border: colorFromStyle(mod.theme.border, 38), + userBg: colorFromStyle(mod.theme.userBg, 48), + userText: colorFromStyle(mod.theme.userText, 38), + systemText: colorFromStyle(mod.theme.system, 38), + toolPendingBg: colorFromStyle(mod.theme.toolPendingBg, 48), + toolSuccessBg: colorFromStyle(mod.theme.toolSuccessBg, 48), + toolErrorBg: colorFromStyle(mod.theme.toolErrorBg, 48), + toolTitle: colorFromStyle(mod.theme.toolTitle, 38), + toolOutput: colorFromStyle(mod.theme.toolOutput, 38), + quote: colorFromStyle(mod.markdownTheme.quote, 38), + quoteBorder: colorFromStyle(mod.markdownTheme.quoteBorder, 38), + code: colorFromStyle(mod.markdownTheme.code, 38), + codeBorder: colorFromStyle(mod.markdownTheme.codeBlockBorder, 38), + link: colorFromStyle(mod.markdownTheme.link, 38), + error: colorFromStyle(mod.theme.error, 38), + success: colorFromStyle(mod.theme.success, 38), + }; +} + async function importThemeWithEnv(env: ThemeEnvOverrides) { if (Object.hasOwn(env, "OPENCLAW_THEME")) { if (env.OPENCLAW_THEME === undefined) { @@ -37,10 +87,16 @@ async function importThemeWithEnv(env: ThemeEnvOverrides) { process.env.COLORFGBG = env.COLORFGBG; } } - return importFreshModule( + const mod = await importFreshModule( import.meta.url, `./theme.js?env=${++themeImportCase}`, ); + const lightPalette = readActivePalette(mod); + return { + ...mod, + lightMode: lightPalette.text === "#1E1E1E", + lightPalette, + }; } function relativeLuminance(hex: string): number { @@ -235,7 +291,7 @@ describe("light palette accessibility", () => { pending: mod.lightPalette.toolPendingBg, success: mod.lightPalette.toolSuccessBg, error: mod.lightPalette.toolErrorBg, - code: mod.lightPalette.codeBlock, + code: "#FFFFFF", }; const textPairs = [ diff --git a/src/tui/theme/theme.ts b/src/tui/theme/theme.ts index 6396cc654daf..332768e389a6 100644 --- a/src/tui/theme/theme.ts +++ b/src/tui/theme/theme.ts @@ -85,8 +85,7 @@ function isLightBackground(): boolean { return false; } -/** Whether the terminal has a light background. Exported for testing only. */ -export const lightMode = isLightBackground(); +const lightMode = isLightBackground(); const darkPalette = { text: "#E8E3D5", @@ -105,14 +104,13 @@ const darkPalette = { quote: "#8CC8FF", quoteBorder: "#3B4D6B", code: "#F0C987", - codeBlock: "#1E232A", codeBorder: "#343A45", link: "#7DD3A5", error: "#F97066", success: "#7DD3A5", } as const; -export const lightPalette = { +const lightPalette = { text: "#1E1E1E", dim: "#5B6472", accent: "#B45309", @@ -129,14 +127,13 @@ export const lightPalette = { quote: "#1D4ED8", quoteBorder: "#2563EB", code: "#92400E", - codeBlock: "#F9FAFB", codeBorder: "#92400E", link: "#047857", error: "#DC2626", success: "#047857", } as const; -export const palette = lightMode ? lightPalette : darkPalette; +const palette = lightMode ? lightPalette : darkPalette; const fg = (hex: string) => (text: string) => chalk.hex(hex)(text); const bg = (hex: string) => (text: string) => chalk.bgHex(hex)(text); diff --git a/src/tui/tui-busy-notice.test.ts b/src/tui/tui-busy-notice.test.ts index e646a5ce7720..ee1c96d1371d 100644 --- a/src/tui/tui-busy-notice.test.ts +++ b/src/tui/tui-busy-notice.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { normalizeTestText } from "../../test/helpers/normalize-text.js"; import { ChatLog } from "./components/chat-log.js"; -import { addBlockedChatSubmitNotice, TUI_AGENT_BUSY_MESSAGE } from "./tui-busy-notice.js"; +import { addBlockedChatSubmitNotice } from "./tui-busy-notice.js"; describe("addBlockedChatSubmitNotice", () => { it("coalesces repeated busy submit notices", () => { @@ -13,6 +13,8 @@ describe("addBlockedChatSubmitNotice", () => { const rendered = normalizeTestText(chatLog.render(120).join("\n")); expect(chatLog.children.length).toBe(1); - expect(rendered).toContain(`${TUI_AGENT_BUSY_MESSAGE} x3`); + expect(rendered).toContain( + "agent is busy — press Esc to abort before sending a new message x3", + ); }); }); diff --git a/src/tui/tui-busy-notice.ts b/src/tui/tui-busy-notice.ts index 8286fd070a09..d593cc6b6dc9 100644 --- a/src/tui/tui-busy-notice.ts +++ b/src/tui/tui-busy-notice.ts @@ -1,7 +1,6 @@ import type { ChatLog } from "./components/chat-log.js"; -export const TUI_AGENT_BUSY_MESSAGE = - "agent is busy — press Esc to abort before sending a new message"; +const TUI_AGENT_BUSY_MESSAGE = "agent is busy — press Esc to abort before sending a new message"; export function addBlockedChatSubmitNotice(chatLog: Pick) { chatLog.addSystem(TUI_AGENT_BUSY_MESSAGE, { coalesceConsecutive: true }); diff --git a/src/tui/tui-last-session.test.ts b/src/tui/tui-last-session.test.ts index ad847f72251a..c8443124b8c7 100644 --- a/src/tui/tui-last-session.test.ts +++ b/src/tui/tui-last-session.test.ts @@ -2,14 +2,11 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, describe, expect, it } from "vitest"; import { buildTuiLastSessionScopeKey, - isHeartbeatLikeTuiSession, readTuiLastSessionKey, resolveRememberedTuiSessionKey, - resolveTuiLastSessionStatePath, writeTuiLastSessionKey, } from "./tui-last-session.js"; @@ -41,7 +38,7 @@ describe("tui last session state", () => { }); await expect(readTuiLastSessionKey({ scopeKey, stateDir })).resolves.toBe("agent:main:tui-123"); - const raw = await fs.readFile(resolveTuiLastSessionStatePath(stateDir), "utf8"); + const raw = await fs.readFile(path.join(stateDir, "tui", "last-session.json"), "utf8"); expect(raw).not.toContain("127.0.0.1"); }); @@ -108,9 +105,6 @@ describe("tui last session state", () => { { key: "agent:main:tui-123" }, ]; - expect( - isHeartbeatLikeTuiSession(expectDefined(sessions[0], "sessions[0] test invariant")), - ).toBe(true); expect( resolveRememberedTuiSessionKey({ rememberedKey: "agent:main:main", diff --git a/src/tui/tui-last-session.ts b/src/tui/tui-last-session.ts index 83d9cd27162b..d2667a62e752 100644 --- a/src/tui/tui-last-session.ts +++ b/src/tui/tui-last-session.ts @@ -16,7 +16,7 @@ type LastSessionRecord = { type LastSessionStore = Record; /** Resolves the private state file for remembered TUI sessions. */ -export function resolveTuiLastSessionStatePath(stateDir = resolveStateDir()): string { +function resolveTuiLastSessionStatePath(stateDir = resolveStateDir()): string { return path.join(stateDir, "tui", "last-session.json"); } @@ -56,7 +56,7 @@ function isHeartbeatSessionKey(sessionKey: string): boolean { } /** Detects heartbeat/system sessions that should not become the remembered human session. */ -export function isHeartbeatLikeTuiSession(session: TuiSessionList["sessions"][number]): boolean { +function isHeartbeatLikeTuiSession(session: TuiSessionList["sessions"][number]): boolean { if (isHeartbeatSessionKey(session.key)) { return true; } diff --git a/src/tui/tui-plugin-approvals.test.ts b/src/tui/tui-plugin-approvals.test.ts index 9e3cceb0d29a..2482d2457bb0 100644 --- a/src/tui/tui-plugin-approvals.test.ts +++ b/src/tui/tui-plugin-approvals.test.ts @@ -2,10 +2,7 @@ import type { Component, OverlayHandle, SelectItem } from "@earendil-works/pi-tu import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; import { stripAnsi } from "../../packages/terminal-core/src/ansi.js"; -import { - createTuiPluginApprovalController, - parseTuiPluginApproval, -} from "./tui-plugin-approvals.js"; +import { createTuiPluginApprovalController } from "./tui-plugin-approvals.js"; type TestSelector = Component & { items: SelectItem[]; @@ -121,9 +118,12 @@ function createHarness() { } describe("TUI plugin approvals", () => { - it("parses the pending plugin approval gateway shape", () => { - expect(parseTuiPluginApproval(approvalPayload())).toEqual(approvalPayload()); - expect(parseTuiPluginApproval({ id: "plugin:missing-request" })).toBeNull(); + it("ignores malformed plugin approval gateway payloads", () => { + const harness = createHarness(); + harness.controller.handleEvent("plugin.approval.requested", { + id: "plugin:missing-request", + }); + expect(harness.openOverlay).not.toHaveBeenCalled(); }); it("shows workspace skill approvals for the active session and resolves the selection", async () => { diff --git a/src/tui/tui-plugin-approvals.ts b/src/tui/tui-plugin-approvals.ts index 5bfc70f07a3a..9d191e22eb5b 100644 --- a/src/tui/tui-plugin-approvals.ts +++ b/src/tui/tui-plugin-approvals.ts @@ -153,7 +153,7 @@ function parseSeverity(value: unknown): TuiPluginApproval["request"]["severity"] } /** Parses the gateway event/list shape used for pending plugin approvals. */ -export function parseTuiPluginApproval(payload: unknown): TuiPluginApproval | null { +function parseTuiPluginApproval(payload: unknown): TuiPluginApproval | null { if (!isRecord(payload) || !isRecord(payload.request)) { return null; } diff --git a/src/tui/tui-task-suggestions.test.ts b/src/tui/tui-task-suggestions.test.ts index 1b8eefe6d28c..43c773e520ba 100644 --- a/src/tui/tui-task-suggestions.test.ts +++ b/src/tui/tui-task-suggestions.test.ts @@ -2,10 +2,7 @@ import type { Component, OverlayHandle, SelectItem } from "@earendil-works/pi-tu import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; import { stripAnsi } from "../../packages/terminal-core/src/ansi.js"; -import { - createTuiTaskSuggestionController, - parseTuiTaskSuggestion, -} from "./tui-task-suggestions.js"; +import { createTuiTaskSuggestionController } from "./tui-task-suggestions.js"; type TestSelector = Component & { items: SelectItem[]; @@ -115,9 +112,13 @@ function createHarness() { } describe("TUI task suggestions", () => { - it("parses the Gateway suggestion shape", () => { - expect(parseTuiTaskSuggestion(suggestionPayload())).toEqual(suggestionPayload()); - expect(parseTuiTaskSuggestion({ id: "task_missing_fields" })).toBeNull(); + it("ignores malformed Gateway suggestion payloads", () => { + const harness = createHarness(); + harness.controller.handleEvent("task.suggestion", { + action: "created", + suggestion: { id: "task_missing_fields" }, + }); + expect(harness.openOverlay).not.toHaveBeenCalled(); }); it("shows an active-session suggestion and starts it after confirmation", async () => { diff --git a/src/tui/tui-task-suggestions.ts b/src/tui/tui-task-suggestions.ts index 388b361f5274..2f65966dde8f 100644 --- a/src/tui/tui-task-suggestions.ts +++ b/src/tui/tui-task-suggestions.ts @@ -69,7 +69,7 @@ function isRecord(value: unknown): value is Record { } /** Parses the task suggestion shape carried by Gateway list and event payloads. */ -export function parseTuiTaskSuggestion(value: unknown): TaskSuggestion | null { +function parseTuiTaskSuggestion(value: unknown): TaskSuggestion | null { if (!isRecord(value)) { return null; } diff --git a/src/tui/tui-waiting.test.ts b/src/tui/tui-waiting.test.ts index 526e41668ed0..356330c170d4 100644 --- a/src/tui/tui-waiting.test.ts +++ b/src/tui/tui-waiting.test.ts @@ -1,6 +1,6 @@ // Verifies TUI waiting indicators and elapsed-time rendering. import { describe, expect, it } from "vitest"; -import { buildWaitingStatusMessage, pickWaitingPhrase } from "./tui-waiting.js"; +import { buildWaitingStatusMessage } from "./tui-waiting.js"; const theme = { dim: (s: string) => `${s}`, @@ -9,13 +9,21 @@ const theme = { } satisfies Parameters[0]["theme"]; describe("tui-waiting", () => { - it("pickWaitingPhrase rotates every 10 ticks", () => { + it("rotates the rendered waiting phrase every 10 ticks", () => { const phrases = ["a", "b", "c"]; - expect(pickWaitingPhrase(0, phrases)).toBe("a"); - expect(pickWaitingPhrase(9, phrases)).toBe("a"); - expect(pickWaitingPhrase(10, phrases)).toBe("b"); - expect(pickWaitingPhrase(20, phrases)).toBe("c"); - expect(pickWaitingPhrase(30, phrases)).toBe("a"); + const renderPhrase = (tick: number) => + buildWaitingStatusMessage({ + theme, + tick, + elapsed: "3s", + connectionStatus: "connected", + phrases, + }).replace(/<[^>]+>/g, ""); + expect(renderPhrase(0)).toMatch(/^a…/); + expect(renderPhrase(9)).toMatch(/^a…/); + expect(renderPhrase(10)).toMatch(/^b…/); + expect(renderPhrase(20)).toMatch(/^c…/); + expect(renderPhrase(30)).toMatch(/^a…/); }); it("buildWaitingStatusMessage includes shimmer markup and metadata", () => { diff --git a/src/tui/tui-waiting.ts b/src/tui/tui-waiting.ts index d397ba34b81f..5fc91612d035 100644 --- a/src/tui/tui-waiting.ts +++ b/src/tui/tui-waiting.ts @@ -20,7 +20,7 @@ export const defaultWaitingPhrases = [ ]; /** Picks a stable phrase for a timer tick. */ -export function pickWaitingPhrase(tick: number, phrases = defaultWaitingPhrases) { +function pickWaitingPhrase(tick: number, phrases = defaultWaitingPhrases) { const idx = Math.floor(tick / 10) % phrases.length; return phrases[idx] ?? phrases[0] ?? "waiting"; }