Skip to content

Commit b027310

Browse files
committed
refactor: extract backend settings menu wrapper
1 parent 16acf57 commit b027310

3 files changed

Lines changed: 287 additions & 132 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import type { PluginConfig } from "../types.js";
2+
import type { UiRuntimeOptions } from "../ui/runtime.js";
3+
import type { MenuItem } from "../ui/select.js";
4+
import type {
5+
BackendCategoryKey,
6+
BackendCategoryOption,
7+
BackendSettingFocusKey,
8+
BackendSettingsHubAction,
9+
} from "./backend-settings-schema.js";
10+
11+
export async function promptBackendSettingsMenu(params: {
12+
initial: PluginConfig;
13+
isInteractive: () => boolean;
14+
ui: UiRuntimeOptions;
15+
cloneBackendPluginConfig: (config: PluginConfig) => PluginConfig;
16+
backendCategoryOptions: readonly BackendCategoryOption[];
17+
getBackendCategoryInitialFocus: (
18+
category: BackendCategoryOption,
19+
) => BackendSettingFocusKey;
20+
buildBackendSettingsPreview: (
21+
config: PluginConfig,
22+
ui: UiRuntimeOptions,
23+
focus: BackendSettingFocusKey,
24+
deps: {
25+
highlightPreviewToken: (text: string, ui: UiRuntimeOptions) => string;
26+
},
27+
) => { label: string; hint: string };
28+
highlightPreviewToken: (text: string, ui: UiRuntimeOptions) => string;
29+
select: <T>(
30+
items: MenuItem<T>[],
31+
options: {
32+
message: string;
33+
subtitle: string;
34+
help: string;
35+
clearScreen: boolean;
36+
theme: UiRuntimeOptions["theme"];
37+
selectedEmphasis: "minimal";
38+
initialCursor?: number;
39+
onCursorChange: (event: { cursor: number }) => void;
40+
onInput: (raw: string) => T | undefined;
41+
},
42+
) => Promise<T | null>;
43+
getBackendCategory: (
44+
key: BackendCategoryKey,
45+
categories: readonly BackendCategoryOption[],
46+
) => BackendCategoryOption | null;
47+
promptBackendCategorySettings: (
48+
initial: PluginConfig,
49+
category: BackendCategoryOption,
50+
focus: BackendSettingFocusKey,
51+
) => Promise<{ draft: PluginConfig; focusKey: BackendSettingFocusKey }>;
52+
backendDefaults: PluginConfig;
53+
copy: {
54+
previewHeading: string;
55+
backendCategoriesHeading: string;
56+
resetDefault: string;
57+
saveAndBack: string;
58+
backNoSave: string;
59+
backendTitle: string;
60+
backendSubtitle: string;
61+
backendHelp: string;
62+
};
63+
}): Promise<PluginConfig | null> {
64+
if (!params.isInteractive()) return null;
65+
66+
let draft = params.cloneBackendPluginConfig(params.initial);
67+
let activeCategory = params.backendCategoryOptions[0]?.key ?? "session-sync";
68+
const focusByCategory: Partial<
69+
Record<BackendCategoryKey, BackendSettingFocusKey>
70+
> = {};
71+
for (const category of params.backendCategoryOptions) {
72+
focusByCategory[category.key] =
73+
params.getBackendCategoryInitialFocus(category);
74+
}
75+
76+
while (true) {
77+
const previewFocus = focusByCategory[activeCategory] ?? null;
78+
const preview = params.buildBackendSettingsPreview(
79+
draft,
80+
params.ui,
81+
previewFocus,
82+
{
83+
highlightPreviewToken: params.highlightPreviewToken,
84+
},
85+
);
86+
const categoryItems: MenuItem<BackendSettingsHubAction>[] =
87+
params.backendCategoryOptions.map((category, index) => ({
88+
label: `${index + 1}. ${category.label}`,
89+
hint: category.description,
90+
value: { type: "open-category", key: category.key },
91+
color: "green",
92+
}));
93+
94+
const items: MenuItem<BackendSettingsHubAction>[] = [
95+
{
96+
label: params.copy.previewHeading,
97+
value: { type: "cancel" },
98+
kind: "heading",
99+
},
100+
{
101+
label: preview.label,
102+
hint: preview.hint,
103+
value: { type: "cancel" },
104+
disabled: true,
105+
color: "green",
106+
hideUnavailableSuffix: true,
107+
},
108+
{ label: "", value: { type: "cancel" }, separator: true },
109+
{
110+
label: params.copy.backendCategoriesHeading,
111+
value: { type: "cancel" },
112+
kind: "heading",
113+
},
114+
...categoryItems,
115+
{ label: "", value: { type: "cancel" }, separator: true },
116+
{
117+
label: params.copy.resetDefault,
118+
value: { type: "reset" },
119+
color: "yellow",
120+
},
121+
{
122+
label: params.copy.saveAndBack,
123+
value: { type: "save" },
124+
color: "green",
125+
},
126+
{
127+
label: params.copy.backNoSave,
128+
value: { type: "cancel" },
129+
color: "red",
130+
},
131+
];
132+
133+
const initialCursor = items.findIndex((item) => {
134+
if (item.separator || item.disabled || item.kind === "heading")
135+
return false;
136+
return (
137+
item.value.type === "open-category" && item.value.key === activeCategory
138+
);
139+
});
140+
141+
const result = await params.select(items, {
142+
message: params.copy.backendTitle,
143+
subtitle: params.copy.backendSubtitle,
144+
help: params.copy.backendHelp,
145+
clearScreen: true,
146+
theme: params.ui.theme,
147+
selectedEmphasis: "minimal",
148+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
149+
onCursorChange: ({ cursor }) => {
150+
const focusedItem = items[cursor];
151+
if (focusedItem?.value.type === "open-category") {
152+
activeCategory = focusedItem.value.key;
153+
}
154+
},
155+
onInput: (raw) => {
156+
const lower = raw.toLowerCase();
157+
if (lower === "q") return { type: "cancel" as const };
158+
if (lower === "s") return { type: "save" as const };
159+
if (lower === "r") return { type: "reset" as const };
160+
const parsed = Number.parseInt(raw, 10);
161+
if (
162+
Number.isFinite(parsed) &&
163+
parsed >= 1 &&
164+
parsed <= params.backendCategoryOptions.length
165+
) {
166+
const target = params.backendCategoryOptions[parsed - 1];
167+
if (target)
168+
return { type: "open-category" as const, key: target.key };
169+
}
170+
return undefined;
171+
},
172+
});
173+
174+
if (!result || result.type === "cancel") return null;
175+
if (result.type === "save") return draft;
176+
if (result.type === "reset") {
177+
draft = params.cloneBackendPluginConfig(params.backendDefaults);
178+
for (const category of params.backendCategoryOptions) {
179+
focusByCategory[category.key] =
180+
params.getBackendCategoryInitialFocus(category);
181+
}
182+
activeCategory = params.backendCategoryOptions[0]?.key ?? activeCategory;
183+
continue;
184+
}
185+
186+
const category = params.getBackendCategory(
187+
result.key,
188+
params.backendCategoryOptions,
189+
);
190+
if (!category) continue;
191+
activeCategory = category.key;
192+
const categoryResult = await params.promptBackendCategorySettings(
193+
draft,
194+
category,
195+
focusByCategory[category.key] ??
196+
params.getBackendCategoryInitialFocus(category),
197+
);
198+
draft = categoryResult.draft;
199+
focusByCategory[category.key] = categoryResult.focusKey;
200+
}
201+
}

