opencode-account-manager 0.4.4 → 0.5.0

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.
@@ -11,4 +11,5 @@ export { PasswordInput } from "./PasswordInput";
11
11
  export { FileBrowser } from "./FileBrowser";
12
12
  export { ExportModal } from "./ExportModal";
13
13
  export { ImportModal } from "./ImportModal";
14
+ export { ActionPalette, PaletteAction } from "./ActionPalette";
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tui/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tui/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ImportModal = exports.ExportModal = exports.FileBrowser = exports.PasswordInput = exports.SectionBox = exports.McpServerList = exports.ProviderList = exports.MenuBar = exports.AccountList = exports.AccountRow = exports.StatsRow = exports.StatCard = exports.StatusBadge = exports.Header = exports.Box = void 0;
3
+ exports.ActionPalette = exports.ImportModal = exports.ExportModal = exports.FileBrowser = exports.PasswordInput = exports.SectionBox = exports.McpServerList = exports.ProviderList = exports.MenuBar = exports.AccountList = exports.AccountRow = exports.StatsRow = exports.StatCard = exports.StatusBadge = exports.Header = exports.Box = void 0;
4
4
  var Box_1 = require("./Box");
5
5
  Object.defineProperty(exports, "Box", { enumerable: true, get: function () { return Box_1.Box; } });
6
6
  var Header_1 = require("./Header");
@@ -29,4 +29,6 @@ var ExportModal_1 = require("./ExportModal");
29
29
  Object.defineProperty(exports, "ExportModal", { enumerable: true, get: function () { return ExportModal_1.ExportModal; } });
30
30
  var ImportModal_1 = require("./ImportModal");
31
31
  Object.defineProperty(exports, "ImportModal", { enumerable: true, get: function () { return ImportModal_1.ImportModal; } });
32
+ var ActionPalette_1 = require("./ActionPalette");
33
+ Object.defineProperty(exports, "ActionPalette", { enumerable: true, get: function () { return ActionPalette_1.ActionPalette; } });
32
34
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tui/components/index.ts"],"names":[],"mappings":";;;AAAA,6BAA4B;AAAnB,0FAAA,GAAG,OAAA;AACZ,mCAAkC;AAAzB,gGAAA,MAAM,OAAA;AACf,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,uCAAgD;AAAvC,oGAAA,QAAQ,OAAA;AAAE,oGAAA,QAAQ,OAAA;AAC3B,6CAAwD;AAA/C,yGAAA,UAAU,OAAA;AAAE,0GAAA,WAAW,OAAA;AAChC,+BAA6C;AAApC,+FAAA,OAAO,OAAA;AAChB,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AACrB,iDAAgD;AAAvC,8GAAA,aAAa,OAAA;AACtB,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,iDAAgD;AAAvC,8GAAA,aAAa,OAAA;AACtB,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,6CAA4C;AAAnC,0GAAA,WAAW,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tui/components/index.ts"],"names":[],"mappings":";;;AAAA,6BAA4B;AAAnB,0FAAA,GAAG,OAAA;AACZ,mCAAkC;AAAzB,gGAAA,MAAM,OAAA;AACf,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,uCAAgD;AAAvC,oGAAA,QAAQ,OAAA;AAAE,oGAAA,QAAQ,OAAA;AAC3B,6CAAwD;AAA/C,yGAAA,UAAU,OAAA;AAAE,0GAAA,WAAW,OAAA;AAChC,+BAA6C;AAApC,+FAAA,OAAO,OAAA;AAChB,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AACrB,iDAAgD;AAAvC,8GAAA,aAAa,OAAA;AACtB,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,iDAAgD;AAAvC,8GAAA,aAAa,OAAA;AACtB,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,iDAA+D;AAAtD,8GAAA,aAAa,OAAA"}
package/docs/ROADMAP.md CHANGED
@@ -48,12 +48,30 @@
48
48
  - [x] **Tab Navigation** - Fixed Tab key to cycle through sections
49
49
  - [x] **UI Indicator** - Show `[1] Providers [2] Accounts [3] MCP` in header
