codex-multi-auth 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -98
- package/assets/readme-hero.svg +2 -2
- package/config/README.md +30 -87
- package/config/{opencode-legacy.json → codex-legacy.json} +571 -571
- package/config/{opencode-modern.json → codex-modern.json} +241 -239
- package/config/{minimal-opencode.json → minimal-codex.json} +15 -13
- package/config/schema/config.schema.json +21 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -20
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts.d.ts +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/auth/auth.d.ts +1 -1
- package/dist/lib/auth/auth.js +1 -1
- package/dist/lib/cli.d.ts +1 -1
- package/dist/lib/cli.js +4 -4
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/codex-manager/settings-hub.d.ts +6 -0
- package/dist/lib/codex-manager/settings-hub.d.ts.map +1 -0
- package/dist/lib/codex-manager/settings-hub.js +1786 -0
- package/dist/lib/codex-manager/settings-hub.js.map +1 -0
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +5 -1637
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +20 -11
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/context-overflow.d.ts +1 -1
- package/dist/lib/context-overflow.js +1 -1
- package/dist/lib/oauth-success.html +1 -1
- package/dist/lib/prompts/codex-host-bridge.d.ts +19 -0
- package/dist/lib/prompts/codex-host-bridge.d.ts.map +1 -0
- package/dist/lib/prompts/{codex-opencode-bridge.js → codex-host-bridge.js} +143 -143
- package/dist/lib/prompts/codex-host-bridge.js.map +1 -0
- package/dist/lib/prompts/codex.d.ts +2 -2
- package/dist/lib/prompts/codex.d.ts.map +1 -1
- package/dist/lib/prompts/codex.js +3 -3
- package/dist/lib/prompts/host-codex-prompt.d.ts +25 -0
- package/dist/lib/prompts/host-codex-prompt.d.ts.map +1 -0
- package/dist/lib/prompts/{opencode-codex.js → host-codex-prompt.js} +136 -47
- package/dist/lib/prompts/host-codex-prompt.js.map +1 -0
- package/dist/lib/recovery/storage.d.ts +2 -2
- package/dist/lib/recovery/storage.js +2 -2
- package/dist/lib/recovery/types.d.ts +1 -1
- package/dist/lib/recovery/types.js +1 -1
- package/dist/lib/recovery.d.ts +1 -1
- package/dist/lib/recovery.d.ts.map +1 -1
- package/dist/lib/recovery.js +1 -4
- package/dist/lib/recovery.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +3 -3
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +6 -2
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/helpers/input-utils.d.ts +2 -2
- package/dist/lib/request/helpers/input-utils.d.ts.map +1 -1
- package/dist/lib/request/helpers/input-utils.js +14 -14
- package/dist/lib/request/helpers/input-utils.js.map +1 -1
- package/dist/lib/request/helpers/model-map.d.ts +2 -2
- package/dist/lib/request/helpers/model-map.js +2 -2
- package/dist/lib/request/request-transformer.d.ts +12 -12
- package/dist/lib/request/request-transformer.d.ts.map +1 -1
- package/dist/lib/request/request-transformer.js +23 -24
- package/dist/lib/request/request-transformer.js.map +1 -1
- package/dist/lib/runtime-paths.d.ts +4 -4
- package/dist/lib/runtime-paths.d.ts.map +1 -1
- package/dist/lib/runtime-paths.js +27 -9
- package/dist/lib/runtime-paths.js.map +1 -1
- package/dist/lib/storage/paths.d.ts +11 -0
- package/dist/lib/storage/paths.d.ts.map +1 -1
- package/dist/lib/storage/paths.js +146 -2
- package/dist/lib/storage/paths.js.map +1 -1
- package/dist/lib/storage.d.ts +1 -0
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +106 -32
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/tools/hashline-tools.d.ts +1 -1
- package/dist/lib/tools/hashline-tools.d.ts.map +1 -1
- package/dist/lib/tools/hashline-tools.js +1 -1
- package/dist/lib/tools/hashline-tools.js.map +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/ui/copy.d.ts +6 -6
- package/dist/lib/ui/copy.js +6 -6
- package/dist/lib/ui/copy.js.map +1 -1
- package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -0
- package/node_modules/@codex-ai/plugin/dist/index.js +2 -0
- package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -0
- package/node_modules/@codex-ai/plugin/dist/tool.js +29 -0
- package/node_modules/@codex-ai/plugin/package.json +9 -0
- package/package.json +30 -16
- package/scripts/bench-format/{opencode.mjs → codex-host.mjs} +206 -205
- package/scripts/bench-format/models.mjs +111 -105
- package/scripts/benchmark-edit-formats.mjs +1162 -1161
- package/scripts/codex.js +40 -2
- package/scripts/install-codex-auth-utils.js +49 -0
- package/scripts/{install-opencode-codex-auth.js → install-codex-auth.js} +220 -193
- package/scripts/repo-hygiene.js +320 -0
- package/scripts/test-model-matrix.js +475 -423
- package/vendor/codex-ai-plugin/dist/index.d.ts +2 -0
- package/vendor/codex-ai-plugin/dist/index.js +2 -0
- package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -0
- package/vendor/codex-ai-plugin/dist/tool.js +29 -0
- package/vendor/codex-ai-plugin/package.json +9 -0
- package/vendor/codex-ai-sdk/dist/index.d.ts +4 -0
- package/vendor/codex-ai-sdk/dist/index.js +2 -0
- package/vendor/codex-ai-sdk/package.json +8 -0
- package/assets/opencode-logo-ornate-dark.svg +0 -18
- package/dist/lib/prompts/codex-opencode-bridge.d.ts +0 -19
- package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +0 -1
- package/dist/lib/prompts/codex-opencode-bridge.js.map +0 -1
- package/dist/lib/prompts/opencode-codex.d.ts +0 -25
- package/dist/lib/prompts/opencode-codex.d.ts.map +0 -1
- package/dist/lib/prompts/opencode-codex.js.map +0 -1
|
@@ -0,0 +1,1786 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
+
import { loadDashboardDisplaySettings, saveDashboardDisplaySettings, getDashboardSettingsPath, DEFAULT_DASHBOARD_DISPLAY_SETTINGS, } from "../dashboard-settings.js";
|
|
3
|
+
import { getDefaultPluginConfig, loadPluginConfig, savePluginConfig } from "../config.js";
|
|
4
|
+
import { getUnifiedSettingsPath } from "../unified-settings.js";
|
|
5
|
+
import { sleep } from "../utils.js";
|
|
6
|
+
import { ANSI } from "../ui/ansi.js";
|
|
7
|
+
import { UI_COPY } from "../ui/copy.js";
|
|
8
|
+
import { getUiRuntimeOptions, setUiRuntimeOptions } from "../ui/runtime.js";
|
|
9
|
+
import { select } from "../ui/select.js";
|
|
10
|
+
const DASHBOARD_DISPLAY_OPTIONS = [
|
|
11
|
+
{
|
|
12
|
+
key: "menuShowStatusBadge",
|
|
13
|
+
label: "Show Status Badges",
|
|
14
|
+
description: "Show [ok], [active], and similar badges.",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: "menuShowCurrentBadge",
|
|
18
|
+
label: "Show [current]",
|
|
19
|
+
description: "Mark the account active in Codex.",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: "menuShowLastUsed",
|
|
23
|
+
label: "Show Last Used",
|
|
24
|
+
description: "Show relative usage like 'today'.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: "menuShowQuotaSummary",
|
|
28
|
+
label: "Show Limits (5h / 7d)",
|
|
29
|
+
description: "Show limit bars in each row.",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: "menuShowQuotaCooldown",
|
|
33
|
+
label: "Show Limit Cooldowns",
|
|
34
|
+
description: "Show reset timers next to 5h/7d bars.",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "menuShowFetchStatus",
|
|
38
|
+
label: "Show Fetch Status",
|
|
39
|
+
description: "Show background limit refresh status in the menu subtitle.",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "menuHighlightCurrentRow",
|
|
43
|
+
label: "Highlight Current Row",
|
|
44
|
+
description: "Use stronger color on the current row.",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "menuSortEnabled",
|
|
48
|
+
label: "Enable Smart Sort",
|
|
49
|
+
description: "Sort accounts by readiness (view only).",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: "menuSortPinCurrent",
|
|
53
|
+
label: "Pin [current] when tied",
|
|
54
|
+
description: "Keep current at top only when it is equally ready.",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: "menuSortQuickSwitchVisibleRow",
|
|
58
|
+
label: "Quick Switch Uses Visible Rows",
|
|
59
|
+
description: "Number keys (1-9) follow what you see in the list.",
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
const DEFAULT_STATUSLINE_FIELDS = ["last-used", "limits", "status"];
|
|
63
|
+
const STATUSLINE_FIELD_OPTIONS = [
|
|
64
|
+
{
|
|
65
|
+
key: "last-used",
|
|
66
|
+
label: "Show Last Used",
|
|
67
|
+
description: "Example: 'today' or '2d ago'.",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: "limits",
|
|
71
|
+
label: "Show Limits (5h / 7d)",
|
|
72
|
+
description: "Uses cached limit data from checks.",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: "status",
|
|
76
|
+
label: "Show Status Text",
|
|
77
|
+
description: "Visible when badges are hidden.",
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
const AUTO_RETURN_OPTIONS_MS = [1_000, 2_000, 4_000];
|
|
81
|
+
const MENU_QUOTA_TTL_OPTIONS_MS = [60_000, 5 * 60_000, 10 * 60_000];
|
|
82
|
+
const THEME_PRESET_OPTIONS = ["green", "blue"];
|
|
83
|
+
const ACCENT_COLOR_OPTIONS = ["green", "cyan", "blue", "yellow"];
|
|
84
|
+
const PREVIEW_ACCOUNT_EMAIL = "demo@example.com";
|
|
85
|
+
const PREVIEW_LAST_USED = "today";
|
|
86
|
+
const PREVIEW_STATUS = "active";
|
|
87
|
+
const PREVIEW_LIMITS = "5h ██████▒▒▒▒ 62% | 7d █████▒▒▒▒▒ 49%";
|
|
88
|
+
const PREVIEW_LIMIT_COOLDOWNS = "5h reset 1h 20m | 7d reset 2d 04h";
|
|
89
|
+
const BACKEND_TOGGLE_OPTIONS = [
|
|
90
|
+
{
|
|
91
|
+
key: "liveAccountSync",
|
|
92
|
+
label: "Enable Live Sync",
|
|
93
|
+
description: "Keep accounts synced when files change in another window.",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: "sessionAffinity",
|
|
97
|
+
label: "Enable Session Affinity",
|
|
98
|
+
description: "Try to keep each conversation on the same account.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: "proactiveRefreshGuardian",
|
|
102
|
+
label: "Enable Token Refresh Guard",
|
|
103
|
+
description: "Refresh tokens early in the background.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
key: "retryAllAccountsRateLimited",
|
|
107
|
+
label: "Retry When All Rate-Limited",
|
|
108
|
+
description: "If all accounts are limited, wait and try again.",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: "parallelProbing",
|
|
112
|
+
label: "Enable Parallel Probing",
|
|
113
|
+
description: "Check multiple accounts at the same time.",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: "storageBackupEnabled",
|
|
117
|
+
label: "Enable Storage Backups",
|
|
118
|
+
description: "Create a backup before account data changes.",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: "preemptiveQuotaEnabled",
|
|
122
|
+
label: "Enable Quota Deferral",
|
|
123
|
+
description: "Delay requests before limits are fully exhausted.",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
key: "fastSession",
|
|
127
|
+
label: "Enable Fast Session Mode",
|
|
128
|
+
description: "Use lighter request handling for faster responses.",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
key: "sessionRecovery",
|
|
132
|
+
label: "Enable Session Recovery",
|
|
133
|
+
description: "Restore recoverable sessions after restart.",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
key: "autoResume",
|
|
137
|
+
label: "Enable Auto Resume",
|
|
138
|
+
description: "Automatically continue sessions when possible.",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
key: "perProjectAccounts",
|
|
142
|
+
label: "Enable Per-Project Accounts",
|
|
143
|
+
description: "Keep separate account lists for each project.",
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
const BACKEND_NUMBER_OPTIONS = [
|
|
147
|
+
{
|
|
148
|
+
key: "liveAccountSyncDebounceMs",
|
|
149
|
+
label: "Live Sync Debounce",
|
|
150
|
+
description: "Wait this long before applying sync file changes.",
|
|
151
|
+
min: 50,
|
|
152
|
+
max: 10_000,
|
|
153
|
+
step: 50,
|
|
154
|
+
unit: "ms",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: "liveAccountSyncPollMs",
|
|
158
|
+
label: "Live Sync Poll",
|
|
159
|
+
description: "How often to check files for account updates.",
|
|
160
|
+
min: 500,
|
|
161
|
+
max: 60_000,
|
|
162
|
+
step: 500,
|
|
163
|
+
unit: "ms",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
key: "sessionAffinityTtlMs",
|
|
167
|
+
label: "Session Affinity TTL",
|
|
168
|
+
description: "How long conversation-to-account mapping is kept.",
|
|
169
|
+
min: 1_000,
|
|
170
|
+
max: 24 * 60 * 60_000,
|
|
171
|
+
step: 60_000,
|
|
172
|
+
unit: "ms",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
key: "sessionAffinityMaxEntries",
|
|
176
|
+
label: "Session Affinity Max Entries",
|
|
177
|
+
description: "Maximum stored conversation mappings.",
|
|
178
|
+
min: 8,
|
|
179
|
+
max: 4_096,
|
|
180
|
+
step: 32,
|
|
181
|
+
unit: "count",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
key: "proactiveRefreshIntervalMs",
|
|
185
|
+
label: "Refresh Guard Interval",
|
|
186
|
+
description: "How often to scan for tokens near expiry.",
|
|
187
|
+
min: 5_000,
|
|
188
|
+
max: 10 * 60_000,
|
|
189
|
+
step: 5_000,
|
|
190
|
+
unit: "ms",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
key: "proactiveRefreshBufferMs",
|
|
194
|
+
label: "Refresh Guard Buffer",
|
|
195
|
+
description: "How early to refresh before expiry.",
|
|
196
|
+
min: 30_000,
|
|
197
|
+
max: 10 * 60_000,
|
|
198
|
+
step: 30_000,
|
|
199
|
+
unit: "ms",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
key: "parallelProbingMaxConcurrency",
|
|
203
|
+
label: "Parallel Probe Concurrency",
|
|
204
|
+
description: "Maximum checks running at once.",
|
|
205
|
+
min: 1,
|
|
206
|
+
max: 5,
|
|
207
|
+
step: 1,
|
|
208
|
+
unit: "count",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
key: "fastSessionMaxInputItems",
|
|
212
|
+
label: "Fast Session Max Inputs",
|
|
213
|
+
description: "Max number of input items kept in fast mode.",
|
|
214
|
+
min: 8,
|
|
215
|
+
max: 200,
|
|
216
|
+
step: 2,
|
|
217
|
+
unit: "count",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
key: "networkErrorCooldownMs",
|
|
221
|
+
label: "Network Error Cooldown",
|
|
222
|
+
description: "Wait time after network errors before retry.",
|
|
223
|
+
min: 0,
|
|
224
|
+
max: 120_000,
|
|
225
|
+
step: 500,
|
|
226
|
+
unit: "ms",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
key: "serverErrorCooldownMs",
|
|
230
|
+
label: "Server Error Cooldown",
|
|
231
|
+
description: "Wait time after server errors before retry.",
|
|
232
|
+
min: 0,
|
|
233
|
+
max: 120_000,
|
|
234
|
+
step: 500,
|
|
235
|
+
unit: "ms",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
key: "fetchTimeoutMs",
|
|
239
|
+
label: "Request Timeout",
|
|
240
|
+
description: "Max time to wait for a request.",
|
|
241
|
+
min: 1_000,
|
|
242
|
+
max: 10 * 60_000,
|
|
243
|
+
step: 5_000,
|
|
244
|
+
unit: "ms",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
key: "streamStallTimeoutMs",
|
|
248
|
+
label: "Stream Stall Timeout",
|
|
249
|
+
description: "Max wait before a stuck stream is retried.",
|
|
250
|
+
min: 1_000,
|
|
251
|
+
max: 10 * 60_000,
|
|
252
|
+
step: 5_000,
|
|
253
|
+
unit: "ms",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
key: "tokenRefreshSkewMs",
|
|
257
|
+
label: "Token Refresh Buffer",
|
|
258
|
+
description: "Refresh this long before token expiry.",
|
|
259
|
+
min: 0,
|
|
260
|
+
max: 10 * 60_000,
|
|
261
|
+
step: 10_000,
|
|
262
|
+
unit: "ms",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
key: "preemptiveQuotaRemainingPercent5h",
|
|
266
|
+
label: "5h Remaining Threshold",
|
|
267
|
+
description: "Start delaying when 5h remaining reaches this percent.",
|
|
268
|
+
min: 0,
|
|
269
|
+
max: 100,
|
|
270
|
+
step: 1,
|
|
271
|
+
unit: "percent",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
key: "preemptiveQuotaRemainingPercent7d",
|
|
275
|
+
label: "7d Remaining Threshold",
|
|
276
|
+
description: "Start delaying when weekly remaining reaches this percent.",
|
|
277
|
+
min: 0,
|
|
278
|
+
max: 100,
|
|
279
|
+
step: 1,
|
|
280
|
+
unit: "percent",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
key: "preemptiveQuotaMaxDeferralMs",
|
|
284
|
+
label: "Max Preemptive Deferral",
|
|
285
|
+
description: "Maximum time allowed for quota-based delay.",
|
|
286
|
+
min: 1_000,
|
|
287
|
+
max: 24 * 60 * 60_000,
|
|
288
|
+
step: 60_000,
|
|
289
|
+
unit: "ms",
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
const BACKEND_DEFAULTS = getDefaultPluginConfig();
|
|
293
|
+
const BACKEND_TOGGLE_OPTION_BY_KEY = new Map(BACKEND_TOGGLE_OPTIONS.map((option) => [option.key, option]));
|
|
294
|
+
const BACKEND_NUMBER_OPTION_BY_KEY = new Map(BACKEND_NUMBER_OPTIONS.map((option) => [option.key, option]));
|
|
295
|
+
const BACKEND_CATEGORY_OPTIONS = [
|
|
296
|
+
{
|
|
297
|
+
key: "session-sync",
|
|
298
|
+
label: "Session & Sync",
|
|
299
|
+
description: "Sync and session behavior.",
|
|
300
|
+
toggleKeys: [
|
|
301
|
+
"liveAccountSync",
|
|
302
|
+
"sessionAffinity",
|
|
303
|
+
"perProjectAccounts",
|
|
304
|
+
"sessionRecovery",
|
|
305
|
+
"autoResume",
|
|
306
|
+
],
|
|
307
|
+
numberKeys: [
|
|
308
|
+
"liveAccountSyncDebounceMs",
|
|
309
|
+
"liveAccountSyncPollMs",
|
|
310
|
+
"sessionAffinityTtlMs",
|
|
311
|
+
"sessionAffinityMaxEntries",
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
key: "rotation-quota",
|
|
316
|
+
label: "Rotation & Quota",
|
|
317
|
+
description: "Quota and retry behavior.",
|
|
318
|
+
toggleKeys: ["preemptiveQuotaEnabled", "retryAllAccountsRateLimited"],
|
|
319
|
+
numberKeys: [
|
|
320
|
+
"preemptiveQuotaRemainingPercent5h",
|
|
321
|
+
"preemptiveQuotaRemainingPercent7d",
|
|
322
|
+
"preemptiveQuotaMaxDeferralMs",
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
key: "refresh-recovery",
|
|
327
|
+
label: "Refresh & Recovery",
|
|
328
|
+
description: "Token refresh and recovery safety.",
|
|
329
|
+
toggleKeys: ["proactiveRefreshGuardian", "storageBackupEnabled"],
|
|
330
|
+
numberKeys: [
|
|
331
|
+
"proactiveRefreshIntervalMs",
|
|
332
|
+
"proactiveRefreshBufferMs",
|
|
333
|
+
"tokenRefreshSkewMs",
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
key: "performance-timeouts",
|
|
338
|
+
label: "Performance & Timeouts",
|
|
339
|
+
description: "Speed, probing, and timeout controls.",
|
|
340
|
+
toggleKeys: ["fastSession", "parallelProbing"],
|
|
341
|
+
numberKeys: [
|
|
342
|
+
"fastSessionMaxInputItems",
|
|
343
|
+
"parallelProbingMaxConcurrency",
|
|
344
|
+
"fetchTimeoutMs",
|
|
345
|
+
"streamStallTimeoutMs",
|
|
346
|
+
"networkErrorCooldownMs",
|
|
347
|
+
"serverErrorCooldownMs",
|
|
348
|
+
],
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
const RETRYABLE_SETTINGS_WRITE_CODES = new Set(["EBUSY", "EPERM", "EAGAIN"]);
|
|
352
|
+
const SETTINGS_WRITE_MAX_ATTEMPTS = 4;
|
|
353
|
+
const SETTINGS_WRITE_BASE_DELAY_MS = 20;
|
|
354
|
+
const SETTINGS_WRITE_MAX_DELAY_MS = 30_000;
|
|
355
|
+
const settingsWriteQueues = new Map();
|
|
356
|
+
const ACCOUNT_LIST_PANEL_KEYS = [
|
|
357
|
+
"menuShowStatusBadge",
|
|
358
|
+
"menuShowCurrentBadge",
|
|
359
|
+
"menuShowLastUsed",
|
|
360
|
+
"menuShowQuotaSummary",
|
|
361
|
+
"menuShowQuotaCooldown",
|
|
362
|
+
"menuShowFetchStatus",
|
|
363
|
+
"menuShowDetailsForUnselectedRows",
|
|
364
|
+
"menuHighlightCurrentRow",
|
|
365
|
+
"menuSortEnabled",
|
|
366
|
+
"menuSortMode",
|
|
367
|
+
"menuSortPinCurrent",
|
|
368
|
+
"menuSortQuickSwitchVisibleRow",
|
|
369
|
+
"menuLayoutMode",
|
|
370
|
+
];
|
|
371
|
+
const STATUSLINE_PANEL_KEYS = ["menuStatuslineFields"];
|
|
372
|
+
const BEHAVIOR_PANEL_KEYS = [
|
|
373
|
+
"actionAutoReturnMs",
|
|
374
|
+
"actionPauseOnKey",
|
|
375
|
+
"menuAutoFetchLimits",
|
|
376
|
+
"menuShowFetchStatus",
|
|
377
|
+
"menuQuotaTtlMs",
|
|
378
|
+
];
|
|
379
|
+
const THEME_PANEL_KEYS = ["uiThemePreset", "uiAccentColor"];
|
|
380
|
+
function readErrorNumber(value) {
|
|
381
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
382
|
+
return value;
|
|
383
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
384
|
+
const parsed = Number.parseInt(value, 10);
|
|
385
|
+
if (Number.isFinite(parsed))
|
|
386
|
+
return parsed;
|
|
387
|
+
}
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
function getErrorStatusCode(error) {
|
|
391
|
+
if (!error || typeof error !== "object")
|
|
392
|
+
return undefined;
|
|
393
|
+
const record = error;
|
|
394
|
+
return readErrorNumber(record.status) ?? readErrorNumber(record.statusCode);
|
|
395
|
+
}
|
|
396
|
+
function getRetryAfterMs(error) {
|
|
397
|
+
if (!error || typeof error !== "object")
|
|
398
|
+
return undefined;
|
|
399
|
+
const record = error;
|
|
400
|
+
return (readErrorNumber(record.retryAfterMs) ??
|
|
401
|
+
readErrorNumber(record.retry_after_ms) ??
|
|
402
|
+
readErrorNumber(record.retryAfter) ??
|
|
403
|
+
readErrorNumber(record.retry_after));
|
|
404
|
+
}
|
|
405
|
+
function isRetryableSettingsWriteError(error) {
|
|
406
|
+
const statusCode = getErrorStatusCode(error);
|
|
407
|
+
if (statusCode === 429)
|
|
408
|
+
return true;
|
|
409
|
+
const code = error?.code;
|
|
410
|
+
return typeof code === "string" && RETRYABLE_SETTINGS_WRITE_CODES.has(code);
|
|
411
|
+
}
|
|
412
|
+
function resolveRetryDelayMs(error, attempt) {
|
|
413
|
+
const retryAfterMs = getRetryAfterMs(error);
|
|
414
|
+
if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
|
|
415
|
+
return Math.max(10, Math.min(SETTINGS_WRITE_MAX_DELAY_MS, Math.round(retryAfterMs)));
|
|
416
|
+
}
|
|
417
|
+
return Math.min(SETTINGS_WRITE_MAX_DELAY_MS, SETTINGS_WRITE_BASE_DELAY_MS * 2 ** attempt);
|
|
418
|
+
}
|
|
419
|
+
async function enqueueSettingsWrite(pathKey, task) {
|
|
420
|
+
const previous = settingsWriteQueues.get(pathKey) ?? Promise.resolve();
|
|
421
|
+
const queued = previous.catch(() => { }).then(task);
|
|
422
|
+
const queueTail = queued.then(() => undefined, () => undefined);
|
|
423
|
+
settingsWriteQueues.set(pathKey, queueTail);
|
|
424
|
+
try {
|
|
425
|
+
return await queued;
|
|
426
|
+
}
|
|
427
|
+
finally {
|
|
428
|
+
if (settingsWriteQueues.get(pathKey) === queueTail) {
|
|
429
|
+
settingsWriteQueues.delete(pathKey);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function withQueuedRetry(pathKey, task) {
|
|
434
|
+
return enqueueSettingsWrite(pathKey, async () => {
|
|
435
|
+
let lastError;
|
|
436
|
+
for (let attempt = 0; attempt < SETTINGS_WRITE_MAX_ATTEMPTS; attempt += 1) {
|
|
437
|
+
try {
|
|
438
|
+
return await task();
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
lastError = error;
|
|
442
|
+
if (!isRetryableSettingsWriteError(error) || attempt + 1 >= SETTINGS_WRITE_MAX_ATTEMPTS) {
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
await sleep(resolveRetryDelayMs(error, attempt));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
throw lastError instanceof Error ? lastError : new Error("settings save retry exhausted");
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function copyDashboardSettingValue(target, source, key) {
|
|
452
|
+
const value = source[key];
|
|
453
|
+
target[key] = Array.isArray(value) ? [...value] : value;
|
|
454
|
+
}
|
|
455
|
+
function applyDashboardDefaultsForKeys(draft, keys) {
|
|
456
|
+
const next = cloneDashboardSettings(draft);
|
|
457
|
+
const defaults = cloneDashboardSettings(DEFAULT_DASHBOARD_DISPLAY_SETTINGS);
|
|
458
|
+
for (const key of keys) {
|
|
459
|
+
copyDashboardSettingValue(next, defaults, key);
|
|
460
|
+
}
|
|
461
|
+
return next;
|
|
462
|
+
}
|
|
463
|
+
function mergeDashboardSettingsForKeys(base, selected, keys) {
|
|
464
|
+
const next = cloneDashboardSettings(base);
|
|
465
|
+
for (const key of keys) {
|
|
466
|
+
copyDashboardSettingValue(next, selected, key);
|
|
467
|
+
}
|
|
468
|
+
return cloneDashboardSettings(next);
|
|
469
|
+
}
|
|
470
|
+
function resolvePluginConfigSavePathKey() {
|
|
471
|
+
const envPath = (process.env.CODEX_MULTI_AUTH_CONFIG_PATH ?? "").trim();
|
|
472
|
+
return envPath.length > 0 ? envPath : getUnifiedSettingsPath();
|
|
473
|
+
}
|
|
474
|
+
function formatPersistError(error) {
|
|
475
|
+
if (error instanceof Error)
|
|
476
|
+
return error.message;
|
|
477
|
+
return String(error);
|
|
478
|
+
}
|
|
479
|
+
function warnPersistFailure(scope, error) {
|
|
480
|
+
console.warn(`Settings save failed (${scope}) after retries: ${formatPersistError(error)}`);
|
|
481
|
+
}
|
|
482
|
+
async function persistDashboardSettingsSelection(selected, keys, scope) {
|
|
483
|
+
const fallback = cloneDashboardSettings(selected);
|
|
484
|
+
try {
|
|
485
|
+
return await withQueuedRetry(getDashboardSettingsPath(), async () => {
|
|
486
|
+
const latest = cloneDashboardSettings(await loadDashboardDisplaySettings());
|
|
487
|
+
const merged = mergeDashboardSettingsForKeys(latest, selected, keys);
|
|
488
|
+
await saveDashboardDisplaySettings(merged);
|
|
489
|
+
return merged;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
warnPersistFailure(scope, error);
|
|
494
|
+
return fallback;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async function persistBackendConfigSelection(selected, scope) {
|
|
498
|
+
const fallback = cloneBackendPluginConfig(selected);
|
|
499
|
+
try {
|
|
500
|
+
await withQueuedRetry(resolvePluginConfigSavePathKey(), async () => {
|
|
501
|
+
await savePluginConfig(buildBackendConfigPatch(selected));
|
|
502
|
+
});
|
|
503
|
+
return fallback;
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
warnPersistFailure(scope, error);
|
|
507
|
+
return fallback;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function normalizeStatuslineFields(fields) {
|
|
511
|
+
const source = fields ?? DEFAULT_STATUSLINE_FIELDS;
|
|
512
|
+
const seen = new Set();
|
|
513
|
+
const normalized = [];
|
|
514
|
+
for (const field of source) {
|
|
515
|
+
if (seen.has(field))
|
|
516
|
+
continue;
|
|
517
|
+
seen.add(field);
|
|
518
|
+
normalized.push(field);
|
|
519
|
+
}
|
|
520
|
+
if (normalized.length === 0) {
|
|
521
|
+
return [...DEFAULT_STATUSLINE_FIELDS];
|
|
522
|
+
}
|
|
523
|
+
return normalized;
|
|
524
|
+
}
|
|
525
|
+
function highlightPreviewToken(text, ui) {
|
|
526
|
+
if (!output.isTTY)
|
|
527
|
+
return text;
|
|
528
|
+
if (ui.v2Enabled) {
|
|
529
|
+
return `${ui.theme.colors.accent}${ANSI.bold}${text}${ui.theme.colors.reset}`;
|
|
530
|
+
}
|
|
531
|
+
return `${ANSI.cyan}${ANSI.bold}${text}${ANSI.reset}`;
|
|
532
|
+
}
|
|
533
|
+
function isLastUsedPreviewFocus(focus) {
|
|
534
|
+
return focus === "menuShowLastUsed" || focus === "last-used";
|
|
535
|
+
}
|
|
536
|
+
function isLimitsPreviewFocus(focus) {
|
|
537
|
+
return focus === "menuShowQuotaSummary" || focus === "limits";
|
|
538
|
+
}
|
|
539
|
+
function isLimitsCooldownPreviewFocus(focus) {
|
|
540
|
+
return focus === "menuShowQuotaCooldown";
|
|
541
|
+
}
|
|
542
|
+
function isStatusPreviewFocus(focus) {
|
|
543
|
+
return focus === "menuShowStatusBadge" || focus === "status";
|
|
544
|
+
}
|
|
545
|
+
function isCurrentBadgePreviewFocus(focus) {
|
|
546
|
+
return focus === "menuShowCurrentBadge";
|
|
547
|
+
}
|
|
548
|
+
function isCurrentRowPreviewFocus(focus) {
|
|
549
|
+
return focus === "menuHighlightCurrentRow";
|
|
550
|
+
}
|
|
551
|
+
function isExpandedRowsPreviewFocus(focus) {
|
|
552
|
+
return focus === "menuShowDetailsForUnselectedRows" || focus === "menuLayoutMode";
|
|
553
|
+
}
|
|
554
|
+
function buildSummaryPreviewText(settings, ui, focus = null) {
|
|
555
|
+
const partsByField = new Map();
|
|
556
|
+
if (settings.menuShowLastUsed !== false) {
|
|
557
|
+
const part = `last used: ${PREVIEW_LAST_USED}`;
|
|
558
|
+
partsByField.set("last-used", isLastUsedPreviewFocus(focus) ? highlightPreviewToken(part, ui) : part);
|
|
559
|
+
}
|
|
560
|
+
if (settings.menuShowQuotaSummary !== false) {
|
|
561
|
+
const limitsText = settings.menuShowQuotaCooldown === false
|
|
562
|
+
? PREVIEW_LIMITS
|
|
563
|
+
: `${PREVIEW_LIMITS} | ${PREVIEW_LIMIT_COOLDOWNS}`;
|
|
564
|
+
const part = `limits: ${limitsText}`;
|
|
565
|
+
partsByField.set("limits", isLimitsPreviewFocus(focus) || isLimitsCooldownPreviewFocus(focus)
|
|
566
|
+
? highlightPreviewToken(part, ui)
|
|
567
|
+
: part);
|
|
568
|
+
}
|
|
569
|
+
if (settings.menuShowStatusBadge === false) {
|
|
570
|
+
const part = `status: ${PREVIEW_STATUS}`;
|
|
571
|
+
partsByField.set("status", isStatusPreviewFocus(focus) ? highlightPreviewToken(part, ui) : part);
|
|
572
|
+
}
|
|
573
|
+
const orderedParts = normalizeStatuslineFields(settings.menuStatuslineFields)
|
|
574
|
+
.map((field) => partsByField.get(field))
|
|
575
|
+
.filter((part) => typeof part === "string" && part.length > 0);
|
|
576
|
+
if (orderedParts.length > 0) {
|
|
577
|
+
return orderedParts.join(" | ");
|
|
578
|
+
}
|
|
579
|
+
const showsStatusField = normalizeStatuslineFields(settings.menuStatuslineFields).includes("status");
|
|
580
|
+
if (showsStatusField && settings.menuShowStatusBadge !== false) {
|
|
581
|
+
const note = "status text appears only when status badges are hidden";
|
|
582
|
+
return isStatusPreviewFocus(focus) ? highlightPreviewToken(note, ui) : note;
|
|
583
|
+
}
|
|
584
|
+
return "no summary text is visible with current account-list settings";
|
|
585
|
+
}
|
|
586
|
+
function buildAccountListPreview(settings, ui, focus = null) {
|
|
587
|
+
const badges = [];
|
|
588
|
+
if (settings.menuShowCurrentBadge !== false) {
|
|
589
|
+
const currentBadge = "[current]";
|
|
590
|
+
badges.push(isCurrentBadgePreviewFocus(focus) ? highlightPreviewToken(currentBadge, ui) : currentBadge);
|
|
591
|
+
}
|
|
592
|
+
if (settings.menuShowStatusBadge !== false) {
|
|
593
|
+
const statusBadge = "[active]";
|
|
594
|
+
badges.push(isStatusPreviewFocus(focus) ? highlightPreviewToken(statusBadge, ui) : statusBadge);
|
|
595
|
+
}
|
|
596
|
+
const badgeSuffix = badges.length > 0 ? ` ${badges.join(" ")}` : "";
|
|
597
|
+
const accountEmail = isCurrentRowPreviewFocus(focus)
|
|
598
|
+
? highlightPreviewToken(PREVIEW_ACCOUNT_EMAIL, ui)
|
|
599
|
+
: PREVIEW_ACCOUNT_EMAIL;
|
|
600
|
+
const rowDetailMode = resolveMenuLayoutMode(settings) === "expanded-rows"
|
|
601
|
+
? "details shown on all rows"
|
|
602
|
+
: "details shown on selected row only";
|
|
603
|
+
const detailModeText = isExpandedRowsPreviewFocus(focus)
|
|
604
|
+
? highlightPreviewToken(rowDetailMode, ui)
|
|
605
|
+
: rowDetailMode;
|
|
606
|
+
return {
|
|
607
|
+
label: `1. ${accountEmail}${badgeSuffix}`,
|
|
608
|
+
hint: `${buildSummaryPreviewText(settings, ui, focus)}\n${detailModeText}`,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function cloneDashboardSettings(settings) {
|
|
612
|
+
const layoutMode = resolveMenuLayoutMode(settings);
|
|
613
|
+
return {
|
|
614
|
+
showPerAccountRows: settings.showPerAccountRows,
|
|
615
|
+
showQuotaDetails: settings.showQuotaDetails,
|
|
616
|
+
showForecastReasons: settings.showForecastReasons,
|
|
617
|
+
showRecommendations: settings.showRecommendations,
|
|
618
|
+
showLiveProbeNotes: settings.showLiveProbeNotes,
|
|
619
|
+
actionAutoReturnMs: settings.actionAutoReturnMs ?? 2_000,
|
|
620
|
+
actionPauseOnKey: settings.actionPauseOnKey ?? true,
|
|
621
|
+
menuAutoFetchLimits: settings.menuAutoFetchLimits ?? true,
|
|
622
|
+
menuSortEnabled: settings.menuSortEnabled ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortEnabled ?? true),
|
|
623
|
+
menuSortMode: settings.menuSortMode ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? "ready-first"),
|
|
624
|
+
menuSortPinCurrent: settings.menuSortPinCurrent ??
|
|
625
|
+
(DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortPinCurrent ?? false),
|
|
626
|
+
menuSortQuickSwitchVisibleRow: settings.menuSortQuickSwitchVisibleRow ?? true,
|
|
627
|
+
uiThemePreset: settings.uiThemePreset ?? "green",
|
|
628
|
+
uiAccentColor: settings.uiAccentColor ?? "green",
|
|
629
|
+
menuShowStatusBadge: settings.menuShowStatusBadge ?? true,
|
|
630
|
+
menuShowCurrentBadge: settings.menuShowCurrentBadge ?? true,
|
|
631
|
+
menuShowLastUsed: settings.menuShowLastUsed ?? true,
|
|
632
|
+
menuShowQuotaSummary: settings.menuShowQuotaSummary ?? true,
|
|
633
|
+
menuShowQuotaCooldown: settings.menuShowQuotaCooldown ?? true,
|
|
634
|
+
menuShowFetchStatus: settings.menuShowFetchStatus ?? true,
|
|
635
|
+
menuShowDetailsForUnselectedRows: layoutMode === "expanded-rows",
|
|
636
|
+
menuLayoutMode: layoutMode,
|
|
637
|
+
menuQuotaTtlMs: settings.menuQuotaTtlMs ?? 5 * 60_000,
|
|
638
|
+
menuFocusStyle: settings.menuFocusStyle ?? "row-invert",
|
|
639
|
+
menuHighlightCurrentRow: settings.menuHighlightCurrentRow ?? true,
|
|
640
|
+
menuStatuslineFields: [...normalizeStatuslineFields(settings.menuStatuslineFields)],
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function dashboardSettingsEqual(left, right) {
|
|
644
|
+
return (left.showPerAccountRows === right.showPerAccountRows &&
|
|
645
|
+
left.showQuotaDetails === right.showQuotaDetails &&
|
|
646
|
+
left.showForecastReasons === right.showForecastReasons &&
|
|
647
|
+
left.showRecommendations === right.showRecommendations &&
|
|
648
|
+
left.showLiveProbeNotes === right.showLiveProbeNotes &&
|
|
649
|
+
(left.actionAutoReturnMs ?? 2_000) === (right.actionAutoReturnMs ?? 2_000) &&
|
|
650
|
+
(left.actionPauseOnKey ?? true) === (right.actionPauseOnKey ?? true) &&
|
|
651
|
+
(left.menuAutoFetchLimits ?? true) === (right.menuAutoFetchLimits ?? true) &&
|
|
652
|
+
(left.menuSortEnabled ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortEnabled ?? true)) ===
|
|
653
|
+
(right.menuSortEnabled ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortEnabled ?? true)) &&
|
|
654
|
+
(left.menuSortMode ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? "ready-first")) ===
|
|
655
|
+
(right.menuSortMode ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? "ready-first")) &&
|
|
656
|
+
(left.menuSortPinCurrent ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortPinCurrent ?? false)) ===
|
|
657
|
+
(right.menuSortPinCurrent ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortPinCurrent ?? false)) &&
|
|
658
|
+
(left.menuSortQuickSwitchVisibleRow ?? true) ===
|
|
659
|
+
(right.menuSortQuickSwitchVisibleRow ?? true) &&
|
|
660
|
+
(left.uiThemePreset ?? "green") === (right.uiThemePreset ?? "green") &&
|
|
661
|
+
(left.uiAccentColor ?? "green") === (right.uiAccentColor ?? "green") &&
|
|
662
|
+
(left.menuShowStatusBadge ?? true) === (right.menuShowStatusBadge ?? true) &&
|
|
663
|
+
(left.menuShowCurrentBadge ?? true) === (right.menuShowCurrentBadge ?? true) &&
|
|
664
|
+
(left.menuShowLastUsed ?? true) === (right.menuShowLastUsed ?? true) &&
|
|
665
|
+
(left.menuShowQuotaSummary ?? true) === (right.menuShowQuotaSummary ?? true) &&
|
|
666
|
+
(left.menuShowQuotaCooldown ?? true) === (right.menuShowQuotaCooldown ?? true) &&
|
|
667
|
+
(left.menuShowFetchStatus ?? true) === (right.menuShowFetchStatus ?? true) &&
|
|
668
|
+
resolveMenuLayoutMode(left) === resolveMenuLayoutMode(right) &&
|
|
669
|
+
(left.menuQuotaTtlMs ?? 5 * 60_000) === (right.menuQuotaTtlMs ?? 5 * 60_000) &&
|
|
670
|
+
(left.menuFocusStyle ?? "row-invert") === (right.menuFocusStyle ?? "row-invert") &&
|
|
671
|
+
(left.menuHighlightCurrentRow ?? true) === (right.menuHighlightCurrentRow ?? true) &&
|
|
672
|
+
JSON.stringify(normalizeStatuslineFields(left.menuStatuslineFields)) ===
|
|
673
|
+
JSON.stringify(normalizeStatuslineFields(right.menuStatuslineFields)));
|
|
674
|
+
}
|
|
675
|
+
function cloneBackendPluginConfig(config) {
|
|
676
|
+
const fallbackChain = config.unsupportedCodexFallbackChain;
|
|
677
|
+
return {
|
|
678
|
+
...BACKEND_DEFAULTS,
|
|
679
|
+
...config,
|
|
680
|
+
unsupportedCodexFallbackChain: fallbackChain && typeof fallbackChain === "object"
|
|
681
|
+
? { ...fallbackChain }
|
|
682
|
+
: {},
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function backendSettingsSnapshot(config) {
|
|
686
|
+
const snapshot = {};
|
|
687
|
+
for (const option of BACKEND_TOGGLE_OPTIONS) {
|
|
688
|
+
snapshot[option.key] = config[option.key] ?? BACKEND_DEFAULTS[option.key] ?? false;
|
|
689
|
+
}
|
|
690
|
+
for (const option of BACKEND_NUMBER_OPTIONS) {
|
|
691
|
+
snapshot[option.key] = config[option.key] ?? BACKEND_DEFAULTS[option.key] ?? option.min;
|
|
692
|
+
}
|
|
693
|
+
return snapshot;
|
|
694
|
+
}
|
|
695
|
+
function backendSettingsEqual(left, right) {
|
|
696
|
+
return JSON.stringify(backendSettingsSnapshot(left)) === JSON.stringify(backendSettingsSnapshot(right));
|
|
697
|
+
}
|
|
698
|
+
function formatBackendNumberValue(option, value) {
|
|
699
|
+
if (option.unit === "percent")
|
|
700
|
+
return `${Math.round(value)}%`;
|
|
701
|
+
if (option.unit === "count")
|
|
702
|
+
return `${Math.round(value)}`;
|
|
703
|
+
if (value >= 60_000 && value % 60_000 === 0) {
|
|
704
|
+
return `${Math.round(value / 60_000)}m`;
|
|
705
|
+
}
|
|
706
|
+
if (value >= 1_000 && value % 1_000 === 0) {
|
|
707
|
+
return `${Math.round(value / 1_000)}s`;
|
|
708
|
+
}
|
|
709
|
+
return `${Math.round(value)}ms`;
|
|
710
|
+
}
|
|
711
|
+
function clampBackendNumber(option, value) {
|
|
712
|
+
return Math.max(option.min, Math.min(option.max, Math.round(value)));
|
|
713
|
+
}
|
|
714
|
+
function buildBackendSettingsPreview(config, ui, focus = null) {
|
|
715
|
+
const liveSync = config.liveAccountSync ?? BACKEND_DEFAULTS.liveAccountSync ?? true;
|
|
716
|
+
const affinity = config.sessionAffinity ?? BACKEND_DEFAULTS.sessionAffinity ?? true;
|
|
717
|
+
const preemptive = config.preemptiveQuotaEnabled ?? BACKEND_DEFAULTS.preemptiveQuotaEnabled ?? true;
|
|
718
|
+
const threshold5h = config.preemptiveQuotaRemainingPercent5h ??
|
|
719
|
+
BACKEND_DEFAULTS.preemptiveQuotaRemainingPercent5h ??
|
|
720
|
+
5;
|
|
721
|
+
const threshold7d = config.preemptiveQuotaRemainingPercent7d ??
|
|
722
|
+
BACKEND_DEFAULTS.preemptiveQuotaRemainingPercent7d ??
|
|
723
|
+
5;
|
|
724
|
+
const fetchTimeout = config.fetchTimeoutMs ?? BACKEND_DEFAULTS.fetchTimeoutMs ?? 60_000;
|
|
725
|
+
const stallTimeout = config.streamStallTimeoutMs ?? BACKEND_DEFAULTS.streamStallTimeoutMs ?? 45_000;
|
|
726
|
+
const fetchTimeoutOption = BACKEND_NUMBER_OPTION_BY_KEY.get("fetchTimeoutMs");
|
|
727
|
+
const stallTimeoutOption = BACKEND_NUMBER_OPTION_BY_KEY.get("streamStallTimeoutMs");
|
|
728
|
+
const highlightIfFocused = (key, text) => {
|
|
729
|
+
if (focus !== key)
|
|
730
|
+
return text;
|
|
731
|
+
return highlightPreviewToken(text, ui);
|
|
732
|
+
};
|
|
733
|
+
const label = [
|
|
734
|
+
`live sync ${highlightIfFocused("liveAccountSync", liveSync ? "on" : "off")}`,
|
|
735
|
+
`affinity ${highlightIfFocused("sessionAffinity", affinity ? "on" : "off")}`,
|
|
736
|
+
`preemptive ${highlightIfFocused("preemptiveQuotaEnabled", preemptive ? "on" : "off")}`,
|
|
737
|
+
].join(" | ");
|
|
738
|
+
const hint = [
|
|
739
|
+
`thresholds 5h<=${highlightIfFocused("preemptiveQuotaRemainingPercent5h", `${threshold5h}%`)}`,
|
|
740
|
+
`7d<=${highlightIfFocused("preemptiveQuotaRemainingPercent7d", `${threshold7d}%`)}`,
|
|
741
|
+
`timeouts ${highlightIfFocused("fetchTimeoutMs", fetchTimeoutOption ? formatBackendNumberValue(fetchTimeoutOption, fetchTimeout) : `${fetchTimeout}ms`)}/${highlightIfFocused("streamStallTimeoutMs", stallTimeoutOption ? formatBackendNumberValue(stallTimeoutOption, stallTimeout) : `${stallTimeout}ms`)}`,
|
|
742
|
+
].join(" | ");
|
|
743
|
+
return { label, hint };
|
|
744
|
+
}
|
|
745
|
+
function buildBackendConfigPatch(config) {
|
|
746
|
+
const patch = {};
|
|
747
|
+
for (const option of BACKEND_TOGGLE_OPTIONS) {
|
|
748
|
+
const value = config[option.key];
|
|
749
|
+
if (typeof value === "boolean") {
|
|
750
|
+
patch[option.key] = value;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
for (const option of BACKEND_NUMBER_OPTIONS) {
|
|
754
|
+
const value = config[option.key];
|
|
755
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
756
|
+
patch[option.key] = clampBackendNumber(option, value);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return patch;
|
|
760
|
+
}
|
|
761
|
+
function applyUiThemeFromDashboardSettings(settings) {
|
|
762
|
+
const current = getUiRuntimeOptions();
|
|
763
|
+
setUiRuntimeOptions({
|
|
764
|
+
v2Enabled: current.v2Enabled,
|
|
765
|
+
colorProfile: current.colorProfile,
|
|
766
|
+
glyphMode: current.glyphMode,
|
|
767
|
+
palette: settings.uiThemePreset ?? "green",
|
|
768
|
+
accent: settings.uiAccentColor ?? "green",
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
function formatDashboardSettingState(value) {
|
|
772
|
+
return value ? "[x]" : "[ ]";
|
|
773
|
+
}
|
|
774
|
+
function formatMenuSortMode(mode) {
|
|
775
|
+
return mode === "ready-first" ? "Ready-First" : "Manual";
|
|
776
|
+
}
|
|
777
|
+
function resolveMenuLayoutMode(settings) {
|
|
778
|
+
if (settings.menuLayoutMode === "expanded-rows") {
|
|
779
|
+
return "expanded-rows";
|
|
780
|
+
}
|
|
781
|
+
if (settings.menuLayoutMode === "compact-details") {
|
|
782
|
+
return "compact-details";
|
|
783
|
+
}
|
|
784
|
+
return settings.menuShowDetailsForUnselectedRows === true ? "expanded-rows" : "compact-details";
|
|
785
|
+
}
|
|
786
|
+
function formatMenuLayoutMode(mode) {
|
|
787
|
+
return mode === "expanded-rows" ? "Expanded Rows" : "Compact + Details Pane";
|
|
788
|
+
}
|
|
789
|
+
function formatMenuQuotaTtl(ttlMs) {
|
|
790
|
+
if (ttlMs >= 60_000 && ttlMs % 60_000 === 0) {
|
|
791
|
+
return `${Math.round(ttlMs / 60_000)}m`;
|
|
792
|
+
}
|
|
793
|
+
if (ttlMs >= 1_000 && ttlMs % 1_000 === 0) {
|
|
794
|
+
return `${Math.round(ttlMs / 1_000)}s`;
|
|
795
|
+
}
|
|
796
|
+
return `${ttlMs}ms`;
|
|
797
|
+
}
|
|
798
|
+
async function promptDashboardDisplaySettings(initial) {
|
|
799
|
+
if (!input.isTTY || !output.isTTY) {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
const ui = getUiRuntimeOptions();
|
|
803
|
+
let draft = cloneDashboardSettings(initial);
|
|
804
|
+
let focusKey = DASHBOARD_DISPLAY_OPTIONS[0]?.key ?? "menuShowStatusBadge";
|
|
805
|
+
while (true) {
|
|
806
|
+
const preview = buildAccountListPreview(draft, ui, focusKey);
|
|
807
|
+
const optionItems = DASHBOARD_DISPLAY_OPTIONS.map((option, index) => {
|
|
808
|
+
const enabled = draft[option.key] ?? true;
|
|
809
|
+
const label = `${formatDashboardSettingState(enabled)} ${index + 1}. ${option.label}`;
|
|
810
|
+
const color = enabled ? "green" : "yellow";
|
|
811
|
+
return {
|
|
812
|
+
label,
|
|
813
|
+
hint: option.description,
|
|
814
|
+
value: { type: "toggle", key: option.key },
|
|
815
|
+
color,
|
|
816
|
+
};
|
|
817
|
+
});
|
|
818
|
+
const sortMode = draft.menuSortMode ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? "ready-first");
|
|
819
|
+
const sortModeItem = {
|
|
820
|
+
label: `Sort mode: ${formatMenuSortMode(sortMode)}`,
|
|
821
|
+
hint: "Applies when smart sort is enabled.",
|
|
822
|
+
value: { type: "cycle-sort-mode" },
|
|
823
|
+
color: sortMode === "ready-first" ? "green" : "yellow",
|
|
824
|
+
};
|
|
825
|
+
const layoutMode = resolveMenuLayoutMode(draft);
|
|
826
|
+
const layoutModeItem = {
|
|
827
|
+
label: `Layout: ${formatMenuLayoutMode(layoutMode)}`,
|
|
828
|
+
hint: "Compact shows one-line rows with a selected details pane.",
|
|
829
|
+
value: { type: "cycle-layout-mode" },
|
|
830
|
+
color: layoutMode === "compact-details" ? "green" : "yellow",
|
|
831
|
+
};
|
|
832
|
+
const items = [
|
|
833
|
+
{ label: UI_COPY.settings.previewHeading, value: { type: "cancel" }, kind: "heading" },
|
|
834
|
+
{
|
|
835
|
+
label: preview.label,
|
|
836
|
+
hint: preview.hint,
|
|
837
|
+
value: { type: "cancel" },
|
|
838
|
+
color: "green",
|
|
839
|
+
disabled: true,
|
|
840
|
+
hideUnavailableSuffix: true,
|
|
841
|
+
},
|
|
842
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
843
|
+
{ label: UI_COPY.settings.displayHeading, value: { type: "cancel" }, kind: "heading" },
|
|
844
|
+
...optionItems,
|
|
845
|
+
sortModeItem,
|
|
846
|
+
layoutModeItem,
|
|
847
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
848
|
+
{ label: UI_COPY.settings.resetDefault, value: { type: "reset" }, color: "yellow" },
|
|
849
|
+
{ label: UI_COPY.settings.saveAndBack, value: { type: "save" }, color: "green" },
|
|
850
|
+
{ label: UI_COPY.settings.backNoSave, value: { type: "cancel" }, color: "red" },
|
|
851
|
+
];
|
|
852
|
+
const initialCursor = items.findIndex((item) => (item.value.type === "toggle" && item.value.key === focusKey) ||
|
|
853
|
+
(item.value.type === "cycle-sort-mode" && focusKey === "menuSortMode") ||
|
|
854
|
+
(item.value.type === "cycle-layout-mode" && focusKey === "menuLayoutMode"));
|
|
855
|
+
const updateFocusedPreview = (cursor) => {
|
|
856
|
+
const focusedItem = items[cursor];
|
|
857
|
+
const focusedKey = focusedItem?.value.type === "toggle"
|
|
858
|
+
? focusedItem.value.key
|
|
859
|
+
: focusedItem?.value.type === "cycle-sort-mode"
|
|
860
|
+
? "menuSortMode"
|
|
861
|
+
: focusedItem?.value.type === "cycle-layout-mode"
|
|
862
|
+
? "menuLayoutMode"
|
|
863
|
+
: focusKey;
|
|
864
|
+
const nextPreview = buildAccountListPreview(draft, ui, focusedKey);
|
|
865
|
+
const previewItem = items[1];
|
|
866
|
+
if (!previewItem)
|
|
867
|
+
return;
|
|
868
|
+
previewItem.label = nextPreview.label;
|
|
869
|
+
previewItem.hint = nextPreview.hint;
|
|
870
|
+
};
|
|
871
|
+
const result = await select(items, {
|
|
872
|
+
message: UI_COPY.settings.accountListTitle,
|
|
873
|
+
subtitle: UI_COPY.settings.accountListSubtitle,
|
|
874
|
+
help: UI_COPY.settings.accountListHelp,
|
|
875
|
+
clearScreen: true,
|
|
876
|
+
theme: ui.theme,
|
|
877
|
+
selectedEmphasis: "minimal",
|
|
878
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
879
|
+
onCursorChange: ({ cursor }) => {
|
|
880
|
+
const focusedItem = items[cursor];
|
|
881
|
+
if (focusedItem?.value.type === "toggle") {
|
|
882
|
+
focusKey = focusedItem.value.key;
|
|
883
|
+
}
|
|
884
|
+
else if (focusedItem?.value.type === "cycle-sort-mode") {
|
|
885
|
+
focusKey = "menuSortMode";
|
|
886
|
+
}
|
|
887
|
+
else if (focusedItem?.value.type === "cycle-layout-mode") {
|
|
888
|
+
focusKey = "menuLayoutMode";
|
|
889
|
+
}
|
|
890
|
+
updateFocusedPreview(cursor);
|
|
891
|
+
},
|
|
892
|
+
onInput: (raw) => {
|
|
893
|
+
const lower = raw.toLowerCase();
|
|
894
|
+
if (lower === "q")
|
|
895
|
+
return { type: "cancel" };
|
|
896
|
+
if (lower === "s")
|
|
897
|
+
return { type: "save" };
|
|
898
|
+
if (lower === "r")
|
|
899
|
+
return { type: "reset" };
|
|
900
|
+
if (lower === "m")
|
|
901
|
+
return { type: "cycle-sort-mode" };
|
|
902
|
+
if (lower === "l")
|
|
903
|
+
return { type: "cycle-layout-mode" };
|
|
904
|
+
const parsed = Number.parseInt(raw, 10);
|
|
905
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= DASHBOARD_DISPLAY_OPTIONS.length) {
|
|
906
|
+
const target = DASHBOARD_DISPLAY_OPTIONS[parsed - 1];
|
|
907
|
+
if (target) {
|
|
908
|
+
return { type: "toggle", key: target.key };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (parsed === DASHBOARD_DISPLAY_OPTIONS.length + 1) {
|
|
912
|
+
return { type: "cycle-sort-mode" };
|
|
913
|
+
}
|
|
914
|
+
if (parsed === DASHBOARD_DISPLAY_OPTIONS.length + 2) {
|
|
915
|
+
return { type: "cycle-layout-mode" };
|
|
916
|
+
}
|
|
917
|
+
return undefined;
|
|
918
|
+
},
|
|
919
|
+
});
|
|
920
|
+
if (!result || result.type === "cancel") {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
if (result.type === "save") {
|
|
924
|
+
return draft;
|
|
925
|
+
}
|
|
926
|
+
if (result.type === "reset") {
|
|
927
|
+
draft = applyDashboardDefaultsForKeys(draft, ACCOUNT_LIST_PANEL_KEYS);
|
|
928
|
+
focusKey = DASHBOARD_DISPLAY_OPTIONS[0]?.key ?? focusKey;
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (result.type === "cycle-sort-mode") {
|
|
932
|
+
const currentMode = draft.menuSortMode ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortMode ?? "ready-first");
|
|
933
|
+
const nextMode = currentMode === "ready-first"
|
|
934
|
+
? "manual"
|
|
935
|
+
: "ready-first";
|
|
936
|
+
draft = {
|
|
937
|
+
...draft,
|
|
938
|
+
menuSortMode: nextMode,
|
|
939
|
+
menuSortEnabled: nextMode === "ready-first"
|
|
940
|
+
? true
|
|
941
|
+
: (draft.menuSortEnabled ?? (DEFAULT_DASHBOARD_DISPLAY_SETTINGS.menuSortEnabled ?? true)),
|
|
942
|
+
};
|
|
943
|
+
focusKey = "menuSortMode";
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (result.type === "cycle-layout-mode") {
|
|
947
|
+
const currentLayout = resolveMenuLayoutMode(draft);
|
|
948
|
+
const nextLayout = currentLayout === "compact-details" ? "expanded-rows" : "compact-details";
|
|
949
|
+
draft = {
|
|
950
|
+
...draft,
|
|
951
|
+
menuLayoutMode: nextLayout,
|
|
952
|
+
menuShowDetailsForUnselectedRows: nextLayout === "expanded-rows",
|
|
953
|
+
};
|
|
954
|
+
focusKey = "menuLayoutMode";
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
focusKey = result.key;
|
|
958
|
+
draft = {
|
|
959
|
+
...draft,
|
|
960
|
+
[result.key]: !draft[result.key],
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async function configureDashboardDisplaySettings(currentSettings) {
|
|
965
|
+
const current = currentSettings ?? await loadDashboardDisplaySettings();
|
|
966
|
+
if (!input.isTTY || !output.isTTY) {
|
|
967
|
+
console.log("Settings require interactive mode.");
|
|
968
|
+
console.log(`Settings file: ${getDashboardSettingsPath()}`);
|
|
969
|
+
return current;
|
|
970
|
+
}
|
|
971
|
+
const selected = await promptDashboardDisplaySettings(current);
|
|
972
|
+
if (!selected)
|
|
973
|
+
return current;
|
|
974
|
+
if (dashboardSettingsEqual(current, selected))
|
|
975
|
+
return current;
|
|
976
|
+
const merged = await persistDashboardSettingsSelection(selected, ACCOUNT_LIST_PANEL_KEYS, "account-list");
|
|
977
|
+
applyUiThemeFromDashboardSettings(merged);
|
|
978
|
+
return merged;
|
|
979
|
+
}
|
|
980
|
+
function reorderField(fields, key, direction) {
|
|
981
|
+
const index = fields.indexOf(key);
|
|
982
|
+
if (index < 0)
|
|
983
|
+
return fields;
|
|
984
|
+
const target = index + direction;
|
|
985
|
+
if (target < 0 || target >= fields.length)
|
|
986
|
+
return fields;
|
|
987
|
+
const next = [...fields];
|
|
988
|
+
const current = next[index];
|
|
989
|
+
const swap = next[target];
|
|
990
|
+
if (!current || !swap)
|
|
991
|
+
return fields;
|
|
992
|
+
next[index] = swap;
|
|
993
|
+
next[target] = current;
|
|
994
|
+
return next;
|
|
995
|
+
}
|
|
996
|
+
async function promptStatuslineSettings(initial) {
|
|
997
|
+
if (!input.isTTY || !output.isTTY) {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
const ui = getUiRuntimeOptions();
|
|
1001
|
+
let draft = cloneDashboardSettings(initial);
|
|
1002
|
+
let focusKey = draft.menuStatuslineFields?.[0] ?? "last-used";
|
|
1003
|
+
while (true) {
|
|
1004
|
+
const preview = buildAccountListPreview(draft, ui, focusKey);
|
|
1005
|
+
const selectedSet = new Set(normalizeStatuslineFields(draft.menuStatuslineFields));
|
|
1006
|
+
const ordered = normalizeStatuslineFields(draft.menuStatuslineFields);
|
|
1007
|
+
const orderMap = new Map();
|
|
1008
|
+
for (let index = 0; index < ordered.length; index += 1) {
|
|
1009
|
+
const key = ordered[index];
|
|
1010
|
+
if (key)
|
|
1011
|
+
orderMap.set(key, index + 1);
|
|
1012
|
+
}
|
|
1013
|
+
const optionItems = STATUSLINE_FIELD_OPTIONS.map((option, index) => {
|
|
1014
|
+
const enabled = selectedSet.has(option.key);
|
|
1015
|
+
const rank = orderMap.get(option.key);
|
|
1016
|
+
const label = `${formatDashboardSettingState(enabled)} ${index + 1}. ${option.label}${rank ? ` (order ${rank})` : ""}`;
|
|
1017
|
+
return {
|
|
1018
|
+
label,
|
|
1019
|
+
hint: option.description,
|
|
1020
|
+
value: { type: "toggle", key: option.key },
|
|
1021
|
+
color: enabled ? "green" : "yellow",
|
|
1022
|
+
};
|
|
1023
|
+
});
|
|
1024
|
+
const items = [
|
|
1025
|
+
{ label: UI_COPY.settings.previewHeading, value: { type: "cancel" }, kind: "heading" },
|
|
1026
|
+
{
|
|
1027
|
+
label: preview.label,
|
|
1028
|
+
hint: preview.hint,
|
|
1029
|
+
value: { type: "cancel" },
|
|
1030
|
+
color: "green",
|
|
1031
|
+
disabled: true,
|
|
1032
|
+
hideUnavailableSuffix: true,
|
|
1033
|
+
},
|
|
1034
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1035
|
+
{ label: UI_COPY.settings.displayHeading, value: { type: "cancel" }, kind: "heading" },
|
|
1036
|
+
...optionItems,
|
|
1037
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1038
|
+
{ label: UI_COPY.settings.moveUp, value: { type: "move-up", key: focusKey }, color: "green" },
|
|
1039
|
+
{ label: UI_COPY.settings.moveDown, value: { type: "move-down", key: focusKey }, color: "green" },
|
|
1040
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1041
|
+
{ label: UI_COPY.settings.resetDefault, value: { type: "reset" }, color: "yellow" },
|
|
1042
|
+
{ label: UI_COPY.settings.saveAndBack, value: { type: "save" }, color: "green" },
|
|
1043
|
+
{ label: UI_COPY.settings.backNoSave, value: { type: "cancel" }, color: "red" },
|
|
1044
|
+
];
|
|
1045
|
+
const initialCursor = items.findIndex((item) => item.value.type === "toggle" && item.value.key === focusKey);
|
|
1046
|
+
const updateFocusedPreview = (cursor) => {
|
|
1047
|
+
const focusedItem = items[cursor];
|
|
1048
|
+
const focusedKey = focusedItem?.value.type === "toggle" ? focusedItem.value.key : focusKey;
|
|
1049
|
+
const nextPreview = buildAccountListPreview(draft, ui, focusedKey);
|
|
1050
|
+
const previewItem = items[1];
|
|
1051
|
+
if (!previewItem)
|
|
1052
|
+
return;
|
|
1053
|
+
previewItem.label = nextPreview.label;
|
|
1054
|
+
previewItem.hint = nextPreview.hint;
|
|
1055
|
+
};
|
|
1056
|
+
const result = await select(items, {
|
|
1057
|
+
message: UI_COPY.settings.summaryTitle,
|
|
1058
|
+
subtitle: UI_COPY.settings.summarySubtitle,
|
|
1059
|
+
help: UI_COPY.settings.summaryHelp,
|
|
1060
|
+
clearScreen: true,
|
|
1061
|
+
theme: ui.theme,
|
|
1062
|
+
selectedEmphasis: "minimal",
|
|
1063
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
1064
|
+
onCursorChange: ({ cursor }) => {
|
|
1065
|
+
const focusedItem = items[cursor];
|
|
1066
|
+
if (focusedItem?.value.type === "toggle") {
|
|
1067
|
+
focusKey = focusedItem.value.key;
|
|
1068
|
+
}
|
|
1069
|
+
updateFocusedPreview(cursor);
|
|
1070
|
+
},
|
|
1071
|
+
onInput: (raw) => {
|
|
1072
|
+
const lower = raw.toLowerCase();
|
|
1073
|
+
if (lower === "q")
|
|
1074
|
+
return { type: "cancel" };
|
|
1075
|
+
if (lower === "s")
|
|
1076
|
+
return { type: "save" };
|
|
1077
|
+
if (lower === "r")
|
|
1078
|
+
return { type: "reset" };
|
|
1079
|
+
if (lower === "[")
|
|
1080
|
+
return { type: "move-up", key: focusKey };
|
|
1081
|
+
if (lower === "]")
|
|
1082
|
+
return { type: "move-down", key: focusKey };
|
|
1083
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1084
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= STATUSLINE_FIELD_OPTIONS.length) {
|
|
1085
|
+
const target = STATUSLINE_FIELD_OPTIONS[parsed - 1];
|
|
1086
|
+
if (target) {
|
|
1087
|
+
return { type: "toggle", key: target.key };
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return undefined;
|
|
1091
|
+
},
|
|
1092
|
+
});
|
|
1093
|
+
if (!result || result.type === "cancel") {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
if (result.type === "save") {
|
|
1097
|
+
return draft;
|
|
1098
|
+
}
|
|
1099
|
+
if (result.type === "reset") {
|
|
1100
|
+
draft = applyDashboardDefaultsForKeys(draft, STATUSLINE_PANEL_KEYS);
|
|
1101
|
+
focusKey = draft.menuStatuslineFields?.[0] ?? "last-used";
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (result.type === "move-up") {
|
|
1105
|
+
draft = {
|
|
1106
|
+
...draft,
|
|
1107
|
+
menuStatuslineFields: reorderField(normalizeStatuslineFields(draft.menuStatuslineFields), result.key, -1),
|
|
1108
|
+
};
|
|
1109
|
+
focusKey = result.key;
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
if (result.type === "move-down") {
|
|
1113
|
+
draft = {
|
|
1114
|
+
...draft,
|
|
1115
|
+
menuStatuslineFields: reorderField(normalizeStatuslineFields(draft.menuStatuslineFields), result.key, 1),
|
|
1116
|
+
};
|
|
1117
|
+
focusKey = result.key;
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
focusKey = result.key;
|
|
1121
|
+
const fields = normalizeStatuslineFields(draft.menuStatuslineFields);
|
|
1122
|
+
const isEnabled = fields.includes(result.key);
|
|
1123
|
+
if (isEnabled) {
|
|
1124
|
+
const next = fields.filter((field) => field !== result.key);
|
|
1125
|
+
draft = {
|
|
1126
|
+
...draft,
|
|
1127
|
+
menuStatuslineFields: next.length > 0 ? next : [result.key],
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
draft = {
|
|
1132
|
+
...draft,
|
|
1133
|
+
menuStatuslineFields: [...fields, result.key],
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
async function configureStatuslineSettings(currentSettings) {
|
|
1139
|
+
const current = currentSettings ?? await loadDashboardDisplaySettings();
|
|
1140
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1141
|
+
console.log("Settings require interactive mode.");
|
|
1142
|
+
console.log(`Settings file: ${getDashboardSettingsPath()}`);
|
|
1143
|
+
return current;
|
|
1144
|
+
}
|
|
1145
|
+
const selected = await promptStatuslineSettings(current);
|
|
1146
|
+
if (!selected)
|
|
1147
|
+
return current;
|
|
1148
|
+
if (dashboardSettingsEqual(current, selected))
|
|
1149
|
+
return current;
|
|
1150
|
+
const merged = await persistDashboardSettingsSelection(selected, STATUSLINE_PANEL_KEYS, "summary-fields");
|
|
1151
|
+
applyUiThemeFromDashboardSettings(merged);
|
|
1152
|
+
return merged;
|
|
1153
|
+
}
|
|
1154
|
+
function formatDelayLabel(delayMs) {
|
|
1155
|
+
return delayMs <= 0 ? "Instant return" : `${Math.round(delayMs / 1000)}s auto-return`;
|
|
1156
|
+
}
|
|
1157
|
+
async function promptBehaviorSettings(initial) {
|
|
1158
|
+
if (!input.isTTY || !output.isTTY)
|
|
1159
|
+
return null;
|
|
1160
|
+
const ui = getUiRuntimeOptions();
|
|
1161
|
+
let draft = cloneDashboardSettings(initial);
|
|
1162
|
+
let focus = {
|
|
1163
|
+
type: "set-delay",
|
|
1164
|
+
delayMs: draft.actionAutoReturnMs ?? 2_000,
|
|
1165
|
+
};
|
|
1166
|
+
while (true) {
|
|
1167
|
+
const currentDelay = draft.actionAutoReturnMs ?? 2_000;
|
|
1168
|
+
const pauseOnKey = draft.actionPauseOnKey ?? true;
|
|
1169
|
+
const autoFetchLimits = draft.menuAutoFetchLimits ?? true;
|
|
1170
|
+
const fetchStatusVisible = draft.menuShowFetchStatus ?? true;
|
|
1171
|
+
const menuQuotaTtlMs = draft.menuQuotaTtlMs ?? 5 * 60_000;
|
|
1172
|
+
const delayItems = AUTO_RETURN_OPTIONS_MS.map((delayMs) => {
|
|
1173
|
+
const color = currentDelay === delayMs ? "green" : "yellow";
|
|
1174
|
+
return {
|
|
1175
|
+
label: `${currentDelay === delayMs ? "[x]" : "[ ]"} ${formatDelayLabel(delayMs)}`,
|
|
1176
|
+
hint: delayMs === 1_000
|
|
1177
|
+
? "Fastest loop for frequent actions."
|
|
1178
|
+
: delayMs === 2_000
|
|
1179
|
+
? "Balanced default for most users."
|
|
1180
|
+
: "More time to read action output.",
|
|
1181
|
+
value: { type: "set-delay", delayMs },
|
|
1182
|
+
color,
|
|
1183
|
+
};
|
|
1184
|
+
});
|
|
1185
|
+
const pauseColor = pauseOnKey ? "green" : "yellow";
|
|
1186
|
+
const items = [
|
|
1187
|
+
{ label: UI_COPY.settings.actionTiming, value: { type: "cancel" }, kind: "heading" },
|
|
1188
|
+
...delayItems,
|
|
1189
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1190
|
+
{
|
|
1191
|
+
label: `${pauseOnKey ? "[x]" : "[ ]"} Pause on key press`,
|
|
1192
|
+
hint: "Press any key to stop auto-return.",
|
|
1193
|
+
value: { type: "toggle-pause" },
|
|
1194
|
+
color: pauseColor,
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
label: `${autoFetchLimits ? "[x]" : "[ ]"} Auto-fetch limits on menu open (5m cache)`,
|
|
1198
|
+
hint: "Refreshes account limits automatically when opening the menu.",
|
|
1199
|
+
value: { type: "toggle-menu-limit-fetch" },
|
|
1200
|
+
color: autoFetchLimits ? "green" : "yellow",
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
label: `${fetchStatusVisible ? "[x]" : "[ ]"} Show limit refresh status`,
|
|
1204
|
+
hint: "Shows background fetch progress like [2/7] in menu subtitle.",
|
|
1205
|
+
value: { type: "toggle-menu-fetch-status" },
|
|
1206
|
+
color: fetchStatusVisible ? "green" : "yellow",
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
label: `Limit cache TTL: ${formatMenuQuotaTtl(menuQuotaTtlMs)}`,
|
|
1210
|
+
hint: "How fresh cached quota data must be before refresh runs.",
|
|
1211
|
+
value: { type: "set-menu-quota-ttl", ttlMs: menuQuotaTtlMs },
|
|
1212
|
+
color: "yellow",
|
|
1213
|
+
},
|
|
1214
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1215
|
+
{ label: UI_COPY.settings.resetDefault, value: { type: "reset" }, color: "yellow" },
|
|
1216
|
+
{ label: UI_COPY.settings.saveAndBack, value: { type: "save" }, color: "green" },
|
|
1217
|
+
{ label: UI_COPY.settings.backNoSave, value: { type: "cancel" }, color: "red" },
|
|
1218
|
+
];
|
|
1219
|
+
const initialCursor = items.findIndex((item) => {
|
|
1220
|
+
const value = item.value;
|
|
1221
|
+
if (value.type !== focus.type)
|
|
1222
|
+
return false;
|
|
1223
|
+
if (value.type === "set-delay" && focus.type === "set-delay") {
|
|
1224
|
+
return value.delayMs === focus.delayMs;
|
|
1225
|
+
}
|
|
1226
|
+
return true;
|
|
1227
|
+
});
|
|
1228
|
+
const result = await select(items, {
|
|
1229
|
+
message: UI_COPY.settings.behaviorTitle,
|
|
1230
|
+
subtitle: UI_COPY.settings.behaviorSubtitle,
|
|
1231
|
+
help: UI_COPY.settings.behaviorHelp,
|
|
1232
|
+
clearScreen: true,
|
|
1233
|
+
theme: ui.theme,
|
|
1234
|
+
selectedEmphasis: "minimal",
|
|
1235
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
1236
|
+
onCursorChange: ({ cursor }) => {
|
|
1237
|
+
const item = items[cursor];
|
|
1238
|
+
if (item && !item.separator && item.kind !== "heading") {
|
|
1239
|
+
focus = item.value;
|
|
1240
|
+
}
|
|
1241
|
+
},
|
|
1242
|
+
onInput: (raw) => {
|
|
1243
|
+
const lower = raw.toLowerCase();
|
|
1244
|
+
if (lower === "q")
|
|
1245
|
+
return { type: "cancel" };
|
|
1246
|
+
if (lower === "s")
|
|
1247
|
+
return { type: "save" };
|
|
1248
|
+
if (lower === "r")
|
|
1249
|
+
return { type: "reset" };
|
|
1250
|
+
if (lower === "p")
|
|
1251
|
+
return { type: "toggle-pause" };
|
|
1252
|
+
if (lower === "l")
|
|
1253
|
+
return { type: "toggle-menu-limit-fetch" };
|
|
1254
|
+
if (lower === "f")
|
|
1255
|
+
return { type: "toggle-menu-fetch-status" };
|
|
1256
|
+
if (lower === "t")
|
|
1257
|
+
return { type: "set-menu-quota-ttl", ttlMs: menuQuotaTtlMs };
|
|
1258
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1259
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= AUTO_RETURN_OPTIONS_MS.length) {
|
|
1260
|
+
const delayMs = AUTO_RETURN_OPTIONS_MS[parsed - 1];
|
|
1261
|
+
if (typeof delayMs === "number")
|
|
1262
|
+
return { type: "set-delay", delayMs };
|
|
1263
|
+
}
|
|
1264
|
+
return undefined;
|
|
1265
|
+
},
|
|
1266
|
+
});
|
|
1267
|
+
if (!result || result.type === "cancel")
|
|
1268
|
+
return null;
|
|
1269
|
+
if (result.type === "save")
|
|
1270
|
+
return draft;
|
|
1271
|
+
if (result.type === "reset") {
|
|
1272
|
+
draft = applyDashboardDefaultsForKeys(draft, BEHAVIOR_PANEL_KEYS);
|
|
1273
|
+
focus = { type: "set-delay", delayMs: draft.actionAutoReturnMs ?? 2_000 };
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
if (result.type === "toggle-pause") {
|
|
1277
|
+
draft = {
|
|
1278
|
+
...draft,
|
|
1279
|
+
actionPauseOnKey: !(draft.actionPauseOnKey ?? true),
|
|
1280
|
+
};
|
|
1281
|
+
focus = result;
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (result.type === "toggle-menu-limit-fetch") {
|
|
1285
|
+
draft = {
|
|
1286
|
+
...draft,
|
|
1287
|
+
menuAutoFetchLimits: !(draft.menuAutoFetchLimits ?? true),
|
|
1288
|
+
};
|
|
1289
|
+
focus = result;
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
if (result.type === "toggle-menu-fetch-status") {
|
|
1293
|
+
draft = {
|
|
1294
|
+
...draft,
|
|
1295
|
+
menuShowFetchStatus: !(draft.menuShowFetchStatus ?? true),
|
|
1296
|
+
};
|
|
1297
|
+
focus = result;
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
if (result.type === "set-menu-quota-ttl") {
|
|
1301
|
+
const currentIndex = MENU_QUOTA_TTL_OPTIONS_MS.findIndex((value) => value === menuQuotaTtlMs);
|
|
1302
|
+
const nextIndex = currentIndex < 0
|
|
1303
|
+
? 0
|
|
1304
|
+
: (currentIndex + 1) % MENU_QUOTA_TTL_OPTIONS_MS.length;
|
|
1305
|
+
const nextTtl = MENU_QUOTA_TTL_OPTIONS_MS[nextIndex] ?? MENU_QUOTA_TTL_OPTIONS_MS[0] ?? menuQuotaTtlMs;
|
|
1306
|
+
draft = {
|
|
1307
|
+
...draft,
|
|
1308
|
+
menuQuotaTtlMs: nextTtl,
|
|
1309
|
+
};
|
|
1310
|
+
focus = { type: "set-menu-quota-ttl", ttlMs: nextTtl };
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
draft = {
|
|
1314
|
+
...draft,
|
|
1315
|
+
actionAutoReturnMs: result.delayMs,
|
|
1316
|
+
};
|
|
1317
|
+
focus = result;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
async function promptThemeSettings(initial) {
|
|
1321
|
+
if (!input.isTTY || !output.isTTY)
|
|
1322
|
+
return null;
|
|
1323
|
+
const baseline = cloneDashboardSettings(initial);
|
|
1324
|
+
let draft = cloneDashboardSettings(initial);
|
|
1325
|
+
let focus = {
|
|
1326
|
+
type: "set-palette",
|
|
1327
|
+
palette: draft.uiThemePreset ?? "green",
|
|
1328
|
+
};
|
|
1329
|
+
while (true) {
|
|
1330
|
+
const ui = getUiRuntimeOptions();
|
|
1331
|
+
const palette = draft.uiThemePreset ?? "green";
|
|
1332
|
+
const accent = draft.uiAccentColor ?? "green";
|
|
1333
|
+
const paletteItems = THEME_PRESET_OPTIONS.map((candidate, index) => {
|
|
1334
|
+
const color = palette === candidate ? "green" : "yellow";
|
|
1335
|
+
return {
|
|
1336
|
+
label: `${palette === candidate ? "[x]" : "[ ]"} ${index + 1}. ${candidate === "green" ? "Green base" : "Blue base"}`,
|
|
1337
|
+
hint: candidate === "green" ? "High-contrast default." : "Codex-style blue look.",
|
|
1338
|
+
value: { type: "set-palette", palette: candidate },
|
|
1339
|
+
color,
|
|
1340
|
+
};
|
|
1341
|
+
});
|
|
1342
|
+
const accentItems = ACCENT_COLOR_OPTIONS.map((candidate) => {
|
|
1343
|
+
const color = accent === candidate ? "green" : "yellow";
|
|
1344
|
+
return {
|
|
1345
|
+
label: `${accent === candidate ? "[x]" : "[ ]"} ${candidate}`,
|
|
1346
|
+
value: { type: "set-accent", accent: candidate },
|
|
1347
|
+
color,
|
|
1348
|
+
};
|
|
1349
|
+
});
|
|
1350
|
+
const items = [
|
|
1351
|
+
{ label: UI_COPY.settings.baseTheme, value: { type: "cancel" }, kind: "heading" },
|
|
1352
|
+
...paletteItems,
|
|
1353
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1354
|
+
{ label: UI_COPY.settings.accentColor, value: { type: "cancel" }, kind: "heading" },
|
|
1355
|
+
...accentItems,
|
|
1356
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1357
|
+
{ label: UI_COPY.settings.resetDefault, value: { type: "reset" }, color: "yellow" },
|
|
1358
|
+
{ label: UI_COPY.settings.saveAndBack, value: { type: "save" }, color: "green" },
|
|
1359
|
+
{ label: UI_COPY.settings.backNoSave, value: { type: "cancel" }, color: "red" },
|
|
1360
|
+
];
|
|
1361
|
+
const initialCursor = items.findIndex((item) => {
|
|
1362
|
+
const value = item.value;
|
|
1363
|
+
if (value.type !== focus.type)
|
|
1364
|
+
return false;
|
|
1365
|
+
if (value.type === "set-palette" && focus.type === "set-palette") {
|
|
1366
|
+
return value.palette === focus.palette;
|
|
1367
|
+
}
|
|
1368
|
+
if (value.type === "set-accent" && focus.type === "set-accent") {
|
|
1369
|
+
return value.accent === focus.accent;
|
|
1370
|
+
}
|
|
1371
|
+
return true;
|
|
1372
|
+
});
|
|
1373
|
+
const result = await select(items, {
|
|
1374
|
+
message: UI_COPY.settings.themeTitle,
|
|
1375
|
+
subtitle: UI_COPY.settings.themeSubtitle,
|
|
1376
|
+
help: UI_COPY.settings.themeHelp,
|
|
1377
|
+
clearScreen: true,
|
|
1378
|
+
theme: ui.theme,
|
|
1379
|
+
selectedEmphasis: "minimal",
|
|
1380
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
1381
|
+
onCursorChange: ({ cursor }) => {
|
|
1382
|
+
const item = items[cursor];
|
|
1383
|
+
if (item && !item.separator && item.kind !== "heading") {
|
|
1384
|
+
focus = item.value;
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
onInput: (raw) => {
|
|
1388
|
+
const lower = raw.toLowerCase();
|
|
1389
|
+
if (lower === "q")
|
|
1390
|
+
return { type: "cancel" };
|
|
1391
|
+
if (lower === "s")
|
|
1392
|
+
return { type: "save" };
|
|
1393
|
+
if (lower === "r")
|
|
1394
|
+
return { type: "reset" };
|
|
1395
|
+
if (raw === "1")
|
|
1396
|
+
return { type: "set-palette", palette: "green" };
|
|
1397
|
+
if (raw === "2")
|
|
1398
|
+
return { type: "set-palette", palette: "blue" };
|
|
1399
|
+
return undefined;
|
|
1400
|
+
},
|
|
1401
|
+
});
|
|
1402
|
+
if (!result || result.type === "cancel") {
|
|
1403
|
+
applyUiThemeFromDashboardSettings(baseline);
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
if (result.type === "save")
|
|
1407
|
+
return draft;
|
|
1408
|
+
if (result.type === "reset") {
|
|
1409
|
+
draft = applyDashboardDefaultsForKeys(draft, THEME_PANEL_KEYS);
|
|
1410
|
+
focus = { type: "set-palette", palette: draft.uiThemePreset ?? "green" };
|
|
1411
|
+
applyUiThemeFromDashboardSettings(draft);
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
if (result.type === "set-palette") {
|
|
1415
|
+
draft = { ...draft, uiThemePreset: result.palette };
|
|
1416
|
+
focus = result;
|
|
1417
|
+
applyUiThemeFromDashboardSettings(draft);
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
draft = { ...draft, uiAccentColor: result.accent };
|
|
1421
|
+
focus = result;
|
|
1422
|
+
applyUiThemeFromDashboardSettings(draft);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
function resolveFocusedBackendNumberKey(focus, numberOptions = BACKEND_NUMBER_OPTIONS) {
|
|
1426
|
+
const numberKeys = new Set(numberOptions.map((option) => option.key));
|
|
1427
|
+
if (focus && numberKeys.has(focus)) {
|
|
1428
|
+
return focus;
|
|
1429
|
+
}
|
|
1430
|
+
return numberOptions[0]?.key ?? "fetchTimeoutMs";
|
|
1431
|
+
}
|
|
1432
|
+
function getBackendCategory(key) {
|
|
1433
|
+
return BACKEND_CATEGORY_OPTIONS.find((category) => category.key === key) ?? null;
|
|
1434
|
+
}
|
|
1435
|
+
function getBackendCategoryInitialFocus(category) {
|
|
1436
|
+
const firstToggle = category.toggleKeys[0];
|
|
1437
|
+
if (firstToggle)
|
|
1438
|
+
return firstToggle;
|
|
1439
|
+
return category.numberKeys[0] ?? null;
|
|
1440
|
+
}
|
|
1441
|
+
function applyBackendCategoryDefaults(draft, category) {
|
|
1442
|
+
const next = { ...draft };
|
|
1443
|
+
for (const key of category.toggleKeys) {
|
|
1444
|
+
next[key] = BACKEND_DEFAULTS[key] ?? false;
|
|
1445
|
+
}
|
|
1446
|
+
for (const key of category.numberKeys) {
|
|
1447
|
+
const option = BACKEND_NUMBER_OPTION_BY_KEY.get(key);
|
|
1448
|
+
const fallback = option?.min ?? 0;
|
|
1449
|
+
next[key] = BACKEND_DEFAULTS[key] ?? fallback;
|
|
1450
|
+
}
|
|
1451
|
+
return next;
|
|
1452
|
+
}
|
|
1453
|
+
async function promptBackendCategorySettings(initial, category, initialFocus) {
|
|
1454
|
+
const ui = getUiRuntimeOptions();
|
|
1455
|
+
let draft = cloneBackendPluginConfig(initial);
|
|
1456
|
+
let focusKey = initialFocus;
|
|
1457
|
+
if (!focusKey ||
|
|
1458
|
+
(!category.toggleKeys.includes(focusKey) &&
|
|
1459
|
+
!category.numberKeys.includes(focusKey))) {
|
|
1460
|
+
focusKey = getBackendCategoryInitialFocus(category);
|
|
1461
|
+
}
|
|
1462
|
+
const toggleOptions = category.toggleKeys
|
|
1463
|
+
.map((key) => BACKEND_TOGGLE_OPTION_BY_KEY.get(key))
|
|
1464
|
+
.filter((option) => !!option);
|
|
1465
|
+
const numberOptions = category.numberKeys
|
|
1466
|
+
.map((key) => BACKEND_NUMBER_OPTION_BY_KEY.get(key))
|
|
1467
|
+
.filter((option) => !!option);
|
|
1468
|
+
while (true) {
|
|
1469
|
+
const preview = buildBackendSettingsPreview(draft, ui, focusKey);
|
|
1470
|
+
const toggleItems = toggleOptions.map((option, index) => {
|
|
1471
|
+
const enabled = draft[option.key] ?? BACKEND_DEFAULTS[option.key] ?? false;
|
|
1472
|
+
return {
|
|
1473
|
+
label: `${formatDashboardSettingState(enabled)} ${index + 1}. ${option.label}`,
|
|
1474
|
+
hint: option.description,
|
|
1475
|
+
value: { type: "toggle", key: option.key },
|
|
1476
|
+
color: enabled ? "green" : "yellow",
|
|
1477
|
+
};
|
|
1478
|
+
});
|
|
1479
|
+
const numberItems = numberOptions.map((option) => {
|
|
1480
|
+
const rawValue = draft[option.key] ?? BACKEND_DEFAULTS[option.key] ?? option.min;
|
|
1481
|
+
const numericValue = typeof rawValue === "number" && Number.isFinite(rawValue)
|
|
1482
|
+
? rawValue
|
|
1483
|
+
: option.min;
|
|
1484
|
+
const clampedValue = clampBackendNumber(option, numericValue);
|
|
1485
|
+
const valueLabel = formatBackendNumberValue(option, clampedValue);
|
|
1486
|
+
return {
|
|
1487
|
+
label: `${option.label}: ${valueLabel}`,
|
|
1488
|
+
hint: `${option.description} Step ${formatBackendNumberValue(option, option.step)}.`,
|
|
1489
|
+
value: { type: "bump", key: option.key, direction: 1 },
|
|
1490
|
+
color: "yellow",
|
|
1491
|
+
};
|
|
1492
|
+
});
|
|
1493
|
+
const focusedNumberKey = resolveFocusedBackendNumberKey(focusKey, numberOptions);
|
|
1494
|
+
const items = [
|
|
1495
|
+
{ label: UI_COPY.settings.previewHeading, value: { type: "back" }, kind: "heading" },
|
|
1496
|
+
{
|
|
1497
|
+
label: preview.label,
|
|
1498
|
+
hint: preview.hint,
|
|
1499
|
+
value: { type: "back" },
|
|
1500
|
+
disabled: true,
|
|
1501
|
+
color: "green",
|
|
1502
|
+
hideUnavailableSuffix: true,
|
|
1503
|
+
},
|
|
1504
|
+
{ label: "", value: { type: "back" }, separator: true },
|
|
1505
|
+
{ label: UI_COPY.settings.backendToggleHeading, value: { type: "back" }, kind: "heading" },
|
|
1506
|
+
...toggleItems,
|
|
1507
|
+
{ label: "", value: { type: "back" }, separator: true },
|
|
1508
|
+
{ label: UI_COPY.settings.backendNumberHeading, value: { type: "back" }, kind: "heading" },
|
|
1509
|
+
...numberItems,
|
|
1510
|
+
];
|
|
1511
|
+
if (numberOptions.length > 0) {
|
|
1512
|
+
items.push({ label: "", value: { type: "back" }, separator: true });
|
|
1513
|
+
items.push({
|
|
1514
|
+
label: UI_COPY.settings.backendDecrease,
|
|
1515
|
+
value: { type: "bump", key: focusedNumberKey, direction: -1 },
|
|
1516
|
+
color: "yellow",
|
|
1517
|
+
});
|
|
1518
|
+
items.push({
|
|
1519
|
+
label: UI_COPY.settings.backendIncrease,
|
|
1520
|
+
value: { type: "bump", key: focusedNumberKey, direction: 1 },
|
|
1521
|
+
color: "green",
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
items.push({ label: "", value: { type: "back" }, separator: true });
|
|
1525
|
+
items.push({ label: UI_COPY.settings.backendResetCategory, value: { type: "reset-category" }, color: "yellow" });
|
|
1526
|
+
items.push({ label: UI_COPY.settings.backendBackToCategories, value: { type: "back" }, color: "red" });
|
|
1527
|
+
const initialCursor = items.findIndex((item) => {
|
|
1528
|
+
if (item.separator || item.disabled || item.kind === "heading")
|
|
1529
|
+
return false;
|
|
1530
|
+
if (item.value.type === "toggle" && focusKey === item.value.key)
|
|
1531
|
+
return true;
|
|
1532
|
+
if (item.value.type === "bump" && focusKey === item.value.key)
|
|
1533
|
+
return true;
|
|
1534
|
+
return false;
|
|
1535
|
+
});
|
|
1536
|
+
const result = await select(items, {
|
|
1537
|
+
message: `${UI_COPY.settings.backendCategoryTitle}: ${category.label}`,
|
|
1538
|
+
subtitle: category.description,
|
|
1539
|
+
help: UI_COPY.settings.backendCategoryHelp,
|
|
1540
|
+
clearScreen: true,
|
|
1541
|
+
theme: ui.theme,
|
|
1542
|
+
selectedEmphasis: "minimal",
|
|
1543
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
1544
|
+
onCursorChange: ({ cursor }) => {
|
|
1545
|
+
const focusedItem = items[cursor];
|
|
1546
|
+
if (focusedItem?.value.type === "toggle" || focusedItem?.value.type === "bump") {
|
|
1547
|
+
focusKey = focusedItem.value.key;
|
|
1548
|
+
}
|
|
1549
|
+
},
|
|
1550
|
+
onInput: (raw) => {
|
|
1551
|
+
const lower = raw.toLowerCase();
|
|
1552
|
+
if (lower === "q")
|
|
1553
|
+
return { type: "back" };
|
|
1554
|
+
if (lower === "r")
|
|
1555
|
+
return { type: "reset-category" };
|
|
1556
|
+
if (numberOptions.length > 0 && (lower === "+" || lower === "=" || lower === "]" || lower === "d")) {
|
|
1557
|
+
return { type: "bump", key: resolveFocusedBackendNumberKey(focusKey, numberOptions), direction: 1 };
|
|
1558
|
+
}
|
|
1559
|
+
if (numberOptions.length > 0 && (lower === "-" || lower === "[" || lower === "a")) {
|
|
1560
|
+
return { type: "bump", key: resolveFocusedBackendNumberKey(focusKey, numberOptions), direction: -1 };
|
|
1561
|
+
}
|
|
1562
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1563
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= toggleOptions.length) {
|
|
1564
|
+
const target = toggleOptions[parsed - 1];
|
|
1565
|
+
if (target)
|
|
1566
|
+
return { type: "toggle", key: target.key };
|
|
1567
|
+
}
|
|
1568
|
+
return undefined;
|
|
1569
|
+
},
|
|
1570
|
+
});
|
|
1571
|
+
if (!result || result.type === "back") {
|
|
1572
|
+
return { draft, focusKey };
|
|
1573
|
+
}
|
|
1574
|
+
if (result.type === "reset-category") {
|
|
1575
|
+
draft = applyBackendCategoryDefaults(draft, category);
|
|
1576
|
+
focusKey = getBackendCategoryInitialFocus(category);
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
if (result.type === "toggle") {
|
|
1580
|
+
const currentValue = draft[result.key] ?? BACKEND_DEFAULTS[result.key] ?? false;
|
|
1581
|
+
draft = { ...draft, [result.key]: !currentValue };
|
|
1582
|
+
focusKey = result.key;
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
const option = BACKEND_NUMBER_OPTION_BY_KEY.get(result.key);
|
|
1586
|
+
if (!option)
|
|
1587
|
+
continue;
|
|
1588
|
+
const currentValue = draft[result.key] ?? BACKEND_DEFAULTS[result.key] ?? option.min;
|
|
1589
|
+
const numericCurrent = typeof currentValue === "number" && Number.isFinite(currentValue)
|
|
1590
|
+
? currentValue
|
|
1591
|
+
: option.min;
|
|
1592
|
+
draft = {
|
|
1593
|
+
...draft,
|
|
1594
|
+
[result.key]: clampBackendNumber(option, numericCurrent + option.step * result.direction),
|
|
1595
|
+
};
|
|
1596
|
+
focusKey = result.key;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
async function promptBackendSettings(initial) {
|
|
1600
|
+
if (!input.isTTY || !output.isTTY)
|
|
1601
|
+
return null;
|
|
1602
|
+
const ui = getUiRuntimeOptions();
|
|
1603
|
+
let draft = cloneBackendPluginConfig(initial);
|
|
1604
|
+
let activeCategory = BACKEND_CATEGORY_OPTIONS[0]?.key ?? "session-sync";
|
|
1605
|
+
const focusByCategory = {};
|
|
1606
|
+
for (const category of BACKEND_CATEGORY_OPTIONS) {
|
|
1607
|
+
focusByCategory[category.key] = getBackendCategoryInitialFocus(category);
|
|
1608
|
+
}
|
|
1609
|
+
while (true) {
|
|
1610
|
+
const previewFocus = focusByCategory[activeCategory] ?? null;
|
|
1611
|
+
const preview = buildBackendSettingsPreview(draft, ui, previewFocus);
|
|
1612
|
+
const categoryItems = BACKEND_CATEGORY_OPTIONS.map((category, index) => {
|
|
1613
|
+
return {
|
|
1614
|
+
label: `${index + 1}. ${category.label}`,
|
|
1615
|
+
hint: category.description,
|
|
1616
|
+
value: { type: "open-category", key: category.key },
|
|
1617
|
+
color: "green",
|
|
1618
|
+
};
|
|
1619
|
+
});
|
|
1620
|
+
const items = [
|
|
1621
|
+
{ label: UI_COPY.settings.previewHeading, value: { type: "cancel" }, kind: "heading" },
|
|
1622
|
+
{
|
|
1623
|
+
label: preview.label,
|
|
1624
|
+
hint: preview.hint,
|
|
1625
|
+
value: { type: "cancel" },
|
|
1626
|
+
disabled: true,
|
|
1627
|
+
color: "green",
|
|
1628
|
+
hideUnavailableSuffix: true,
|
|
1629
|
+
},
|
|
1630
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1631
|
+
{ label: UI_COPY.settings.backendCategoriesHeading, value: { type: "cancel" }, kind: "heading" },
|
|
1632
|
+
...categoryItems,
|
|
1633
|
+
{ label: "", value: { type: "cancel" }, separator: true },
|
|
1634
|
+
{ label: UI_COPY.settings.resetDefault, value: { type: "reset" }, color: "yellow" },
|
|
1635
|
+
{ label: UI_COPY.settings.saveAndBack, value: { type: "save" }, color: "green" },
|
|
1636
|
+
{ label: UI_COPY.settings.backNoSave, value: { type: "cancel" }, color: "red" },
|
|
1637
|
+
];
|
|
1638
|
+
const initialCursor = items.findIndex((item) => {
|
|
1639
|
+
if (item.separator || item.disabled || item.kind === "heading")
|
|
1640
|
+
return false;
|
|
1641
|
+
return item.value.type === "open-category" && item.value.key === activeCategory;
|
|
1642
|
+
});
|
|
1643
|
+
const result = await select(items, {
|
|
1644
|
+
message: UI_COPY.settings.backendTitle,
|
|
1645
|
+
subtitle: UI_COPY.settings.backendSubtitle,
|
|
1646
|
+
help: UI_COPY.settings.backendHelp,
|
|
1647
|
+
clearScreen: true,
|
|
1648
|
+
theme: ui.theme,
|
|
1649
|
+
selectedEmphasis: "minimal",
|
|
1650
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
1651
|
+
onCursorChange: ({ cursor }) => {
|
|
1652
|
+
const focusedItem = items[cursor];
|
|
1653
|
+
if (focusedItem?.value.type === "open-category") {
|
|
1654
|
+
activeCategory = focusedItem.value.key;
|
|
1655
|
+
}
|
|
1656
|
+
},
|
|
1657
|
+
onInput: (raw) => {
|
|
1658
|
+
const lower = raw.toLowerCase();
|
|
1659
|
+
if (lower === "q")
|
|
1660
|
+
return { type: "cancel" };
|
|
1661
|
+
if (lower === "s")
|
|
1662
|
+
return { type: "save" };
|
|
1663
|
+
if (lower === "r")
|
|
1664
|
+
return { type: "reset" };
|
|
1665
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1666
|
+
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= BACKEND_CATEGORY_OPTIONS.length) {
|
|
1667
|
+
const target = BACKEND_CATEGORY_OPTIONS[parsed - 1];
|
|
1668
|
+
if (target)
|
|
1669
|
+
return { type: "open-category", key: target.key };
|
|
1670
|
+
}
|
|
1671
|
+
return undefined;
|
|
1672
|
+
},
|
|
1673
|
+
});
|
|
1674
|
+
if (!result || result.type === "cancel")
|
|
1675
|
+
return null;
|
|
1676
|
+
if (result.type === "save")
|
|
1677
|
+
return draft;
|
|
1678
|
+
if (result.type === "reset") {
|
|
1679
|
+
draft = cloneBackendPluginConfig(BACKEND_DEFAULTS);
|
|
1680
|
+
for (const category of BACKEND_CATEGORY_OPTIONS) {
|
|
1681
|
+
focusByCategory[category.key] = getBackendCategoryInitialFocus(category);
|
|
1682
|
+
}
|
|
1683
|
+
activeCategory = BACKEND_CATEGORY_OPTIONS[0]?.key ?? activeCategory;
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
const category = getBackendCategory(result.key);
|
|
1687
|
+
if (!category)
|
|
1688
|
+
continue;
|
|
1689
|
+
activeCategory = category.key;
|
|
1690
|
+
const categoryResult = await promptBackendCategorySettings(draft, category, focusByCategory[category.key] ?? getBackendCategoryInitialFocus(category));
|
|
1691
|
+
draft = categoryResult.draft;
|
|
1692
|
+
focusByCategory[category.key] = categoryResult.focusKey;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
async function configureBackendSettings(currentConfig) {
|
|
1696
|
+
const current = cloneBackendPluginConfig(currentConfig ?? loadPluginConfig());
|
|
1697
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1698
|
+
console.log("Settings require interactive mode.");
|
|
1699
|
+
return current;
|
|
1700
|
+
}
|
|
1701
|
+
const selected = await promptBackendSettings(current);
|
|
1702
|
+
if (!selected)
|
|
1703
|
+
return current;
|
|
1704
|
+
if (backendSettingsEqual(current, selected))
|
|
1705
|
+
return current;
|
|
1706
|
+
return persistBackendConfigSelection(selected, "backend");
|
|
1707
|
+
}
|
|
1708
|
+
async function promptSettingsHub(initialFocus = "account-list") {
|
|
1709
|
+
if (!input.isTTY || !output.isTTY)
|
|
1710
|
+
return null;
|
|
1711
|
+
const ui = getUiRuntimeOptions();
|
|
1712
|
+
const items = [
|
|
1713
|
+
{ label: UI_COPY.settings.sectionTitle, value: { type: "back" }, kind: "heading" },
|
|
1714
|
+
{ label: UI_COPY.settings.accountList, value: { type: "account-list" }, color: "green" },
|
|
1715
|
+
{ label: UI_COPY.settings.summaryFields, value: { type: "summary-fields" }, color: "green" },
|
|
1716
|
+
{ label: UI_COPY.settings.behavior, value: { type: "behavior" }, color: "green" },
|
|
1717
|
+
{ label: UI_COPY.settings.theme, value: { type: "theme" }, color: "green" },
|
|
1718
|
+
{ label: "", value: { type: "back" }, separator: true },
|
|
1719
|
+
{ label: UI_COPY.settings.advancedTitle, value: { type: "back" }, kind: "heading" },
|
|
1720
|
+
{ label: UI_COPY.settings.backend, value: { type: "backend" }, color: "green" },
|
|
1721
|
+
{ label: "", value: { type: "back" }, separator: true },
|
|
1722
|
+
{ label: UI_COPY.settings.exitTitle, value: { type: "back" }, kind: "heading" },
|
|
1723
|
+
{ label: UI_COPY.settings.back, value: { type: "back" }, color: "red" },
|
|
1724
|
+
];
|
|
1725
|
+
const initialCursor = items.findIndex((item) => {
|
|
1726
|
+
if (item.separator || item.disabled || item.kind === "heading")
|
|
1727
|
+
return false;
|
|
1728
|
+
return item.value.type === initialFocus;
|
|
1729
|
+
});
|
|
1730
|
+
return select(items, {
|
|
1731
|
+
message: UI_COPY.settings.title,
|
|
1732
|
+
subtitle: UI_COPY.settings.subtitle,
|
|
1733
|
+
help: UI_COPY.settings.help,
|
|
1734
|
+
clearScreen: true,
|
|
1735
|
+
theme: ui.theme,
|
|
1736
|
+
selectedEmphasis: "minimal",
|
|
1737
|
+
initialCursor: initialCursor >= 0 ? initialCursor : undefined,
|
|
1738
|
+
onInput: (raw) => {
|
|
1739
|
+
const lower = raw.toLowerCase();
|
|
1740
|
+
if (lower === "q")
|
|
1741
|
+
return { type: "back" };
|
|
1742
|
+
return undefined;
|
|
1743
|
+
},
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
async function configureUnifiedSettings(initialSettings) {
|
|
1747
|
+
let current = cloneDashboardSettings(initialSettings ?? await loadDashboardDisplaySettings());
|
|
1748
|
+
let backendConfig = cloneBackendPluginConfig(loadPluginConfig());
|
|
1749
|
+
applyUiThemeFromDashboardSettings(current);
|
|
1750
|
+
let hubFocus = "account-list";
|
|
1751
|
+
while (true) {
|
|
1752
|
+
const action = await promptSettingsHub(hubFocus);
|
|
1753
|
+
if (!action || action.type === "back") {
|
|
1754
|
+
return current;
|
|
1755
|
+
}
|
|
1756
|
+
hubFocus = action.type;
|
|
1757
|
+
if (action.type === "account-list") {
|
|
1758
|
+
current = await configureDashboardDisplaySettings(current);
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
if (action.type === "summary-fields") {
|
|
1762
|
+
current = await configureStatuslineSettings(current);
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
if (action.type === "behavior") {
|
|
1766
|
+
const selected = await promptBehaviorSettings(current);
|
|
1767
|
+
if (selected && !dashboardSettingsEqual(current, selected)) {
|
|
1768
|
+
current = await persistDashboardSettingsSelection(selected, BEHAVIOR_PANEL_KEYS, "behavior");
|
|
1769
|
+
}
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
if (action.type === "theme") {
|
|
1773
|
+
const selected = await promptThemeSettings(current);
|
|
1774
|
+
if (selected && !dashboardSettingsEqual(current, selected)) {
|
|
1775
|
+
current = await persistDashboardSettingsSelection(selected, THEME_PANEL_KEYS, "theme");
|
|
1776
|
+
applyUiThemeFromDashboardSettings(current);
|
|
1777
|
+
}
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
if (action.type === "backend") {
|
|
1781
|
+
backendConfig = await configureBackendSettings(backendConfig);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
export { configureUnifiedSettings, applyUiThemeFromDashboardSettings, resolveMenuLayoutMode };
|
|
1786
|
+
//# sourceMappingURL=settings-hub.js.map
|