lib/codex-manager/settings-hub.ts

Lines changed: 17 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { loadAccounts, normalizeAccountStorage } from "../storage.js";
2020
import type { PluginConfig } from "../types.js";
2121
import { UI_COPY } from "../ui/copy.js";
2222
import { getUiRuntimeOptions, setUiRuntimeOptions } from "../ui/runtime.js";
23-
import { type MenuItem, select } from "../ui/select.js";
23+
import { select } from "../ui/select.js";
2424
import { sleep } from "../utils.js";
2525
import {
2626
applyBackendCategoryDefaults,
@@ -39,16 +39,15 @@ import {
3939
cloneBackendPluginConfig,
4040
formatBackendNumberValue,
4141
} from "./backend-settings-helpers.js";
42+
import { promptBackendSettingsMenu } from "./backend-settings-prompt.js";
4243
import {
4344
BACKEND_CATEGORY_OPTIONS,
4445
BACKEND_DEFAULTS,
4546
BACKEND_NUMBER_OPTION_BY_KEY,
4647
BACKEND_TOGGLE_OPTION_BY_KEY,
47-
type BackendCategoryKey,
4848
type BackendCategoryOption,
4949
type BackendNumberSettingOption,
5050
type BackendSettingFocusKey,
51-
type BackendSettingsHubAction,
5251
} from "./backend-settings-schema.js";
5352
import { promptBehaviorSettingsPanel } from "./behavior-settings-panel.js";
5453
import { promptDashboardDisplayPanel } from "./dashboard-display-panel.js";
@@ -622,135 +621,21 @@ async function promptBackendCategorySettings(
622621
async function promptBackendSettings(
623622
initial: PluginConfig,
624623
): Promise<PluginConfig | null> {
625-
if (!input.isTTY || !output.isTTY) return null;
626-
627-
const ui = getUiRuntimeOptions();
628-
let draft = cloneBackendPluginConfig(initial);
629-
let activeCategory = BACKEND_CATEGORY_OPTIONS[0]?.key ?? "session-sync";
630-
const focusByCategory: Partial<
631-
Record<BackendCategoryKey, BackendSettingFocusKey>
632-
> = {};
633-
for (const category of BACKEND_CATEGORY_OPTIONS) {
634-
focusByCategory[category.key] = getBackendCategoryInitialFocus(category);
635-
}
636-
637-
while (true) {
638-
const previewFocus = focusByCategory[activeCategory] ?? null;
639-
const preview = buildBackendSettingsPreview(draft, ui, previewFocus, {
640-
highlightPreviewToken,
641-
});
642-
const categoryItems: MenuItem<BackendSettingsHubAction>[] =
643-
BACKEND_CATEGORY_OPTIONS.map((category, index) => {
644-
return {
645-
label: `${index + 1}. ${category.label}`,
646-
hint: category.description,
647-
value: { type: "open-category", key: category.key },
648-
color: "green",
649-
};
650-
});
651-
652-
const items: MenuItem<BackendSettingsHubAction>[] = [
653-
{
654-
label: UI_COPY.settings.previewHeading,
655-
value: { type: "cancel" },
656-
kind: "heading",
657-
},
658-
{
659-
label: preview.label,
660-
hint: preview.hint,
661-
value: { type: "cancel" },
662-
disabled: true,
663-
color: "green",
664-
hideUnavailableSuffix: true,
665-
},
666-
{ label: "", value: { type: "cancel" }, separator: true },
667-
{
668-
label: UI_COPY.settings.backendCategoriesHeading,
669-
value: { type: "cancel" },
670-
kind: "heading",
671-
},
672-
...categoryItems,
673-
{ label: "", value: { type: "cancel" }, separator: true },
674-
{
675-
label: UI_COPY.settings.resetDefault,
676-
value: { type: "reset" },
677-
color: "yellow",
678-
},
679-
{
680-
label: UI_COPY.settings.saveAndBack,
681-
value: { type: "save" },
682-
color: "green",
683-
},
684-
{
685-
label: UI_COPY.settings.backNoSave,
686-
value: { type: "cancel" },
687-
color: "red",
688-
},
689-
];
690-
691-
const initialCursor = items.findIndex((item) => {
692-
if (item.separator || item.disabled || item.kind === "heading")
693-
return false;
694-
return (
695-
item.value.type === "open-category" && item.value.key === activeCategory
696-
);
697-
});
698-
699-
const result = await select<BackendSettingsHubAction>(items, {
700-
message: UI_COPY.settings.backendTitle,
701-
subtitle: UI_COPY.settings.backendSubtitle,
702-
help: UI_COPY.settings.backendHelp,
703-
clearScreen: true,
704-
theme: ui.theme,
705-
selectedEmphasis: "minimal",
706-
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
707-
onCursorChange: ({ cursor }) => {
708-
const focusedItem = items[cursor];
709-
if (focusedItem?.value.type === "open-category") {
710-
activeCategory = focusedItem.value.key;
711-
}
712-
},
713-
onInput: (raw) => {
714-
const lower = raw.toLowerCase();
715-
if (lower === "q") return { type: "cancel" };
716-
if (lower === "s") return { type: "save" };
717-
if (lower === "r") return { type: "reset" };
718-
const parsed = Number.parseInt(raw, 10);
719-
if (
720-
Number.isFinite(parsed) &&
721-
parsed >= 1 &&
722-
parsed <= BACKEND_CATEGORY_OPTIONS.length
723-
) {
724-
const target = BACKEND_CATEGORY_OPTIONS[parsed - 1];
725-
if (target) return { type: "open-category", key: target.key };
726-
}
727-
return undefined;
728-
},
729-
});
730-
731-
if (!result || result.type === "cancel") return null;
732-
if (result.type === "save") return draft;
733-
if (result.type === "reset") {
734-
draft = cloneBackendPluginConfig(BACKEND_DEFAULTS);
735-
for (const category of BACKEND_CATEGORY_OPTIONS) {
736-
focusByCategory[category.key] =
737-
getBackendCategoryInitialFocus(category);
738-
}
739-
activeCategory = BACKEND_CATEGORY_OPTIONS[0]?.key ?? activeCategory;
740-
continue;
741-
}
742-
743-
const category = getBackendCategory(result.key, BACKEND_CATEGORY_OPTIONS);
744-
if (!category) continue;
745-
activeCategory = category.key;
746-
const categoryResult = await promptBackendCategorySettings(
747-
draft,
748-
category,
749-
focusByCategory[category.key] ?? getBackendCategoryInitialFocus(category),
750-
);
751-
draft = categoryResult.draft;
752-
focusByCategory[category.key] = categoryResult.focusKey;
753-
}
624+
return promptBackendSettingsMenu({
625+
initial,
626+
isInteractive: () => input.isTTY && output.isTTY,
627+
ui: getUiRuntimeOptions(),
628+
cloneBackendPluginConfig,
629+
backendCategoryOptions: BACKEND_CATEGORY_OPTIONS,
630+
getBackendCategoryInitialFocus,
631+
buildBackendSettingsPreview,
632+
highlightPreviewToken,
633+
select,
634+
getBackendCategory,
635+
promptBackendCategorySettings,
636+
backendDefaults: BACKEND_DEFAULTS,
637+
copy: UI_COPY.settings,
638+
});
754639
}
755640

756641
async function loadExperimentalSyncTarget(): Promise<

0 commit comments

Comments
 (0)