50
50
 
51
- ### v0.4.4 - Arrow Key Navigation (Current)
51
+ ### v0.4.4 - Arrow Key Navigation
52
52
  - [x] **←→ Arrow Keys** - Switch between sections (Providers ↔ Accounts ↔ MCP)
53
53
  - [x] **↑↓ Arrow Keys** - Navigate account list, auto-enters select mode
54
54
  - [x] **Space Key** - Toggle selection in select mode
55
55
  - [x] **Updated Help Text** - Show arrow key hints in UI
56
56
 
57
+ ### v0.5.0 - OpenCode-style UX (Current)
58
+ - [x] **Action Palette** - Press P to open command palette (like OpenCode Ctrl+P)
59
+ - [x] **Unified Navigation** - ↑↓ to navigate everything, Enter to expand/select
60
+ - [x] **Simplified Controls** - No more complex keyboard shortcuts to remember
61
+ - [x] **Inline Selection** - Space to toggle, selection count shown in help bar
62
+ - [x] **Removed MenuBar** - Clean minimal interface
63
+
64
+ **New Controls:**
65
+ | Key | Action |
66
+ |-----|--------|
67
+ | ↑↓ | Navigate sections and accounts |
68
+ | Enter | Expand section / Toggle account selection |
69
+ | Space | Toggle account selection |
70
+ | P | Open Action Palette |
71
+ | Q | Quit |
72
+ | R | Refresh |
73
+ | Esc | Clear selection |
74
+
57
75
  ---
58
76
 
59
77
  ## Future Ideas (Backlog)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-account-manager",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "TUI Dashboard for OpenCode - View providers, MCP servers, and plugin accounts",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -41,7 +41,7 @@ const program = new Command();
41
41
  program
42
42
  .name("ocam")
43
43
  .description("OpenCode Account Manager - TUI dashboard and CLI for managing accounts")
44
- .version("0.4.4");
44
+ .version("0.5.0");
45
45
 
46
46
  // Default command - show dashboard
47
47
  program
@@ -4,13 +4,13 @@ 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
14
  } from "./components";
