FN-7346: fix TUI settings arrow editing
Fix Settings keyboard handling so arrow keys edit selected enum values in the terminal dashboard. - Keep Tab as the Settings pane switcher so arrow keys remain scoped to the active pane. - Route detail-pane left/right arrows and h/l keys through enum setting cycling and remote provider activation. - Add terminal dashboard coverage for list navigation and enum cycling, plus CLI docs and release note coverage. Files changed: .changeset/fn-7346-tui-settings-arrows.md | 7 ++ docs/cli-reference.md | 5 + .../commands/dashboard-tui/__tests__/app.test.tsx | 121 ++++++++++++++++++++- packages/cli/src/commands/dashboard-tui/app.tsx | 119 ++++++++++---------- 4 files changed, 186 insertions(+), 66 deletions(-) Fusion-Task-Id: FN-7346 Fusion-Task-Lineage: c67861a7-7bb3-408d-926c-e85a44fe2dec Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7346-tui-settings-arrows.md
Normal file
7
.changeset/fn-7346-tui-settings-arrows.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Fix arrow-key editing for Settings in the terminal dashboard.
|
||||||
|
category: fix
|
||||||
|
dev: Settings detail-pane arrows now edit enum values instead of switching panes.
|
||||||
@@ -270,6 +270,11 @@ Remote actions support:
|
|||||||
|
|
||||||
> ⚠️ Remote URL/QR payloads include tokenized query data. Treat them like credentials and avoid sharing them in screenshots/chat/logs. Prefer short-lived links for ad-hoc phone login.
|
> ⚠️ Remote URL/QR payloads include tokenized query data. Treat them like credentials and avoid sharing them in screenshots/chat/logs. Prefer short-lived links for ad-hoc phone login.
|
||||||
|
|
||||||
|
Settings pane navigation and editing:
|
||||||
|
- `Tab` switches focus between the settings list and the detail/edit pane.
|
||||||
|
- In the settings list, `↑`/`↓` or `k`/`j` moves the selected setting.
|
||||||
|
- In the detail/edit pane, `←`/`→` or `h`/`l` cycles enum values such as **Remote Provider**; `Space` toggles booleans; `+`/`-` adjusts numbers.
|
||||||
|
|
||||||
Remote action keys in Settings detail pane:
|
Remote action keys in Settings detail pane:
|
||||||
- `C` activate selected provider
|
- `C` activate selected provider
|
||||||
- `V` start tunnel
|
- `V` start tunnel
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ function makeInteractiveData(opts: {
|
|||||||
models?: ModelItem[];
|
models?: ModelItem[];
|
||||||
taskDetail?: TaskDetailData | null;
|
taskDetail?: TaskDetailData | null;
|
||||||
updateAgentState?: (id: string, state: string) => Promise<void>;
|
updateAgentState?: (id: string, state: string) => Promise<void>;
|
||||||
|
updateSettings?: (partial: Partial<SettingsValues>) => Promise<void>;
|
||||||
remote?: Partial<{
|
remote?: Partial<{
|
||||||
getSettings: () => Promise<{ activeProvider: "tailscale" | "cloudflare" | null; tailscaleEnabled: boolean; cloudflareEnabled: boolean; shortLivedEnabled: boolean; shortLivedTtlMs: number }>;
|
getSettings: () => Promise<{ activeProvider: "tailscale" | "cloudflare" | null; tailscaleEnabled: boolean; cloudflareEnabled: boolean; shortLivedEnabled: boolean; shortLivedTtlMs: number }>;
|
||||||
getStatus: () => Promise<{ provider: "tailscale" | "cloudflare" | null; state: "stopped" | "starting" | "running" | "error"; url: string | null; lastError: string | null }>;
|
getStatus: () => Promise<{ provider: "tailscale" | "cloudflare" | null; state: "stopped" | "starting" | "running" | "error"; url: string | null; lastError: string | null }>;
|
||||||
@@ -63,7 +64,7 @@ function makeInteractiveData(opts: {
|
|||||||
const agents = opts.agents ?? [];
|
const agents = opts.agents ?? [];
|
||||||
const detail = opts.detail ?? null;
|
const detail = opts.detail ?? null;
|
||||||
const taskDetail = opts.taskDetail ?? null;
|
const taskDetail = opts.taskDetail ?? null;
|
||||||
const settings: SettingsValues = opts.settings ?? {
|
let settings: SettingsValues = opts.settings ?? {
|
||||||
maxConcurrent: 1,
|
maxConcurrent: 1,
|
||||||
maxWorktrees: 2,
|
maxWorktrees: 2,
|
||||||
autoMerge: false,
|
autoMerge: false,
|
||||||
@@ -109,7 +110,9 @@ function makeInteractiveData(opts: {
|
|||||||
updateAgentState: opts.updateAgentState ?? (async (_id: string, _state: string) => {}),
|
updateAgentState: opts.updateAgentState ?? (async (_id: string, _state: string) => {}),
|
||||||
deleteAgent: async (_id: string) => {},
|
deleteAgent: async (_id: string) => {},
|
||||||
getSettings: async () => settings,
|
getSettings: async () => settings,
|
||||||
updateSettings: async (_partial: Partial<SettingsValues>) => {},
|
updateSettings: opts.updateSettings ?? (async (partial: Partial<SettingsValues>) => {
|
||||||
|
settings = { ...settings, ...partial };
|
||||||
|
}),
|
||||||
listModels: () => models,
|
listModels: () => models,
|
||||||
remote,
|
remote,
|
||||||
git: {
|
git: {
|
||||||
@@ -188,11 +191,20 @@ async function waitForFrameUpdateAfterInput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function focusSettingsDetailPane(stdin: { write: (chunk: string) => void }, lastFrame: () => string | undefined) {
|
async function focusSettingsDetailPane(stdin: { write: (chunk: string) => void }, lastFrame: () => string | undefined) {
|
||||||
stdin.write("\u001b[C");
|
stdin.write("\t");
|
||||||
await waitForFrameUpdateAfterInput();
|
await waitForFrameUpdateAfterInput();
|
||||||
await waitForFrameContains(lastFrame, "[C/V/X/P/L/U/K/R] remote actions");
|
await waitForFrameContains(lastFrame, "[C/V/X/P/L/U/K/R] remote actions");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function selectSettingsRow(stdin: { write: (chunk: string) => void }, label: string, rowOffsetFromTop: number, lastFrame: () => string | undefined) {
|
||||||
|
await waitForFrameContains(lastFrame, label);
|
||||||
|
for (let i = 0; i < rowOffsetFromTop; i += 1) {
|
||||||
|
stdin.write("\u001b[B");
|
||||||
|
await waitForFrameUpdateAfterInput();
|
||||||
|
}
|
||||||
|
await waitForFrameContains(lastFrame, label);
|
||||||
|
}
|
||||||
|
|
||||||
function findTokenPosition(frame: string, token: string): { row: number; col: number } {
|
function findTokenPosition(frame: string, token: string): { row: number; col: number } {
|
||||||
const lines = frame.split("\n");
|
const lines = frame.split("\n");
|
||||||
const row = lines.findIndex((line) => line.includes(token));
|
const row = lines.findIndex((line) => line.includes(token));
|
||||||
@@ -556,6 +568,109 @@ describe("Settings view", () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps list-pane up/down arrows scoped to settings selection", async () => {
|
||||||
|
const controller = newController();
|
||||||
|
controller.setSystemInfo(makeSystemInfo());
|
||||||
|
controller.setInteractiveData(makeInteractiveData());
|
||||||
|
controller.setMode("interactive");
|
||||||
|
controller.setInteractiveView("settings");
|
||||||
|
|
||||||
|
const { lastFrame, stdin, unmount } = render(renderDashboardAppNode(controller));
|
||||||
|
await waitForFrameContains(lastFrame, "Max Concurrent");
|
||||||
|
|
||||||
|
stdin.write("\u001b[B");
|
||||||
|
await waitForFrameUpdateAfterInput();
|
||||||
|
expect(lastFrame() ?? "").toContain("▶ Max Worktrees");
|
||||||
|
|
||||||
|
stdin.write("\u001b[A");
|
||||||
|
await waitForFrameUpdateAfterInput();
|
||||||
|
expect(lastFrame() ?? "").toContain("▶ Max Concurrent");
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cycles Remote Provider with right arrow in the focused detail pane", async () => {
|
||||||
|
const controller = newController();
|
||||||
|
controller.setSystemInfo(makeSystemInfo());
|
||||||
|
const settings: SettingsValues = {
|
||||||
|
maxConcurrent: 1,
|
||||||
|
maxWorktrees: 2,
|
||||||
|
autoMerge: false,
|
||||||
|
mergeStrategy: "direct",
|
||||||
|
pollIntervalMs: 60000,
|
||||||
|
enginePaused: false,
|
||||||
|
globalPause: false,
|
||||||
|
remoteActiveProvider: "tailscale",
|
||||||
|
remoteShortLivedEnabled: true,
|
||||||
|
remoteShortLivedTtlMs: 600000,
|
||||||
|
remoteStatus: { provider: "tailscale", state: "running", url: "https://remote.example.com", lastError: null },
|
||||||
|
};
|
||||||
|
const updateSettings = vi.fn(async (partial: Partial<SettingsValues>) => {
|
||||||
|
Object.assign(settings, partial);
|
||||||
|
});
|
||||||
|
const activateProvider = vi.fn(async (_provider: "tailscale" | "cloudflare") => {});
|
||||||
|
controller.setInteractiveData(makeInteractiveData({
|
||||||
|
settings,
|
||||||
|
updateSettings,
|
||||||
|
remote: { activateProvider },
|
||||||
|
}));
|
||||||
|
controller.setMode("interactive");
|
||||||
|
controller.setInteractiveView("settings");
|
||||||
|
|
||||||
|
const { lastFrame, stdin, unmount } = render(renderDashboardAppNode(controller));
|
||||||
|
await selectSettingsRow(stdin, "Remote Provid", 7, lastFrame);
|
||||||
|
await focusSettingsDetailPane(stdin, lastFrame);
|
||||||
|
|
||||||
|
stdin.write("\u001b[C");
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(updateSettings).toHaveBeenCalledWith({ remoteActiveProvider: "cloudflare" }));
|
||||||
|
await vi.waitFor(() => expect(activateProvider).toHaveBeenCalledWith("cloudflare"));
|
||||||
|
await waitForFrameContains(lastFrame, "Provider: cloudflare");
|
||||||
|
expect(lastFrame() ?? "").toContain("[←/→] cycle options:");
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cycles merge strategy with vim-style detail-pane enum keys", async () => {
|
||||||
|
const controller = newController();
|
||||||
|
controller.setSystemInfo(makeSystemInfo());
|
||||||
|
const settings: SettingsValues = {
|
||||||
|
maxConcurrent: 1,
|
||||||
|
maxWorktrees: 2,
|
||||||
|
autoMerge: false,
|
||||||
|
mergeStrategy: "direct",
|
||||||
|
pollIntervalMs: 60000,
|
||||||
|
enginePaused: false,
|
||||||
|
globalPause: false,
|
||||||
|
remoteActiveProvider: null,
|
||||||
|
remoteShortLivedEnabled: false,
|
||||||
|
remoteShortLivedTtlMs: 900000,
|
||||||
|
};
|
||||||
|
const updateSettings = vi.fn(async (partial: Partial<SettingsValues>) => {
|
||||||
|
Object.assign(settings, partial);
|
||||||
|
});
|
||||||
|
controller.setInteractiveData(makeInteractiveData({
|
||||||
|
updateSettings,
|
||||||
|
settings,
|
||||||
|
}));
|
||||||
|
controller.setMode("interactive");
|
||||||
|
controller.setInteractiveView("settings");
|
||||||
|
|
||||||
|
const { lastFrame, stdin, unmount } = render(renderDashboardAppNode(controller));
|
||||||
|
await selectSettingsRow(stdin, "Merge Strategy", 3, lastFrame);
|
||||||
|
await focusSettingsDetailPane(stdin, lastFrame);
|
||||||
|
|
||||||
|
stdin.write("l");
|
||||||
|
await vi.waitFor(() => expect(updateSettings).toHaveBeenCalledWith({ mergeStrategy: "squash" }));
|
||||||
|
await waitForFrameContains(lastFrame, "squash");
|
||||||
|
|
||||||
|
stdin.write("h");
|
||||||
|
await vi.waitFor(() => expect(updateSettings).toHaveBeenCalledWith({ mergeStrategy: "direct" }));
|
||||||
|
await waitForFrameContains(lastFrame, "direct");
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders the Remote subsection and supports provider/lifecycle actions", async () => {
|
it("renders the Remote subsection and supports provider/lifecycle actions", async () => {
|
||||||
const controller = newController();
|
const controller = newController();
|
||||||
controller.setSystemInfo(makeSystemInfo());
|
controller.setSystemInfo(makeSystemInfo());
|
||||||
|
|||||||
@@ -2562,23 +2562,6 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState;
|
|||||||
}
|
}
|
||||||
|
|
||||||
useInput((input, key) => {
|
useInput((input, key) => {
|
||||||
// Tab cycles list ↔ detail. Left/right also switch — list = left,
|
|
||||||
// detail = right, matching the visual layout (consistent with AgentsView).
|
|
||||||
if (key.tab) {
|
|
||||||
setDetailFocused((f) => !f);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key.leftArrow) {
|
|
||||||
setDetailFocused(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key.rightArrow) {
|
|
||||||
setDetailFocused(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputUpper = input.toUpperCase();
|
|
||||||
|
|
||||||
if (ttlInputMode) {
|
if (ttlInputMode) {
|
||||||
if (key.escape) {
|
if (key.escape) {
|
||||||
setTtlInputMode(false);
|
setTtlInputMode(false);
|
||||||
@@ -2587,7 +2570,16 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputUpper === "R") {
|
/*
|
||||||
|
FNXC:TuiSettingsKeyboard 2026-06-30-22:52:
|
||||||
|
Tab is the only Settings pane-switch key. Arrow keys must remain available to the focused pane so Windows terminals can use ←/→ to edit enum settings such as Remote Provider instead of getting trapped switching focus.
|
||||||
|
*/
|
||||||
|
if (key.tab) {
|
||||||
|
setDetailFocused((f) => !f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input === "R") {
|
||||||
void refreshRemoteStatus();
|
void refreshRemoteStatus();
|
||||||
setStatusMsg(t("tui.settingsRemoteStatusRefreshed", "Remote status refreshed"));
|
setStatusMsg(t("tui.settingsRemoteStatusRefreshed", "Remote status refreshed"));
|
||||||
return;
|
return;
|
||||||
@@ -2595,62 +2587,6 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState;
|
|||||||
|
|
||||||
if (!localSettings) return;
|
if (!localSettings) return;
|
||||||
|
|
||||||
if (data?.remote && inputUpper === "C") {
|
|
||||||
const provider = localSettings.remoteActiveProvider;
|
|
||||||
if (!provider) {
|
|
||||||
setStatusMsg(t("tui.settingsSelectProviderFirst", "Select a remote provider first"));
|
|
||||||
} else {
|
|
||||||
void data.remote.activateProvider(provider)
|
|
||||||
.then(() => refreshRemoteStatus())
|
|
||||||
.then(() => setStatusMsg(t("tui.settingsActivatedProvider", "Activated provider: {{provider}}", { provider })))
|
|
||||||
.catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.remote && inputUpper === "V") {
|
|
||||||
void data.remote.startTunnel().then(() => refreshRemoteStatus()).then(() => setStatusMsg(t("tui.settingsTunnelStarting", "Remote tunnel starting")))
|
|
||||||
.catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.remote && inputUpper === "X") {
|
|
||||||
void data.remote.stopTunnel().then(() => refreshRemoteStatus()).then(() => setStatusMsg(t("tui.settingsTunnelStopped", "Remote tunnel stopped")))
|
|
||||||
.catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.remote && inputUpper === "P") {
|
|
||||||
void data.remote.regeneratePersistentToken()
|
|
||||||
.then((result) => {
|
|
||||||
setPersistentMaskedToken(result.maskedToken ?? null);
|
|
||||||
setStatusMsg(t("tui.settingsPersistentTokenRegenerated", "Persistent token regenerated"));
|
|
||||||
})
|
|
||||||
.catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.remote && inputUpper === "L") {
|
|
||||||
setTtlInputValue(String(localSettings.remoteShortLivedTtlMs));
|
|
||||||
setTtlInputMode(true);
|
|
||||||
setStatusMsg(t("tui.settingsEnterTtl", "Enter TTL milliseconds and press Enter"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.remote && inputUpper === "U") {
|
|
||||||
void handleFetchRemoteUrl("persistent")
|
|
||||||
.then(() => setStatusMsg(t("tui.settingsRemoteUrlFetched", "Remote URL fetched")))
|
|
||||||
.catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.remote && inputUpper === "K") {
|
|
||||||
void handleFetchRemoteQr("persistent")
|
|
||||||
.then(() => setStatusMsg(t("tui.settingsQrFetched", "QR payload fetched")))
|
|
||||||
.catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!detailFocused) {
|
if (!detailFocused) {
|
||||||
if (key.upArrow || input === "k") {
|
if (key.upArrow || input === "k") {
|
||||||
setSelectedIndex((i) => Math.max(0, i - 1));
|
setSelectedIndex((i) => Math.max(0, i - 1));
|
||||||
@@ -2665,6 +2601,87 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState;
|
|||||||
|
|
||||||
if (!selectedDef) return;
|
if (!selectedDef) return;
|
||||||
|
|
||||||
|
if (selectedDef.type === "enum" && selectedDef.options) {
|
||||||
|
const current = localSettings[selectedDef.key];
|
||||||
|
const idx = typeof current === "string" ? selectedDef.options.indexOf(current) : -1;
|
||||||
|
if (key.rightArrow || input === "l") {
|
||||||
|
const next = selectedDef.options[(idx + 1) % selectedDef.options.length];
|
||||||
|
const updated = { ...localSettings, [selectedDef.key]: next };
|
||||||
|
setLocalSettings(updated);
|
||||||
|
void saveField({ [selectedDef.key]: next });
|
||||||
|
if (selectedDef.key === "remoteActiveProvider" && data?.remote) {
|
||||||
|
void data.remote.activateProvider(next as "tailscale" | "cloudflare").catch(() => {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key.leftArrow || input === "h") {
|
||||||
|
const prev = selectedDef.options[(idx - 1 + selectedDef.options.length) % selectedDef.options.length];
|
||||||
|
const updated = { ...localSettings, [selectedDef.key]: prev };
|
||||||
|
setLocalSettings(updated);
|
||||||
|
void saveField({ [selectedDef.key]: prev });
|
||||||
|
if (selectedDef.key === "remoteActiveProvider" && data?.remote) {
|
||||||
|
void data.remote.activateProvider(prev as "tailscale" | "cloudflare").catch(() => {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.remote && input === "C") {
|
||||||
|
const provider = localSettings.remoteActiveProvider;
|
||||||
|
if (!provider) {
|
||||||
|
setStatusMsg(t("tui.settingsSelectProviderFirst", "Select a remote provider first"));
|
||||||
|
} else {
|
||||||
|
void data.remote.activateProvider(provider)
|
||||||
|
.then(() => refreshRemoteStatus())
|
||||||
|
.then(() => setStatusMsg(t("tui.settingsActivatedProvider", "Activated provider: {{provider}}", { provider })))
|
||||||
|
.catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.remote && input === "V") {
|
||||||
|
void data.remote.startTunnel().then(() => refreshRemoteStatus()).then(() => setStatusMsg(t("tui.settingsTunnelStarting", "Remote tunnel starting")))
|
||||||
|
.catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.remote && input === "X") {
|
||||||
|
void data.remote.stopTunnel().then(() => refreshRemoteStatus()).then(() => setStatusMsg(t("tui.settingsTunnelStopped", "Remote tunnel stopped")))
|
||||||
|
.catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.remote && input === "P") {
|
||||||
|
void data.remote.regeneratePersistentToken()
|
||||||
|
.then((result) => {
|
||||||
|
setPersistentMaskedToken(result.maskedToken ?? null);
|
||||||
|
setStatusMsg(t("tui.settingsPersistentTokenRegenerated", "Persistent token regenerated"));
|
||||||
|
})
|
||||||
|
.catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.remote && input === "L") {
|
||||||
|
setTtlInputValue(String(localSettings.remoteShortLivedTtlMs));
|
||||||
|
setTtlInputMode(true);
|
||||||
|
setStatusMsg(t("tui.settingsEnterTtl", "Enter TTL milliseconds and press Enter"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.remote && input === "U") {
|
||||||
|
void handleFetchRemoteUrl("persistent")
|
||||||
|
.then(() => setStatusMsg(t("tui.settingsRemoteUrlFetched", "Remote URL fetched")))
|
||||||
|
.catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.remote && input === "K") {
|
||||||
|
void handleFetchRemoteQr("persistent")
|
||||||
|
.then(() => setStatusMsg(t("tui.settingsQrFetched", "QR payload fetched")))
|
||||||
|
.catch((err) => setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedDef.type === "boolean" && input === " ") {
|
if (selectedDef.type === "boolean" && input === " ") {
|
||||||
const current = localSettings[selectedDef.key] as boolean;
|
const current = localSettings[selectedDef.key] as boolean;
|
||||||
const updated = { ...localSettings, [selectedDef.key]: !current };
|
const updated = { ...localSettings, [selectedDef.key]: !current };
|
||||||
@@ -2692,30 +2709,6 @@ function SettingsInteractiveView({ state, controller }: { state: DashboardState;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedDef.type === "enum" && selectedDef.options) {
|
|
||||||
const current = localSettings[selectedDef.key] as string;
|
|
||||||
const idx = selectedDef.options.indexOf(current);
|
|
||||||
if (key.rightArrow || input === "l") {
|
|
||||||
const next = selectedDef.options[(idx + 1) % selectedDef.options.length];
|
|
||||||
const updated = { ...localSettings, [selectedDef.key]: next };
|
|
||||||
setLocalSettings(updated);
|
|
||||||
void saveField({ [selectedDef.key]: next });
|
|
||||||
if (selectedDef.key === "remoteActiveProvider" && data?.remote) {
|
|
||||||
void data.remote.activateProvider(next as "tailscale" | "cloudflare").catch(() => {});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key.leftArrow || input === "h") {
|
|
||||||
const prev = selectedDef.options[(idx - 1 + selectedDef.options.length) % selectedDef.options.length];
|
|
||||||
const updated = { ...localSettings, [selectedDef.key]: prev };
|
|
||||||
setLocalSettings(updated);
|
|
||||||
void saveField({ [selectedDef.key]: prev });
|
|
||||||
if (selectedDef.key === "remoteActiveProvider" && data?.remote) {
|
|
||||||
void data.remote.activateProvider(prev as "tailscale" | "cloudflare").catch(() => {});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderValue(def: SettingDef, settings: SettingsValues): React.ReactNode {
|
function renderValue(def: SettingDef, settings: SettingsValues): React.ReactNode {
|
||||||
|
|||||||
Reference in New Issue
Block a user