opencode-account-manager 0.4.4 → 0.5.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.
@@ -4,13 +4,14 @@ import {
4
4
  Header,
5
5
  StatsRow,
6
6
  AccountList,
7
- MenuBar,
8
- MenuAction,
9
7
  ProviderList,
10
8
  McpServerList,
11
9
  SectionBox,
12
10
  ExportModal,
13
11
  ImportModal,
12
+ ActionPalette,
13
+ PaletteAction,
14
+ DashboardView,
14
15
  } from "./components";
15
16
  import {
16
17
  readPluginAccountsFile,
@@ -32,8 +33,8 @@ interface DashboardProps {
32
33
  pluginPath?: string;
33
34
  }
34
35
 
35
- type ActiveSection = "providers" | "accounts" | "mcp";
36
- type ModalType = "none" | "export" | "import" | "export-selected";
36
+ type ModalType = "none" | "export" | "import" | "export-selected" | "palette";
37
+ type MainTab = "dashboard" | "settings";
37
38
 
38
39
  function safeReadPluginFile(pluginPath: string): PluginAccountsFile {
39
40
  try {
@@ -55,10 +56,15 @@ export function Dashboard({ pluginPath }: DashboardProps) {
55
56
  const [summary, setSummary] = useState({ total: 0, available: 0, limited: 0 });
56
57
  const [message, setMessage] = useState<string | null>(null);
57
58
 
58
- // UI state
59
- const [activeSection, setActiveSection] = useState<ActiveSection>("providers");
60
- const [selectMode, setSelectMode] = useState(false);
61
- const [selectedIndex, setSelectedIndex] = useState(0);
59
+ // Main tab state
60
+ const [activeTab, setActiveTab] = useState<MainTab>("dashboard");
61
+
62
+ // Dashboard tab state
63
+ const [dashboardIndex, setDashboardIndex] = useState(0);
64
+
65
+ // Settings tab state
66
+ const [expandedSection, setExpandedSection] = useState<"providers" | "accounts" | "mcp">("accounts");
67
+ const [settingsNavIndex, setSettingsNavIndex] = useState(0);
62
68
  const [checkedEmails, setCheckedEmails] = useState<Set<string>>(new Set());
63
69
 
64
70
  // Modal state
@@ -79,7 +85,6 @@ export function Dashboard({ pluginPath }: DashboardProps) {
79
85
  setAccounts(file.accounts);
80
86
  setSummary(summarizeAccounts(file.accounts));
81
87
  setCheckedEmails(new Set());
82
- setSelectedIndex(0);
83
88
  };
84
89
 
85
90
  const refresh = () => {
@@ -93,75 +98,128 @@ export function Dashboard({ pluginPath }: DashboardProps) {
93
98
  loadAccounts();
94
99
  }, []);
95
100
 
96
- // Keyboard navigation (only when no modal is open)
97
- useInput((input, key) => {
98
- if (activeModal !== "none") return;
101
+ // Build settings navigation items
102
+ const buildSettingsNavItems = () => {
103
+ const items: Array<{ type: "section" | "account"; id?: string; index?: number; email?: string }> = [
104
+ { type: "section", id: "providers" },
105
+ { type: "section", id: "accounts" },
106
+ ];
99
107
 
100
- // Section switching with Tab or Left/Right arrows
101
- if (key.tab || key.rightArrow) {
102
- setActiveSection(prev => {
103
- if (prev === "providers") return "accounts";
104
- if (prev === "accounts") return "mcp";
105
- return "providers";
108
+ if (expandedSection === "accounts") {
109
+ accounts.forEach((acc, index) => {
110
+ items.push({ type: "account", index, email: acc.email });
106
111
  });
107
- if (!key.rightArrow || activeSection !== "accounts") {
108
- setSelectMode(false);
109
- }
110
- return;
111
112
  }
112
113
 
113
- if (key.leftArrow) {
114
- setActiveSection(prev => {
115
- if (prev === "mcp") return "accounts";
116
- if (prev === "accounts") return "providers";
117
- return "mcp";
118
- });
119
- if (activeSection !== "accounts") {
120
- setSelectMode(false);
121
- }
114
+ items.push({ type: "section", id: "mcp" });
115
+ return items;
116
+ };
117
+
118
+ const settingsNavItems = buildSettingsNavItems();
119
+
120
+ // Palette actions
121
+ const paletteActions: PaletteAction[] = [
122
+ { id: "refresh", label: "Refresh", shortcut: "R" },
123
+ { id: "export", label: "Export All Accounts", shortcut: "E" },
124
+ { id: "import", label: "Import from File", shortcut: "I" },
125
+ { id: "import-am", label: "Import from Antigravity Manager", shortcut: "A" },
126
+ ...(checkedEmails.size > 0 ? [
127
+ { id: "export-selected", label: `Export Selected (${checkedEmails.size})`, shortcut: "X" },
128
+ { id: "enable-selected", label: `Enable Selected (${checkedEmails.size})` },
129
+ { id: "disable-selected", label: `Disable Selected (${checkedEmails.size})` },
130
+ { id: "delete-selected", label: `Delete Selected (${checkedEmails.size})`, shortcut: "Del" },
131
+ { id: "clear-selection", label: "Clear Selection", shortcut: "N" },
132
+ ] : []),
133
+ { id: "select-all", label: "Select All Accounts", shortcut: "Ctrl+A" },
134
+ { id: "quit", label: "Quit", shortcut: "Q" },
135
+ ];
136
+
137
+ // Keyboard navigation
138
+ useInput((input, key) => {
139
+ if (activeModal === "palette") return;
140
+ if (activeModal !== "none") return;
141
+
142
+ // Tab to switch between main tabs
143
+ if (key.tab) {
144
+ setActiveTab(prev => prev === "dashboard" ? "settings" : "dashboard");
122
145
  return;
123
146
  }
124
147
 
125
- // Number keys for section switching
126
- if (input === "1") {
127
- setActiveSection("providers");
128
- setSelectMode(false);
148
+ // Open palette with P
149
+ if (input === "p" || input === "P") {
150
+ setActiveModal("palette");
129
151
  return;
130
152
  }
131
- if (input === "2") {
132
- setActiveSection("accounts");
153
+
154
+ // Quick shortcuts
155
+ if (input === "q" || input === "Q") {
156
+ exit();
133
157
  return;
134
158
  }
135
- if (input === "3") {
136
- setActiveSection("mcp");
137
- setSelectMode(false);
159
+ if (input === "r" || input === "R") {
160
+ refresh();
138
161
  return;
139
162
  }
140
163
 
141
- // Up/Down arrows for list navigation in accounts section
142
- if (activeSection === "accounts") {
164
+ // Tab-specific navigation
165
+ if (activeTab === "dashboard") {
166
+ // Dashboard tab navigation
143
167
  if (key.upArrow) {
144
- if (!selectMode) {
145
- setSelectMode(true);
146
- setSelectedIndex(0);
147
- } else {
148
- setSelectedIndex(prev => Math.max(0, prev - 1));
168
+ setDashboardIndex(prev => Math.max(0, prev - 1));
169
+ return;
170
+ }
171
+ if (key.downArrow) {
172
+ setDashboardIndex(prev => Math.min(accounts.length - 1, prev + 1));
173
+ return;
174
+ }
175
+ // Space/Enter to toggle selection
176
+ if (input === " " || key.return) {
177
+ const email = accounts[dashboardIndex]?.email;
178
+ if (email) {
179
+ setCheckedEmails(prev => {
180
+ const next = new Set(prev);
181
+ if (next.has(email)) {
182
+ next.delete(email);
183
+ } else {
184
+ next.add(email);
185
+ }
186
+ return next;
187
+ });
149
188
  }
150
189
  return;
151
190
  }
191
+ } else {
192
+ // Settings tab navigation
193
+ if (key.upArrow) {
194
+ setSettingsNavIndex(prev => Math.max(0, prev - 1));
195
+ return;
196
+ }
152
197
  if (key.downArrow) {
153
- if (!selectMode) {
154
- setSelectMode(true);
155
- setSelectedIndex(0);
156
- } else {
157
- setSelectedIndex(prev => Math.min(accounts.length - 1, prev + 1));
198
+ setSettingsNavIndex(prev => Math.min(settingsNavItems.length - 1, prev + 1));
199
+ return;
200
+ }
201
+ if (key.return) {
202
+ const currentItem = settingsNavItems[settingsNavIndex];
203
+ if (currentItem?.type === "section" && currentItem.id) {
204
+ setExpandedSection(currentItem.id as "providers" | "accounts" | "mcp");
205
+ } else if (currentItem?.type === "account" && currentItem.email) {
206
+ const email = currentItem.email;
207
+ setCheckedEmails(prev => {
208
+ const next = new Set(prev);
209
+ if (next.has(email)) {
210
+ next.delete(email);
211
+ } else {
212
+ next.add(email);
213
+ }
214
+ return next;
215
+ });
158
216
  }
159
217
  return;
160
218
  }
161
- // Space to toggle selection in select mode
162
- if (selectMode && input === " ") {
163
- const email = accounts[selectedIndex]?.email;
164
- if (email) {
219
+ if (input === " ") {
220
+ const currentItem = settingsNavItems[settingsNavIndex];
221
+ if (currentItem?.type === "account" && currentItem.email) {
222
+ const email = currentItem.email;
165
223
  setCheckedEmails(prev => {
166
224
  const next = new Set(prev);
167
225
  if (next.has(email)) {
@@ -172,10 +230,67 @@ export function Dashboard({ pluginPath }: DashboardProps) {
172
230
  return next;
173
231
  });
174
232
  }
233
+ return;
234
+ }
235
+ }
236
+
237
+ // Escape to clear selection
238
+ if (key.escape) {
239
+ if (checkedEmails.size > 0) {
240
+ setCheckedEmails(new Set());
241
+ showMessage("Selection cleared", 1500);
175
242
  }
243
+ return;
176
244
  }
177
245
  });
178
246
 
247
+ // Handle palette action
248
+ const handlePaletteAction = (actionId: string) => {
249
+ setActiveModal("none");
250
+
251
+ switch (actionId) {
252
+ case "refresh":
253
+ refresh();
254
+ break;
255
+ case "export":
256
+ setActiveModal("export");
257
+ break;
258
+ case "import":
259
+ setActiveModal("import");
260
+ break;
261
+ case "import-am":
262
+ handleImportAM();
263
+ break;
264
+ case "export-selected":
265
+ if (checkedEmails.size > 0) {
266
+ setActiveModal("export-selected");
267
+ } else {
268
+ showMessage("No accounts selected", 2000);
269
+ }
270
+ break;
271
+ case "enable-selected":
272
+ handleEnableSelected();
273
+ break;
274
+ case "disable-selected":
275
+ handleDisableSelected();
276
+ break;
277
+ case "delete-selected":
278
+ handleDeleteSelected();
279
+ break;
280
+ case "select-all":
281
+ setCheckedEmails(new Set(accounts.map(a => a.email)));
282
+ showMessage(`Selected ${accounts.length} accounts`, 2000);
283
+ break;
284
+ case "clear-selection":
285
+ setCheckedEmails(new Set());
286
+ showMessage("Selection cleared", 1500);
287
+ break;
288
+ case "quit":
289
+ exit();
290
+ break;
291
+ }
292
+ };
293
+
179
294
  // Export completion handler
180
295
  const handleExportComplete = (filePath: string) => {
181
296
  setActiveModal("none");
@@ -184,7 +299,6 @@ export function Dashboard({ pluginPath }: DashboardProps) {
184
299
 
185
300
  // Import completion handler
186
301
  const handleImportComplete = (importedAccounts: Account[], newCount: number, overwrittenCount: number) => {
187
- // Merge imported accounts with existing (overwrite mode)
188
302
  const file = safeReadPluginFile(resolvedPath);
189
303
  const merged = mergeAccounts(file, importedAccounts, "merge");
190
304
  writePluginAccountsFile(pluginPath, merged);
@@ -214,7 +328,7 @@ export function Dashboard({ pluginPath }: DashboardProps) {
214
328
 
215
329
  const added = merged.accounts.length - existingFile.accounts.length;
216
330
  showMessage(
217
- `Imported from AM: ${result.accounts.length} found, ${added} new. Total: ${merged.accounts.length}`,
331
+ `Imported from AM: ${result.accounts.length} found, ${added} new`,
218
332
  5000
219
333
  );
220
334
 
@@ -241,7 +355,7 @@ export function Dashboard({ pluginPath }: DashboardProps) {
241
355
  writePluginAccountsFile(pluginPath, file);
242
356
  showMessage(`Enabled ${count} accounts`, 3000);
243
357
  loadAccounts();
244
- setSelectMode(false);
358
+ setCheckedEmails(new Set());
245
359
  };
246
360
 
247
361
  const handleDisableSelected = () => {
@@ -264,7 +378,7 @@ export function Dashboard({ pluginPath }: DashboardProps) {
264
378
  writePluginAccountsFile(pluginPath, file);
265
379
  showMessage(`Disabled ${count} accounts`, 3000);
266
380
  loadAccounts();
267
- setSelectMode(false);
381
+ setCheckedEmails(new Set());
268
382
  };
269
383
 
270
384
  const handleDeleteSelected = () => {
@@ -282,75 +396,13 @@ export function Dashboard({ pluginPath }: DashboardProps) {
282
396
  writePluginAccountsFile(pluginPath, file);
283
397
  showMessage(`Deleted ${deletedCount} accounts`, 3000);
284
398
  loadAccounts();
285
- setSelectMode(false);
286
- };
287
-
288
- const handleSelectAll = () => {
289
- setCheckedEmails(new Set(accounts.map(a => a.email)));
290
- };
291
-
292
- const handleSelectNone = () => {
293
399
  setCheckedEmails(new Set());
294
400
  };
295
401
 
296
- const handleAction = (action: MenuAction) => {
297
- // Don't handle actions when modal is open
298
- if (activeModal !== "none") return;
299
-
300
- switch (action) {
301
- case "refresh":
302
- refresh();
303
- break;
304
- case "export":
305
- setActiveModal("export");
306
- break;
307
- case "import-file":
308
- setActiveModal("import");
309
- break;
310
- case "import-am":
311
- handleImportAM();
312
- break;
313
- case "toggle-select-mode":
314
- if (activeSection === "accounts") {
315
- setSelectMode(prev => !prev);
316
- setCheckedEmails(new Set());
317
- setSelectedIndex(0);
318
- } else {
319
- showMessage("Switch to Accounts section first (Tab)", 2000);
320
- }
321
- break;
322
- case "select-all":
323
- handleSelectAll();
324
- break;
325
- case "select-none":
326
- handleSelectNone();
327
- break;
328
- case "enable-selected":
329
- handleEnableSelected();
330
- break;
331
- case "disable-selected":
332
- handleDisableSelected();
333
- break;
334
- case "delete-selected":
335
- handleDeleteSelected();
336
- break;
337
- case "export-selected":
338
- if (checkedEmails.size === 0) {
339
- showMessage("No accounts selected", 2000);
340
- } else {
341
- setActiveModal("export-selected");
342
- }
343
- break;
344
- case "quit":
345
- exit();
346
- break;
347
- }
348
- };
349
-
350
402
  // Calculate stats
351
403
  const configSummary = opencodeInfo ? getConfigSummary(opencodeInfo) : null;
352
404
 
353
- // Get accounts to export (all or selected)
405
+ // Get accounts to export
354
406
  const getAccountsForExport = (): Account[] => {
355
407
  if (activeModal === "export-selected") {
356
408
  return accounts.filter(acc => checkedEmails.has(acc.email));
@@ -358,7 +410,11 @@ export function Dashboard({ pluginPath }: DashboardProps) {
358
410
  return accounts;
359
411
  };
360
412
 
361
- // If modal is open, render only the modal
413
+ // Settings nav helpers
414
+ const currentSettingsItem = settingsNavItems[settingsNavIndex];
415
+ const isOnSection = (id: string) => currentSettingsItem?.type === "section" && currentSettingsItem.id === id;
416
+
417
+ // Render modals
362
418
  if (activeModal === "export" || activeModal === "export-selected") {
363
419
  return (
364
420
  <Box flexDirection="column" padding={1}>
@@ -385,90 +441,116 @@ export function Dashboard({ pluginPath }: DashboardProps) {
385
441
 
386
442
  return (
387
443
  <Box flexDirection="column" padding={1}>
388
- <Header title="OpenCode Account Manager" subtitle="Dashboard" />
444
+ <Header title="OpenCode Account Manager" subtitle={activeTab === "dashboard" ? "Dashboard" : "Settings"} />
445
+
446
+ {/* Tab bar */}
447
+ <Box marginBottom={1}>
448
+ <Text
449
+ backgroundColor={activeTab === "dashboard" ? "cyan" : undefined}
450
+ color={activeTab === "dashboard" ? "black" : "gray"}
451
+ bold={activeTab === "dashboard"}
452
+ >
453
+ {" DASHBOARD "}
454
+ </Text>
455
+ <Text> </Text>
456
+ <Text
457
+ backgroundColor={activeTab === "settings" ? "cyan" : undefined}
458
+ color={activeTab === "settings" ? "black" : "gray"}
459
+ bold={activeTab === "settings"}
460
+ >
461
+ {" SETTINGS "}
462
+ </Text>
463
+ <Text dimColor> (Tab to switch)</Text>
464
+ </Box>
389
465
 
390
466
  {/* Global Stats */}
391
467
  <StatsRow
392
468
  stats={[
393
- { label: "Providers", value: configSummary?.providers || 0, color: "cyan" },
394
- { label: "Models", value: configSummary?.models || 0, color: "yellow" },
395
- { label: "MCP On", value: configSummary?.mcpEnabled || 0, color: "green" },
396
- { label: "MCP Off", value: configSummary?.mcpDisabled || 0, color: "red" },
397
469
  { label: "Accounts", value: summary.total, color: "white" },
398
470
  { label: "Available", value: summary.available, color: "green" },
399
471
  { label: "Limited", value: summary.limited, color: "yellow" },
472
+ { label: "Providers", value: configSummary?.providers || 0, color: "cyan" },
473
+ { label: "MCP", value: configSummary?.mcpEnabled || 0, color: "magenta" },
400
474
  ]}
401
475
  />
402
476
 
403
- {/* Tab indicator */}
477
+ {/* Help bar */}
404
478
  <Box marginY={1}>
405
- <Text dimColor>Sections: </Text>
406
- <Text color={activeSection === "providers" ? "cyan" : "gray"} bold={activeSection === "providers"}>
407
- [1] Providers
408
- </Text>
409
- <Text> </Text>
410
- <Text color={activeSection === "accounts" ? "cyan" : "gray"} bold={activeSection === "accounts"}>
411
- [2] Accounts
412
- </Text>
413
- <Text> </Text>
414
- <Text color={activeSection === "mcp" ? "cyan" : "gray"} bold={activeSection === "mcp"}>
415
- [3] MCP
416
- </Text>
417
- <Text dimColor> (←→ or Tab to switch, ↑↓ in Accounts)</Text>
479
+ <Text dimColor>↑↓ navigate • Space select • </Text>
480
+ <Text color="cyan" bold>P</Text>
481
+ <Text dimColor> actions • </Text>
482
+ <Text color="cyan" bold>Tab</Text>
483
+ <Text dimColor> switch • Q quit</Text>
484
+ {checkedEmails.size > 0 && (
485
+ <Text color="yellow"> • {checkedEmails.size} selected</Text>
486
+ )}
418
487
  </Box>
419
488
 
420
- {/* Providers Section */}
421
- <SectionBox
422
- title="PROVIDERS"
423
- borderColor={activeSection === "providers" ? "cyan" : "gray"}
424
- collapsed={activeSection !== "providers"}
425
- >
426
- {opencodeInfo && <ProviderList providers={opencodeInfo.providers} />}
427
- </SectionBox>
428
-
429
- {/* Plugin Accounts Section */}
430
- <SectionBox
431
- title={`PLUGIN ACCOUNTS (${opencodeInfo?.plugins[0]?.name || "antigravity-auth"})`}
432
- borderColor={activeSection === "accounts" ? (selectMode ? "yellow" : "cyan") : "gray"}
433
- collapsed={activeSection !== "accounts"}
434
- >
435
- <AccountList
436
- accounts={accounts}
437
- selectedIndex={selectMode ? selectedIndex : -1}
438
- checkedEmails={checkedEmails}
439
- showCheckbox={selectMode}
440
- />
441
- </SectionBox>
442
-
443
- {/* MCP Servers Section */}
444
- <SectionBox
445
- title="MCP SERVERS"
446
- borderColor={activeSection === "mcp" ? "cyan" : "gray"}
447
- collapsed={activeSection !== "mcp"}
448
- >
449
- {opencodeInfo && <McpServerList servers={opencodeInfo.mcpServers} />}
450
- </SectionBox>
489
+ {/* Tab content */}
490
+ {activeTab === "dashboard" ? (
491
+ // Dashboard Tab - Rate limits view like Antigravity Manager
492
+ <Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1}>
493
+ <DashboardView
494
+ accounts={accounts}
495
+ selectedIndex={dashboardIndex}
496
+ />
497
+ </Box>
498
+ ) : (
499
+ // Settings Tab - Original sections view
500
+ <>
501
+ <SectionBox
502
+ title="PROVIDERS"
503
+ borderColor={isOnSection("providers") ? "cyan" : (expandedSection === "providers" ? "white" : "gray")}
504
+ collapsed={expandedSection !== "providers"}
505
+ >
506
+ {opencodeInfo && <ProviderList providers={opencodeInfo.providers} />}
507
+ </SectionBox>
508
+
509
+ <SectionBox
510
+ title={`ACCOUNTS (${opencodeInfo?.plugins[0]?.name || "antigravity-auth"})`}
511
+ borderColor={isOnSection("accounts") || (currentSettingsItem?.type === "account") ? "cyan" : (expandedSection === "accounts" ? "white" : "gray")}
512
+ collapsed={expandedSection !== "accounts"}
513
+ >
514
+ <AccountList
515
+ accounts={accounts}
516
+ selectedIndex={currentSettingsItem?.type === "account" ? (currentSettingsItem.index ?? -1) : -1}
517
+ checkedEmails={checkedEmails}
518
+ showCheckbox={true}
519
+ />
520
+ </SectionBox>
521
+
522
+ <SectionBox
523
+ title="MCP SERVERS"
524
+ borderColor={isOnSection("mcp") ? "cyan" : (expandedSection === "mcp" ? "white" : "gray")}
525
+ collapsed={expandedSection !== "mcp"}
526
+ >
527
+ {opencodeInfo && <McpServerList servers={opencodeInfo.mcpServers} />}
528
+ </SectionBox>
529
+ </>
530
+ )}
451
531
 
452
532
  {/* Config path */}
453
533
  <Box marginTop={1}>
454
534
  <Text dimColor>Config: {opencodeInfo?.configPath || "N/A"}</Text>
455
535
  </Box>
456
536
 
457
- {/* Menu */}
458
- <Box marginTop={1}>
459
- <MenuBar
460
- onSelect={handleAction}
461
- selectMode={selectMode}
462
- selectedCount={checkedEmails.size}
463
- />
464
- </Box>
465
-
466
537
  {/* Message */}
467
538
  {message && (
468
539
  <Box marginTop={1}>
469
540
  <Text color="green">→ {message}</Text>
470
541
  </Box>
471
542
  )}
543
+
544
+ {/* Action Palette overlay */}
545
+ {activeModal === "palette" && (
546
+ <Box position="absolute" marginTop={3} marginLeft={10}>
547
+ <ActionPalette
548
+ actions={paletteActions}
549
+ onSelect={handlePaletteAction}
550
+ onClose={() => setActiveModal("none")}
551
+ />
552
+ </Box>
553
+ )}
472
554
  </Box>
473
555
  );
474
556
  }