15
15
  import {
16
16
  readPluginAccountsFile,
@@ -32,8 +32,12 @@ interface DashboardProps {
32
32
  pluginPath?: string;
33
33
  }
34
34
 
35
- type ActiveSection = "providers" | "accounts" | "mcp";
36
- type ModalType = "none" | "export" | "import" | "export-selected";
35
+ type ModalType = "none" | "export" | "import" | "export-selected" | "palette";
36
+
37
+ // Navigation items
38
+ type NavItem =
39
+ | { type: "section"; id: "providers" | "accounts" | "mcp"; label: string }
40
+ | { type: "account"; index: number; email: string };
37
41
 
38
42
  function safeReadPluginFile(pluginPath: string): PluginAccountsFile {
39
43
  try {
@@ -55,10 +59,9 @@ export function Dashboard({ pluginPath }: DashboardProps) {
55
59
  const [summary, setSummary] = useState({ total: 0, available: 0, limited: 0 });
56
60
  const [message, setMessage] = useState<string | null>(null);
57
61
 
58
- // UI state
59
- const [activeSection, setActiveSection] = useState<ActiveSection>("providers");
60
- const [selectMode, setSelectMode] = useState(false);
61
- const [selectedIndex, setSelectedIndex] = useState(0);
62
+ // UI state - simplified
63
+ const [expandedSection, setExpandedSection] = useState<"providers" | "accounts" | "mcp">("accounts");
64
+ const [navIndex, setNavIndex] = useState(0); // Current navigation index
62
65
  const [checkedEmails, setCheckedEmails] = useState<Set<string>>(new Set());
63
66
 
64
67
  // Modal state
@@ -79,7 +82,6 @@ export function Dashboard({ pluginPath }: DashboardProps) {
79
82
  setAccounts(file.accounts);
80
83
  setSummary(summarizeAccounts(file.accounts));
81
84
  setCheckedEmails(new Set());
82
- setSelectedIndex(0);
83
85
  };
84
86
 
85
87
  const refresh = () => {
@@ -93,89 +95,175 @@ export function Dashboard({ pluginPath }: DashboardProps) {
93
95
  loadAccounts();
94
96
  }, []);
95
97
 
96
- // Keyboard navigation (only when no modal is open)
98
+ // Build navigation items list
99
+ const buildNavItems = (): NavItem[] => {
100
+ const items: NavItem[] = [
101
+ { type: "section", id: "providers", label: "PROVIDERS" },
102
+ { type: "section", id: "accounts", label: "ACCOUNTS" },
103
+ ];
104
+
105
+ // If accounts section is expanded, add account items
106
+ if (expandedSection === "accounts") {
107
+ accounts.forEach((acc, index) => {
108
+ items.push({ type: "account", index, email: acc.email });
109
+ });
110
+ }
111
+
112
+ items.push({ type: "section", id: "mcp", label: "MCP SERVERS" });
113
+
114
+ return items;
115
+ };
116
+
117
+ const navItems = buildNavItems();
118
+
119
+ // Palette actions
120
+ const paletteActions: PaletteAction[] = [
121
+ { id: "refresh", label: "Refresh", shortcut: "R" },
122
+ { id: "export", label: "Export All Accounts", shortcut: "E" },
123
+ { id: "import", label: "Import from File", shortcut: "I" },
124
+ { id: "import-am", label: "Import from Antigravity Manager", shortcut: "A" },
125
+ ...(checkedEmails.size > 0 ? [
126
+ { id: "export-selected", label: `Export Selected (${checkedEmails.size})`, shortcut: "X" },
127
+ { id: "enable-selected", label: `Enable Selected (${checkedEmails.size})` },
128
+ { id: "disable-selected", label: `Disable Selected (${checkedEmails.size})` },
129
+ { id: "delete-selected", label: `Delete Selected (${checkedEmails.size})`, shortcut: "Del" },
130
+ { id: "clear-selection", label: "Clear Selection", shortcut: "N" },
131
+ ] : []),
132
+ { id: "select-all", label: "Select All Accounts", shortcut: "Ctrl+A" },
133
+ { id: "quit", label: "Quit", shortcut: "Q" },
134
+ ];
135
+
136
+ // Keyboard navigation
97
137
  useInput((input, key) => {
138
+ // Handle palette separately
139
+ if (activeModal === "palette") return;
98
140
  if (activeModal !== "none") return;
99
141
 
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";
106
- });
107
- if (!key.rightArrow || activeSection !== "accounts") {
108
- setSelectMode(false);
109
- }
142
+ // Open palette with Ctrl+P or P
143
+ if ((key.ctrl && input === "p") || input === "p" || input === "P") {
144
+ setActiveModal("palette");
110
145
  return;
111
146
  }
112
147
 
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
- }
148
+ // Quick shortcuts
149
+ if (input === "q" || input === "Q") {
150
+ exit();
122
151
  return;
123
152
  }
124
-
125
- // Number keys for section switching
126
- if (input === "1") {
127
- setActiveSection("providers");
128
- setSelectMode(false);
153
+ if (input === "r" || input === "R") {
154
+ refresh();
129
155
  return;
130
156
  }
131
- if (input === "2") {
132
- setActiveSection("accounts");
157
+
158
+ // Navigate up
159
+ if (key.upArrow) {
160
+ setNavIndex(prev => Math.max(0, prev - 1));
133
161
  return;
134
162
  }
135
- if (input === "3") {
136
- setActiveSection("mcp");
137
- setSelectMode(false);
163
+
164
+ // Navigate down
165
+ if (key.downArrow) {
166
+ setNavIndex(prev => Math.min(navItems.length - 1, prev + 1));
138
167
  return;
139
168
  }
140
169
 
141
- // Up/Down arrows for list navigation in accounts section
142
- if (activeSection === "accounts") {
143
- if (key.upArrow) {
144
- if (!selectMode) {
145
- setSelectMode(true);
146
- setSelectedIndex(0);
147
- } else {
148
- setSelectedIndex(prev => Math.max(0, prev - 1));
149
- }
150
- return;
170
+ // Enter to expand/collapse section or toggle account
171
+ if (key.return) {
172
+ const currentItem = navItems[navIndex];
173
+ if (currentItem?.type === "section") {
174
+ setExpandedSection(currentItem.id);
175
+ // Reset nav index to stay on section header
176
+ } else if (currentItem?.type === "account") {
177
+ // Toggle selection
178
+ const email = currentItem.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
+ });
151
188
  }
152
- if (key.downArrow) {
153
- if (!selectMode) {
154
- setSelectMode(true);
155
- setSelectedIndex(0);
156
- } else {
157
- setSelectedIndex(prev => Math.min(accounts.length - 1, prev + 1));
158
- }
159
- return;
189
+ return;
190
+ }
191
+
192
+ // Space to toggle selection
193
+ if (input === " ") {
194
+ const currentItem = navItems[navIndex];
195
+ if (currentItem?.type === "account") {
196
+ const email = currentItem.email;
197
+ setCheckedEmails(prev => {
198
+ const next = new Set(prev);
199
+ if (next.has(email)) {
200
+ next.delete(email);
201
+ } else {
202
+ next.add(email);
203
+ }
204
+ return next;
205
+ });
160
206
  }
161
- // Space to toggle selection in select mode
162
- if (selectMode && input === " ") {
163
- const email = accounts[selectedIndex]?.email;
164
- if (email) {
165
- setCheckedEmails(prev => {
166
- const next = new Set(prev);
167
- if (next.has(email)) {
168
- next.delete(email);
169
- } else {
170
- next.add(email);
171
- }
172
- return next;
173
- });
174
- }
207
+ return;
208
+ }
209
+
210
+ // Escape to clear selection
211
+ if (key.escape) {
212
+ if (checkedEmails.size > 0) {
213
+ setCheckedEmails(new Set());
214
+ showMessage("Selection cleared", 1500);
175
215
  }
216
+ return;
176
217
  }
177
218
  });
178
219
 
220
+ // Handle palette action
221
+ const handlePaletteAction = (actionId: string) => {
222
+ setActiveModal("none");
223
+
224
+ switch (actionId) {
225
+ case "refresh":
226
+ refresh();
227
+ break;
228
+ case "export":
229
+ setActiveModal("export");
230
+ break;
231
+ case "import":
232
+ setActiveModal("import");
233
+ break;
234
+ case "import-am":
235
+ handleImportAM();
236
+ break;
237
+ case "export-selected":
238
+ if (checkedEmails.size > 0) {
239
+ setActiveModal("export-selected");
240
+ } else {
241
+ showMessage("No accounts selected", 2000);
242
+ }
243
+ break;
244
+ case "enable-selected":
245
+ handleEnableSelected();
246
+ break;
247
+ case "disable-selected":
248
+ handleDisableSelected();
249
+ break;
250
+ case "delete-selected":
251
+ handleDeleteSelected();
252
+ break;
253
+ case "select-all":
254
+ setCheckedEmails(new Set(accounts.map(a => a.email)));
255
+ showMessage(`Selected ${accounts.length} accounts`, 2000);
256
+ break;
257
+ case "clear-selection":
258
+ setCheckedEmails(new Set());
259
+ showMessage("Selection cleared", 1500);
260
+ break;
261
+ case "quit":
262
+ exit();
263
+ break;
264
+ }
265
+ };
266
+
179
267
  // Export completion handler
180
268
  const handleExportComplete = (filePath: string) => {
181
269
  setActiveModal("none");
@@ -184,7 +272,6 @@ export function Dashboard({ pluginPath }: DashboardProps) {
184
272
 
185
273
  // Import completion handler
186
274
  const handleImportComplete = (importedAccounts: Account[], newCount: number, overwrittenCount: number) => {
187
- // Merge imported accounts with existing (overwrite mode)
188
275
  const file = safeReadPluginFile(resolvedPath);
189
276
  const merged = mergeAccounts(file, importedAccounts, "merge");
190
277
  writePluginAccountsFile(pluginPath, merged);
@@ -214,7 +301,7 @@ export function Dashboard({ pluginPath }: DashboardProps) {
214
301
 
215
302
  const added = merged.accounts.length - existingFile.accounts.length;
216
303
  showMessage(
217
- `Imported from AM: ${result.accounts.length} found, ${added} new. Total: ${merged.accounts.length}`,
304
+ `Imported from AM: ${result.accounts.length} found, ${added} new`,
218
305
  5000
219
306
  );
220
307
 
@@ -241,7 +328,7 @@ export function Dashboard({ pluginPath }: DashboardProps) {
241
328
  writePluginAccountsFile(pluginPath, file);
242
329
  showMessage(`Enabled ${count} accounts`, 3000);
243
330
  loadAccounts();
244
- setSelectMode(false);
331
+ setCheckedEmails(new Set());
245
332
  };
246
333
 
247
334
  const handleDisableSelected = () => {
@@ -264,7 +351,7 @@ export function Dashboard({ pluginPath }: DashboardProps) {
264
351
  writePluginAccountsFile(pluginPath, file);
265
352
  showMessage(`Disabled ${count} accounts`, 3000);
266
353
  loadAccounts();
267
- setSelectMode(false);
354
+ setCheckedEmails(new Set());
268
355
  };
269
356
 
270
357
  const handleDeleteSelected = () => {
@@ -282,75 +369,13 @@ export function Dashboard({ pluginPath }: DashboardProps) {
282
369
  writePluginAccountsFile(pluginPath, file);
283
370
  showMessage(`Deleted ${deletedCount} accounts`, 3000);
284
371
  loadAccounts();
285
- setSelectMode(false);
286
- };
287
-
288
- const handleSelectAll = () => {
289
- setCheckedEmails(new Set(accounts.map(a => a.email)));
290
- };
291
-
292
- const handleSelectNone = () => {
293
372
  setCheckedEmails(new Set());
294
373
  };
295
374
 
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
375
  // Calculate stats
351
376
  const configSummary = opencodeInfo ? getConfigSummary(opencodeInfo) : null;
352
377
 
353
- // Get accounts to export (all or selected)
378
+ // Get accounts to export
354
379
  const getAccountsForExport = (): Account[] => {
355
380
  if (activeModal === "export-selected") {
356
381
  return accounts.filter(acc => checkedEmails.has(acc.email));
@@ -358,7 +383,15 @@ export function Dashboard({ pluginPath }: DashboardProps) {
358
383
  return accounts;
359
384
  };
360
385
 
361
- // If modal is open, render only the modal
386
+ // Find current nav item for highlighting
387
+ const currentNavItem = navItems[navIndex];
388
+ const isOnSection = (id: string) => currentNavItem?.type === "section" && currentNavItem.id === id;
389
+ const getAccountNavState = (index: number) => {
390
+ const item = navItems[navIndex];
391
+ return item?.type === "account" && item.index === index;
392
+ };
393
+
394
+ // Render modals
362
395
  if (activeModal === "export" || activeModal === "export-selected") {
363
396
  return (
364
397
  <Box flexDirection="column" padding={1}>
@@ -392,59 +425,52 @@ export function Dashboard({ pluginPath }: DashboardProps) {
392
425
  stats={[
393
426
  { label: "Providers", value: configSummary?.providers || 0, color: "cyan" },
394
427
  { 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" },
428
+ { label: "MCP", value: configSummary?.mcpEnabled || 0, color: "green" },
397
429
  { label: "Accounts", value: summary.total, color: "white" },
398
430
  { label: "Available", value: summary.available, color: "green" },
399
431
  { label: "Limited", value: summary.limited, color: "yellow" },
400
432
  ]}
401
433
  />
402
434
 
403
- {/* Tab indicator */}
435
+ {/* Help bar */}
404
436
  <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>
437
+ <Text dimColor>↑↓ navigate • Enter expand/select • Space toggle • </Text>
438
+ <Text color="cyan" bold>P</Text>
439
+ <Text dimColor> actions • </Text>
440
+ <Text dimColor>Q quit</Text>
441
+ {checkedEmails.size > 0 && (
442
+ <Text color="yellow"> {checkedEmails.size} selected</Text>
443
+ )}
418
444
  </Box>
419
445
 
420
446
  {/* Providers Section */}
421
447
  <SectionBox
422
448
  title="PROVIDERS"
423
- borderColor={activeSection === "providers" ? "cyan" : "gray"}
424
- collapsed={activeSection !== "providers"}
449
+ borderColor={isOnSection("providers") ? "cyan" : (expandedSection === "providers" ? "white" : "gray")}
450
+ collapsed={expandedSection !== "providers"}
425
451
  >
426
452
  {opencodeInfo && <ProviderList providers={opencodeInfo.providers} />}
427
453
  </SectionBox>
428
454
 
429
455
  {/* Plugin Accounts Section */}
430
456
  <SectionBox
431
- title={`PLUGIN ACCOUNTS (${opencodeInfo?.plugins[0]?.name || "antigravity-auth"})`}
432
- borderColor={activeSection === "accounts" ? (selectMode ? "yellow" : "cyan") : "gray"}
433
- collapsed={activeSection !== "accounts"}
457
+ title={`ACCOUNTS (${opencodeInfo?.plugins[0]?.name || "antigravity-auth"})`}
458
+ borderColor={isOnSection("accounts") || (currentNavItem?.type === "account") ? "cyan" : (expandedSection === "accounts" ? "white" : "gray")}
459
+ collapsed={expandedSection !== "accounts"}
434
460
  >
435
461
  <AccountList
436
462
  accounts={accounts}
437
- selectedIndex={selectMode ? selectedIndex : -1}
463
+ selectedIndex={currentNavItem?.type === "account" ? currentNavItem.index : -1}
438
464
  checkedEmails={checkedEmails}
439
- showCheckbox={selectMode}
465
+ showCheckbox={true}
440
466
  />
441
467
  </SectionBox>
442
468
 
443
469
  {/* MCP Servers Section */}
444
470
  <SectionBox
445
471
  title="MCP SERVERS"
446
- borderColor={activeSection === "mcp" ? "cyan" : "gray"}
447
- collapsed={activeSection !== "mcp"}
472
+ borderColor={isOnSection("mcp") ? "cyan" : (expandedSection === "mcp" ? "white" : "gray")}
473
+ collapsed={expandedSection !== "mcp"}
448
474
  >
449
475
  {opencodeInfo && <McpServerList servers={opencodeInfo.mcpServers} />}
450
476
  </SectionBox>
@@ -454,21 +480,27 @@ export function Dashboard({ pluginPath }: DashboardProps) {
454
480
  <Text dimColor>Config: {opencodeInfo?.configPath || "N/A"}</Text>
455
481
  </Box>
456
482
 
457
- {/* Menu */}
458
- <Box marginTop={1}>
459
- <MenuBar
460
- onSelect={handleAction}
461
- selectMode={selectMode}
462
- selectedCount={checkedEmails.size}
463
- />
464
- </Box>
465
-
466
483
  {/* Message */}
467
484
  {message && (
468
485
  <Box marginTop={1}>
469
486
  <Text color="green">→ {message}</Text>
470
487
  </Box>
471
488
  )}
489
+
490
+ {/* Action Palette overlay */}
491
+ {activeModal === "palette" && (
492
+ <Box
493
+ position="absolute"
494
+ marginTop={3}
495
+ marginLeft={10}
496
+ >
497
+ <ActionPalette
498
+ actions={paletteActions}
499
+ onSelect={handlePaletteAction}
500
+ onClose={() => setActiveModal("none")}
501
+ />
502
+ </Box>
503
+ )}
472
504
  </Box>
473
505
  );
474
506
  }