From f63e9496fa690dbefd841c372c8289f27e84031f Mon Sep 17 00:00:00 2001 From: ndycode Date: Sat, 21 Mar 2026 11:50:02 +0800 Subject: [PATCH 1/2] refactor: extract statusline settings panel --- lib/codex-manager/settings-hub.ts | 207 +--------------- .../statusline-settings-panel.ts | 233 ++++++++++++++++++ 2 files changed, 245 insertions(+), 195 deletions(-) create mode 100644 lib/codex-manager/statusline-settings-panel.ts diff --git a/lib/codex-manager/settings-hub.ts b/lib/codex-manager/settings-hub.ts index c122d515..30417ec3 100644 --- a/lib/codex-manager/settings-hub.ts +++ b/lib/codex-manager/settings-hub.ts @@ -33,6 +33,7 @@ import { getUnifiedSettingsPath } from "../unified-settings.js"; import { sleep } from "../utils.js"; import { promptBehaviorSettingsPanel } from "./behavior-settings-panel.js"; import { promptDashboardDisplayPanel } from "./dashboard-display-panel.js"; +import { promptStatuslineSettingsPanel } from "./statusline-settings-panel.js"; import { promptThemeSettingsPanel } from "./theme-settings-panel.js"; type DashboardDisplaySettingKey = @@ -154,14 +155,6 @@ type PreviewFocusKey = | "menuLayoutMode" | null; -type StatuslineConfigAction = - | { type: "toggle"; key: DashboardStatuslineField } - | { type: "move-up"; key: DashboardStatuslineField } - | { type: "move-down"; key: DashboardStatuslineField } - | { type: "reset" } - | { type: "save" } - | { type: "cancel" }; - type BackendToggleSettingKey = | "liveAccountSync" | "sessionAffinity" @@ -1372,193 +1365,17 @@ function reorderField( async function promptStatuslineSettings( initial: DashboardDisplaySettings, ): Promise { - if (!input.isTTY || !output.isTTY) { - return null; - } - - const ui = getUiRuntimeOptions(); - let draft = cloneDashboardSettings(initial); - let focusKey: DashboardStatuslineField = - draft.menuStatuslineFields?.[0] ?? "last-used"; - while (true) { - const preview = buildAccountListPreview(draft, ui, focusKey); - const selectedSet = new Set( - normalizeStatuslineFields(draft.menuStatuslineFields), - ); - const ordered = normalizeStatuslineFields(draft.menuStatuslineFields); - const orderMap = new Map(); - for (let index = 0; index < ordered.length; index += 1) { - const key = ordered[index]; - if (key) orderMap.set(key, index + 1); - } - - const optionItems: MenuItem[] = - STATUSLINE_FIELD_OPTIONS.map((option, index) => { - const enabled = selectedSet.has(option.key); - const rank = orderMap.get(option.key); - const label = `${formatDashboardSettingState(enabled)} ${index + 1}. ${option.label}${rank ? ` (order ${rank})` : ""}`; - return { - label, - hint: option.description, - value: { type: "toggle", key: option.key }, - color: enabled ? "green" : "yellow", - }; - }); - - const items: MenuItem[] = [ - { - label: UI_COPY.settings.previewHeading, - value: { type: "cancel" }, - kind: "heading", - }, - { - label: preview.label, - hint: preview.hint, - value: { type: "cancel" }, - color: "green", - disabled: true, - hideUnavailableSuffix: true, - }, - { label: "", value: { type: "cancel" }, separator: true }, - { - label: UI_COPY.settings.displayHeading, - value: { type: "cancel" }, - kind: "heading", - }, - ...optionItems, - { label: "", value: { type: "cancel" }, separator: true }, - { - label: UI_COPY.settings.moveUp, - value: { type: "move-up", key: focusKey }, - color: "green", - }, - { - label: UI_COPY.settings.moveDown, - value: { type: "move-down", key: focusKey }, - color: "green", - }, - { label: "", value: { type: "cancel" }, separator: true }, - { - label: UI_COPY.settings.resetDefault, - value: { type: "reset" }, - color: "yellow", - }, - { - label: UI_COPY.settings.saveAndBack, - value: { type: "save" }, - color: "green", - }, - { - label: UI_COPY.settings.backNoSave, - value: { type: "cancel" }, - color: "red", - }, - ]; - - const initialCursor = items.findIndex( - (item) => item.value.type === "toggle" && item.value.key === focusKey, - ); - - const updateFocusedPreview = (cursor: number) => { - const focusedItem = items[cursor]; - const focusedKey = - focusedItem?.value.type === "toggle" ? focusedItem.value.key : focusKey; - const nextPreview = buildAccountListPreview(draft, ui, focusedKey); - const previewItem = items[1]; - if (!previewItem) return; - previewItem.label = nextPreview.label; - previewItem.hint = nextPreview.hint; - }; - - const result = await select(items, { - message: UI_COPY.settings.summaryTitle, - subtitle: UI_COPY.settings.summarySubtitle, - help: UI_COPY.settings.summaryHelp, - clearScreen: true, - theme: ui.theme, - selectedEmphasis: "minimal", - initialCursor: initialCursor >= 0 ? initialCursor : undefined, - onCursorChange: ({ cursor }) => { - const focusedItem = items[cursor]; - if (focusedItem?.value.type === "toggle") { - focusKey = focusedItem.value.key; - } - updateFocusedPreview(cursor); - }, - onInput: (raw) => { - const lower = raw.toLowerCase(); - if (lower === "q") return { type: "cancel" }; - if (lower === "s") return { type: "save" }; - if (lower === "r") return { type: "reset" }; - if (lower === "[") return { type: "move-up", key: focusKey }; - if (lower === "]") return { type: "move-down", key: focusKey }; - const parsed = Number.parseInt(raw, 10); - if ( - Number.isFinite(parsed) && - parsed >= 1 && - parsed <= STATUSLINE_FIELD_OPTIONS.length - ) { - const target = STATUSLINE_FIELD_OPTIONS[parsed - 1]; - if (target) { - return { type: "toggle", key: target.key }; - } - } - return undefined; - }, - }); - - if (!result || result.type === "cancel") { - return null; - } - if (result.type === "save") { - return draft; - } - if (result.type === "reset") { - draft = applyDashboardDefaultsForKeys(draft, STATUSLINE_PANEL_KEYS); - focusKey = draft.menuStatuslineFields?.[0] ?? "last-used"; - continue; - } - if (result.type === "move-up") { - draft = { - ...draft, - menuStatuslineFields: reorderField( - normalizeStatuslineFields(draft.menuStatuslineFields), - result.key, - -1, - ), - }; - focusKey = result.key; - continue; - } - if (result.type === "move-down") { - draft = { - ...draft, - menuStatuslineFields: reorderField( - normalizeStatuslineFields(draft.menuStatuslineFields), - result.key, - 1, - ), - }; - focusKey = result.key; - continue; - } - - focusKey = result.key; - const fields = normalizeStatuslineFields(draft.menuStatuslineFields); - const isEnabled = fields.includes(result.key); - if (isEnabled) { - const next = fields.filter((field) => field !== result.key); - draft = { - ...draft, - menuStatuslineFields: next.length > 0 ? next : [result.key], - }; - } else { - draft = { - ...draft, - menuStatuslineFields: [...fields, result.key], - }; - } - } + return promptStatuslineSettingsPanel(initial, { + cloneDashboardSettings, + buildAccountListPreview, + normalizeStatuslineFields, + formatDashboardSettingState, + reorderField, + applyDashboardDefaultsForKeys, + STATUSLINE_FIELD_OPTIONS, + STATUSLINE_PANEL_KEYS, + UI_COPY, + }); } async function configureStatuslineSettings( diff --git a/lib/codex-manager/statusline-settings-panel.ts b/lib/codex-manager/statusline-settings-panel.ts new file mode 100644 index 00000000..9018d91c --- /dev/null +++ b/lib/codex-manager/statusline-settings-panel.ts @@ -0,0 +1,233 @@ +import { stdin as input, stdout as output } from "node:process"; +import type { + DashboardDisplaySettings, + DashboardStatuslineField, +} from "../dashboard-settings.js"; +import type { UI_COPY } from "../ui/copy.js"; +import { getUiRuntimeOptions } from "../ui/runtime.js"; +import { type MenuItem, select } from "../ui/select.js"; + +export type StatuslineConfigAction = + | { type: "toggle"; key: DashboardStatuslineField } + | { type: "move-up"; key: DashboardStatuslineField } + | { type: "move-down"; key: DashboardStatuslineField } + | { type: "reset" } + | { type: "save" } + | { type: "cancel" }; + +export interface StatuslineFieldOption { + key: DashboardStatuslineField; + label: string; + description: string; +} + +export interface StatuslineSettingsPanelDeps { + cloneDashboardSettings: ( + settings: DashboardDisplaySettings, + ) => DashboardDisplaySettings; + buildAccountListPreview: ( + settings: DashboardDisplaySettings, + ui: ReturnType, + focusKey: DashboardStatuslineField, + ) => { label: string; hint?: string }; + normalizeStatuslineFields: ( + fields: DashboardDisplaySettings["menuStatuslineFields"], + ) => DashboardStatuslineField[]; + formatDashboardSettingState: (enabled: boolean) => string; + reorderField: ( + fields: DashboardStatuslineField[], + key: DashboardStatuslineField, + direction: -1 | 1, + ) => DashboardStatuslineField[]; + applyDashboardDefaultsForKeys: ( + draft: DashboardDisplaySettings, + keys: readonly (keyof DashboardDisplaySettings)[], + ) => DashboardDisplaySettings; + STATUSLINE_FIELD_OPTIONS: readonly StatuslineFieldOption[]; + STATUSLINE_PANEL_KEYS: readonly (keyof DashboardDisplaySettings)[]; + UI_COPY: typeof UI_COPY; +} + +export async function promptStatuslineSettingsPanel( + initial: DashboardDisplaySettings, + deps: StatuslineSettingsPanelDeps, +): Promise { + if (!input.isTTY || !output.isTTY) return null; + + const ui = getUiRuntimeOptions(); + let draft = deps.cloneDashboardSettings(initial); + let focusKey: DashboardStatuslineField = + draft.menuStatuslineFields?.[0] ?? "last-used"; + + while (true) { + const preview = deps.buildAccountListPreview(draft, ui, focusKey); + const selectedSet = new Set( + deps.normalizeStatuslineFields(draft.menuStatuslineFields), + ); + const ordered = deps.normalizeStatuslineFields(draft.menuStatuslineFields); + const orderMap = new Map(); + for (let index = 0; index < ordered.length; index += 1) { + const key = ordered[index]; + if (key) orderMap.set(key, index + 1); + } + + const optionItems: MenuItem[] = + deps.STATUSLINE_FIELD_OPTIONS.map((option, index) => { + const enabled = selectedSet.has(option.key); + const rank = orderMap.get(option.key); + return { + label: `${deps.formatDashboardSettingState(enabled)} ${index + 1}. ${option.label}${rank ? ` (order ${rank})` : ""}`, + hint: option.description, + value: { type: "toggle", key: option.key }, + color: enabled ? "green" : "yellow", + }; + }); + + const items: MenuItem[] = [ + { + label: deps.UI_COPY.settings.previewHeading, + value: { type: "cancel" }, + kind: "heading", + }, + { + label: preview.label, + hint: preview.hint, + value: { type: "cancel" }, + color: "green", + disabled: true, + hideUnavailableSuffix: true, + }, + { label: "", value: { type: "cancel" }, separator: true }, + { + label: deps.UI_COPY.settings.displayHeading, + value: { type: "cancel" }, + kind: "heading", + }, + ...optionItems, + { label: "", value: { type: "cancel" }, separator: true }, + { + label: deps.UI_COPY.settings.moveUp, + value: { type: "move-up", key: focusKey }, + color: "green", + }, + { + label: deps.UI_COPY.settings.moveDown, + value: { type: "move-down", key: focusKey }, + color: "green", + }, + { label: "", value: { type: "cancel" }, separator: true }, + { + label: deps.UI_COPY.settings.resetDefault, + value: { type: "reset" }, + color: "yellow", + }, + { + label: deps.UI_COPY.settings.saveAndBack, + value: { type: "save" }, + color: "green", + }, + { + label: deps.UI_COPY.settings.backNoSave, + value: { type: "cancel" }, + color: "red", + }, + ]; + + const initialCursor = items.findIndex( + (item) => item.value.type === "toggle" && item.value.key === focusKey, + ); + + const updateFocusedPreview = (cursor: number) => { + const focusedItem = items[cursor]; + const focused = + focusedItem?.value.type === "toggle" ? focusedItem.value.key : focusKey; + const nextPreview = deps.buildAccountListPreview(draft, ui, focused); + const previewItem = items[1]; + if (!previewItem) return; + previewItem.label = nextPreview.label; + previewItem.hint = nextPreview.hint; + }; + + const result = await select(items, { + message: deps.UI_COPY.settings.summaryTitle, + subtitle: deps.UI_COPY.settings.summarySubtitle, + help: deps.UI_COPY.settings.summaryHelp, + clearScreen: true, + theme: ui.theme, + selectedEmphasis: "minimal", + initialCursor: initialCursor >= 0 ? initialCursor : undefined, + onCursorChange: ({ cursor }) => { + const focusedItem = items[cursor]; + if (focusedItem?.value.type === "toggle") + focusKey = focusedItem.value.key; + updateFocusedPreview(cursor); + }, + onInput: (raw) => { + const lower = raw.toLowerCase(); + if (lower === "q") return { type: "cancel" }; + if (lower === "s") return { type: "save" }; + if (lower === "r") return { type: "reset" }; + if (lower === "[") return { type: "move-up", key: focusKey }; + if (lower === "]") return { type: "move-down", key: focusKey }; + const parsed = Number.parseInt(raw, 10); + if ( + Number.isFinite(parsed) && + parsed >= 1 && + parsed <= deps.STATUSLINE_FIELD_OPTIONS.length + ) { + const target = deps.STATUSLINE_FIELD_OPTIONS[parsed - 1]; + if (target) return { type: "toggle", key: target.key }; + } + return undefined; + }, + }); + + if (!result || result.type === "cancel") return null; + if (result.type === "save") return draft; + if (result.type === "reset") { + draft = deps.applyDashboardDefaultsForKeys( + draft, + deps.STATUSLINE_PANEL_KEYS, + ); + focusKey = draft.menuStatuslineFields?.[0] ?? "last-used"; + continue; + } + if (result.type === "move-up") { + draft = { + ...draft, + menuStatuslineFields: deps.reorderField( + deps.normalizeStatuslineFields(draft.menuStatuslineFields), + result.key, + -1, + ), + }; + focusKey = result.key; + continue; + } + if (result.type === "move-down") { + draft = { + ...draft, + menuStatuslineFields: deps.reorderField( + deps.normalizeStatuslineFields(draft.menuStatuslineFields), + result.key, + 1, + ), + }; + focusKey = result.key; + continue; + } + + focusKey = result.key; + const fields = deps.normalizeStatuslineFields(draft.menuStatuslineFields); + const isEnabled = fields.includes(result.key); + if (isEnabled) { + const next = fields.filter((field) => field !== result.key); + draft = { + ...draft, + menuStatuslineFields: next.length > 0 ? next : [result.key], + }; + } else { + draft = { ...draft, menuStatuslineFields: [...fields, result.key] }; + } + } +} From c6de32c147434b9574b1318edf2742dbbedbf7d0 Mon Sep 17 00:00:00 2001 From: ndycode <405533+ndycode@users.noreply.github.com> Date: Sun, 22 Mar 2026 23:02:44 +0800 Subject: [PATCH 2/2] test: cover statusline panel hotkeys --- test/settings-hub-utils.test.ts | 56 +++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/test/settings-hub-utils.test.ts b/test/settings-hub-utils.test.ts index 6e0cc6de..36ca1e3b 100644 --- a/test/settings-hub-utils.test.ts +++ b/test/settings-hub-utils.test.ts @@ -644,6 +644,62 @@ describe("settings-hub utility coverage", () => { ]); }); + it("returns null for statusline settings when stdin or stdout is not a tty", async () => { + const api = await loadSettingsHubTestApi(); + setStreamIsTTY(process.stdin, false); + setStreamIsTTY(process.stdout, false); + await expect( + api.promptStatuslineSettings({ + ...DEFAULT_DASHBOARD_DISPLAY_SETTINGS, + }), + ).resolves.toBeNull(); + }); + + it("supports statusline reorder hotkeys before saving", async () => { + const api = await loadSettingsHubTestApi(); + queueSelectResults( + triggerSettingsHubHotkey("]"), + triggerSettingsHubHotkey("s"), + ); + const selected = await api.promptStatuslineSettings({ + ...DEFAULT_DASHBOARD_DISPLAY_SETTINGS, + menuStatuslineFields: ["last-used", "limits", "status"], + }); + expect(selected?.menuStatuslineFields).toEqual([ + "limits", + "last-used", + "status", + ]); + }); + + it("supports statusline reset hotkeys before saving", async () => { + const api = await loadSettingsHubTestApi(); + queueSelectResults( + triggerSettingsHubHotkey("r"), + triggerSettingsHubHotkey("s"), + ); + const selected = await api.promptStatuslineSettings({ + ...DEFAULT_DASHBOARD_DISPLAY_SETTINGS, + menuStatuslineFields: ["status"], + }); + expect(selected?.menuStatuslineFields).toEqual( + DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuStatuslineFields, + ); + }); + + it("keeps the last statusline field enabled when toggled off by numeric hotkey", async () => { + const api = await loadSettingsHubTestApi(); + queueSelectResults( + triggerSettingsHubHotkey("1"), + triggerSettingsHubHotkey("s"), + ); + const selected = await api.promptStatuslineSettings({ + ...DEFAULT_DASHBOARD_DISPLAY_SETTINGS, + menuStatuslineFields: ["last-used"], + }); + expect(selected?.menuStatuslineFields).toEqual(["last-used"]); + }); + it("toggles behavior settings before returning the draft", async () => { const api = await loadSettingsHubTestApi(); queueSelectResults({ type: "toggle-pause" }, { type: "save" });