opencode-account-manager 0.6.4 → 0.6.5

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.
Files changed (86) hide show
  1. package/README.md +235 -216
  2. package/README_VI.md +235 -216
  3. package/dist/cli.js +83 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/config-store.d.ts +12 -0
  6. package/dist/core/config-store.d.ts.map +1 -1
  7. package/dist/core/config-store.js +98 -0
  8. package/dist/core/config-store.js.map +1 -1
  9. package/dist/core/health-log.d.ts +9 -0
  10. package/dist/core/health-log.d.ts.map +1 -0
  11. package/dist/core/health-log.js +154 -0
  12. package/dist/core/health-log.js.map +1 -0
  13. package/dist/core/health-oauth.d.ts +5 -0
  14. package/dist/core/health-oauth.d.ts.map +1 -0
  15. package/dist/core/health-oauth.js +147 -0
  16. package/dist/core/health-oauth.js.map +1 -0
  17. package/dist/core/health-orchestrator.d.ts +32 -0
  18. package/dist/core/health-orchestrator.d.ts.map +1 -0
  19. package/dist/core/health-orchestrator.js +148 -0
  20. package/dist/core/health-orchestrator.js.map +1 -0
  21. package/dist/core/health-utils.d.ts +15 -0
  22. package/dist/core/health-utils.d.ts.map +1 -0
  23. package/dist/core/health-utils.js +60 -0
  24. package/dist/core/health-utils.js.map +1 -0
  25. package/dist/core/paths.d.ts +1 -0
  26. package/dist/core/paths.d.ts.map +1 -1
  27. package/dist/core/paths.js +4 -0
  28. package/dist/core/paths.js.map +1 -1
  29. package/dist/core/types.d.ts +26 -0
  30. package/dist/core/types.d.ts.map +1 -1
  31. package/dist/tui/Dashboard.d.ts.map +1 -1
  32. package/dist/tui/Dashboard.js +69 -2
  33. package/dist/tui/Dashboard.js.map +1 -1
  34. package/dist/tui/components/AccountList.d.ts +5 -3
  35. package/dist/tui/components/AccountList.d.ts.map +1 -1
  36. package/dist/tui/components/AccountList.js +9 -3
  37. package/dist/tui/components/AccountList.js.map +1 -1
  38. package/dist/tui/components/DashboardView.d.ts +3 -2
  39. package/dist/tui/components/DashboardView.d.ts.map +1 -1
  40. package/dist/tui/components/DashboardView.js +50 -4
  41. package/dist/tui/components/DashboardView.js.map +1 -1
  42. package/dist/tui/components/HealthBadge.d.ts +9 -0
  43. package/dist/tui/components/HealthBadge.d.ts.map +1 -0
  44. package/dist/tui/components/HealthBadge.js +56 -0
  45. package/dist/tui/components/HealthBadge.js.map +1 -0
  46. package/dist/tui/components/StatusBadge.d.ts +2 -1
  47. package/dist/tui/components/StatusBadge.d.ts.map +1 -1
  48. package/dist/tui/components/StatusBadge.js +30 -2
  49. package/dist/tui/components/StatusBadge.js.map +1 -1
  50. package/dist/tui/components/index.d.ts +1 -0
  51. package/dist/tui/components/index.d.ts.map +1 -1
  52. package/dist/tui/components/index.js +3 -1
  53. package/dist/tui/components/index.js.map +1 -1
  54. package/docs/BLUEPRINT.md +476 -476
  55. package/docs/ROADMAP.md +125 -107
  56. package/package.json +36 -36
  57. package/src/cli.ts +139 -38
  58. package/src/core/config-store.ts +278 -171
  59. package/src/core/crypto.ts +162 -162
  60. package/src/core/health-log.ts +173 -0
  61. package/src/core/health-oauth.ts +190 -0
  62. package/src/core/health-orchestrator.ts +224 -0
  63. package/src/core/importers/amExport.ts +177 -177
  64. package/src/core/opencode-config.ts +217 -217
  65. package/src/core/paths.ts +10 -6
  66. package/src/core/types.ts +193 -147
  67. package/src/tui/Dashboard.tsx +557 -478
  68. package/src/tui/components/AccountList.tsx +122 -104
  69. package/src/tui/components/ActionPalette.tsx +117 -117
  70. package/src/tui/components/Box.tsx +7 -7
  71. package/src/tui/components/DashboardView.tsx +285 -230
  72. package/src/tui/components/ExportModal.tsx +255 -255
  73. package/src/tui/components/FileBrowser.tsx +393 -393
  74. package/src/tui/components/Header.tsx +26 -26
  75. package/src/tui/components/HealthBadge.tsx +64 -0
  76. package/src/tui/components/ImportModal.tsx +334 -334
  77. package/src/tui/components/McpServerList.tsx +67 -67
  78. package/src/tui/components/Menu.tsx +61 -61
  79. package/src/tui/components/PasswordInput.tsx +159 -159
  80. package/src/tui/components/ProviderList.tsx +59 -59
  81. package/src/tui/components/SectionBox.tsx +35 -35
  82. package/src/tui/components/StatsRow.tsx +33 -33
  83. package/src/tui/components/StatusBadge.tsx +36 -3
  84. package/src/tui/components/index.ts +15 -14
  85. package/test-minimal.js +26 -26
  86. package/test-with-accounts.js +58 -58
@@ -1,393 +1,393 @@
1
- import React, { useState, useEffect } from "react";
2
- import { Box, Text, useInput } from "ink";
3
- import * as fs from "fs";
4
- import * as path from "path";
5
- import { getQuickLocations, QuickLocation } from "../../core/config-store";
6
-
7
- interface FileBrowserProps {
8
- mode: "folder" | "file";
9
- initialPath?: string;
10
- extensions?: string[]; // e.g., [".ocam", ".json"]
11
- title?: string;
12
- onSelect: (selectedPath: string) => void;
13
- onCancel: () => void;
14
- }
15
-
16
- interface FileItem {
17
- name: string;
18
- isDirectory: boolean;
19
- path: string;
20
- size?: number;
21
- mtime?: Date;
22
- }
23
-
24
- type ViewMode = "quick" | "browse" | "input";
25
-
26
- export function FileBrowser({
27
- mode,
28
- initialPath,
29
- extensions = [],
30
- title,
31
- onSelect,
32
- onCancel,
33
- }: FileBrowserProps) {
34
- const [currentPath, setCurrentPath] = useState(initialPath || process.cwd());
35
- const [items, setItems] = useState<FileItem[]>([]);
36
- const [selectedIndex, setSelectedIndex] = useState(0);
37
- const [viewMode, setViewMode] = useState<ViewMode>("quick");
38
- const [inputPath, setInputPath] = useState("");
39
- const [error, setError] = useState<string | null>(null);
40
- const [quickLocations, setQuickLocations] = useState<QuickLocation[]>([]);
41
-
42
- // Load quick locations on mount
43
- useEffect(() => {
44
- setQuickLocations(getQuickLocations());
45
- }, []);
46
-
47
- // Load directory contents when path changes
48
- useEffect(() => {
49
- if (viewMode !== "browse") return;
50
-
51
- try {
52
- if (!fs.existsSync(currentPath)) {
53
- setError(`Path not found: ${currentPath}`);
54
- setItems([]);
55
- return;
56
- }
57
-
58
- const stat = fs.statSync(currentPath);
59
- if (!stat.isDirectory()) {
60
- setError("Not a directory");
61
- setItems([]);
62
- return;
63
- }
64
-
65
- const entries = fs.readdirSync(currentPath, { withFileTypes: true });
66
- const fileItems: FileItem[] = [];
67
-
68
- // Add parent directory
69
- const parentPath = path.dirname(currentPath);
70
- if (parentPath !== currentPath) {
71
- fileItems.push({
72
- name: "..",
73
- isDirectory: true,
74
- path: parentPath,
75
- });
76
- }
77
-
78
- // Add directories and files
79
- for (const entry of entries) {
80
- const fullPath = path.join(currentPath, entry.name);
81
- const isDir = entry.isDirectory();
82
-
83
- // Skip hidden files/folders (starting with .)
84
- if (entry.name.startsWith(".")) continue;
85
-
86
- // In file mode, filter by extension
87
- if (mode === "file" && !isDir) {
88
- if (extensions.length > 0) {
89
- const ext = path.extname(entry.name).toLowerCase();
90
- if (!extensions.includes(ext)) continue;
91
- }
92
- }
93
-
94
- // In folder mode, only show directories
95
- if (mode === "folder" && !isDir) continue;
96
-
97
- try {
98
- const stat = fs.statSync(fullPath);
99
- fileItems.push({
100
- name: entry.name,
101
- isDirectory: isDir,
102
- path: fullPath,
103
- size: stat.size,
104
- mtime: stat.mtime,
105
- });
106
- } catch {
107
- // Skip files we can't stat
108
- }
109
- }
110
-
111
- // Sort: directories first, then files
112
- fileItems.sort((a, b) => {
113
- if (a.name === "..") return -1;
114
- if (b.name === "..") return 1;
115
- if (a.isDirectory && !b.isDirectory) return -1;
116
- if (!a.isDirectory && b.isDirectory) return 1;
117
- return a.name.localeCompare(b.name);
118
- });
119
-
120
- setItems(fileItems);
121
- setSelectedIndex(0);
122
- setError(null);
123
- } catch (err) {
124
- setError(`Error reading directory: ${err}`);
125
- setItems([]);
126
- }
127
- }, [currentPath, viewMode, mode, extensions]);
128
-
129
- useInput((input, key) => {
130
- // Handle escape
131
- if (key.escape) {
132
- if (viewMode === "input") {
133
- setViewMode("quick");
134
- setInputPath("");
135
- } else {
136
- onCancel();
137
- }
138
- return;
139
- }
140
-
141
- // Handle input mode
142
- if (viewMode === "input") {
143
- if (key.return) {
144
- // Try to navigate to input path
145
- const trimmed = inputPath.trim();
146
- if (trimmed) {
147
- if (fs.existsSync(trimmed)) {
148
- const stat = fs.statSync(trimmed);
149
- if (mode === "folder" && stat.isDirectory()) {
150
- onSelect(trimmed);
151
- } else if (mode === "file" && stat.isFile()) {
152
- onSelect(trimmed);
153
- } else if (stat.isDirectory()) {
154
- setCurrentPath(trimmed);
155
- setViewMode("browse");
156
- } else {
157
- setError("Invalid path for this mode");
158
- }
159
- } else {
160
- setError("Path does not exist");
161
- }
162
- }
163
- return;
164
- }
165
-
166
- if (key.backspace || key.delete) {
167
- setInputPath(prev => prev.slice(0, -1));
168
- return;
169
- }
170
-
171
- if (input && !key.ctrl && !key.meta) {
172
- setInputPath(prev => prev + input);
173
- return;
174
- }
175
- return;
176
- }
177
-
178
- // Handle quick location mode
179
- if (viewMode === "quick") {
180
- // Number keys for quick locations
181
- const num = parseInt(input, 10);
182
- if (!isNaN(num) && num >= 1 && num <= quickLocations.length) {
183
- const loc = quickLocations[num - 1];
184
- if (mode === "folder") {
185
- onSelect(loc.path);
186
- } else {
187
- setCurrentPath(loc.path);
188
- setViewMode("browse");
189
- }
190
- return;
191
- }
192
-
193
- // 'p' for paste path
194
- if (input.toLowerCase() === "p") {
195
- setViewMode("input");
196
- return;
197
- }
198
-
199
- // 'b' for browse
200
- if (input.toLowerCase() === "b") {
201
- setViewMode("browse");
202
- return;
203
- }
204
-
205
- // Arrow keys to navigate quick locations
206
- if (key.upArrow) {
207
- setSelectedIndex(prev => Math.max(0, prev - 1));
208
- }
209
- if (key.downArrow) {
210
- setSelectedIndex(prev => Math.min(quickLocations.length - 1, prev + 1));
211
- }
212
- if (key.return) {
213
- const loc = quickLocations[selectedIndex];
214
- if (loc) {
215
- if (mode === "folder") {
216
- onSelect(loc.path);
217
- } else {
218
- setCurrentPath(loc.path);
219
- setViewMode("browse");
220
- }
221
- }
222
- }
223
- return;
224
- }
225
-
226
- // Handle browse mode
227
- if (viewMode === "browse") {
228
- if (key.upArrow) {
229
- setSelectedIndex(prev => Math.max(0, prev - 1));
230
- }
231
- if (key.downArrow) {
232
- setSelectedIndex(prev => Math.min(items.length - 1, prev + 1));
233
- }
234
- if (key.return) {
235
- const item = items[selectedIndex];
236
- if (!item) return;
237
-
238
- if (item.isDirectory) {
239
- if (mode === "folder") {
240
- // In folder mode, enter selects current folder
241
- // Navigate into with right arrow
242
- onSelect(item.path === path.dirname(currentPath) ? currentPath : item.path);
243
- } else {
244
- // In file mode, navigate into directory
245
- setCurrentPath(item.path);
246
- }
247
- } else {
248
- // Select file
249
- onSelect(item.path);
250
- }
251
- }
252
- if (key.rightArrow) {
253
- const item = items[selectedIndex];
254
- if (item?.isDirectory && item.name !== "..") {
255
- setCurrentPath(item.path);
256
- }
257
- }
258
- if (key.leftArrow) {
259
- const parent = path.dirname(currentPath);
260
- if (parent !== currentPath) {
261
- setCurrentPath(parent);
262
- }
263
- }
264
-
265
- // 'p' for paste path
266
- if (input.toLowerCase() === "p") {
267
- setViewMode("input");
268
- return;
269
- }
270
-
271
- // 'q' for quick locations
272
- if (input.toLowerCase() === "q") {
273
- setViewMode("quick");
274
- return;
275
- }
276
- }
277
- });
278
-
279
- const formatSize = (bytes?: number): string => {
280
- if (bytes === undefined) return "";
281
- if (bytes < 1024) return `${bytes}B`;
282
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
283
- return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
284
- };
285
-
286
- const truncatePath = (p: string, maxLen: number): string => {
287
- if (p.length <= maxLen) return p;
288
- return "..." + p.slice(-(maxLen - 3));
289
- };
290
-
291
- return (
292
- <Box
293
- flexDirection="column"
294
- borderStyle="round"
295
- borderColor="gray"
296
- paddingX={2}
297
- paddingY={1}
298
- >
299
- {/* Title */}
300
- <Box marginBottom={1}>
301
- <Text bold color="cyan">
302
- {title || (mode === "folder" ? "Select Folder" : "Select File")}
303
- </Text>
304
- </Box>
305
-
306
- {/* Current path */}
307
- <Box marginBottom={1}>
308
- <Text dimColor>Path: </Text>
309
- <Text>{truncatePath(currentPath, 50)}</Text>
310
- </Box>
311
-
312
- {/* Error */}
313
- {error && (
314
- <Box marginBottom={1}>
315
- <Text color="red">✗ {error}</Text>
316
- </Box>
317
- )}
318
-
319
- {/* Quick Locations View */}
320
- {viewMode === "quick" && (
321
- <Box flexDirection="column">
322
- <Text dimColor>Quick Locations:</Text>
323
- {quickLocations.map((loc, index) => (
324
- <Box key={loc.path}>
325
- <Text color={index === selectedIndex ? "yellow" : "white"}>
326
- {index === selectedIndex ? "> " : " "}
327
- [{index + 1}] {loc.label}
328
- </Text>
329
- <Text dimColor> ({truncatePath(loc.path, 30)})</Text>
330
- </Box>
331
- ))}
332
- <Box marginTop={1}>
333
- <Text dimColor>[1-{quickLocations.length}] Select [B] Browse [P] Paste path [Esc] Cancel</Text>
334
- </Box>
335
- </Box>
336
- )}
337
-
338
- {/* Browse View */}
339
- {viewMode === "browse" && (
340
- <Box flexDirection="column">
341
- <Box flexDirection="column" height={10}>
342
- {items.slice(
343
- Math.max(0, selectedIndex - 4),
344
- Math.max(10, selectedIndex + 6)
345
- ).map((item, displayIndex) => {
346
- const actualIndex = Math.max(0, selectedIndex - 4) + displayIndex;
347
- const isSelected = actualIndex === selectedIndex;
348
-
349
- return (
350
- <Box key={item.path}>
351
- <Text color={isSelected ? "yellow" : "white"}>
352
- {isSelected ? "> " : " "}
353
- {item.isDirectory ? "📁 " : "📄 "}
354
- {item.name}
355
- </Text>
356
- {!item.isDirectory && (
357
- <Text dimColor> ({formatSize(item.size)})</Text>
358
- )}
359
- </Box>
360
- );
361
- })}
362
- </Box>
363
- <Box marginTop={1}>
364
- <Text dimColor>
365
- [↑↓] Navigate [Enter] Select [←→] Navigate dirs [Q] Quick [P] Paste [Esc] Cancel
366
- </Text>
367
- </Box>
368
- </Box>
369
- )}
370
-
371
- {/* Input View */}
372
- {viewMode === "input" && (
373
- <Box flexDirection="column">
374
- <Text dimColor>Paste or type path:</Text>
375
- <Box
376
- borderStyle="single"
377
- borderColor="yellow"
378
- paddingX={1}
379
- marginY={1}
380
- >
381
- <Text>
382
- {inputPath || " "}
383
- <Text color="yellow">▌</Text>
384
- </Text>
385
- </Box>
386
- <Box>
387
- <Text dimColor>[Enter] Confirm [Esc] Back</Text>
388
- </Box>
389
- </Box>
390
- )}
391
- </Box>
392
- );
393
- }
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ import { getQuickLocations, QuickLocation } from "../../core/config-store";
6
+
7
+ interface FileBrowserProps {
8
+ mode: "folder" | "file";
9
+ initialPath?: string;
10
+ extensions?: string[]; // e.g., [".ocam", ".json"]
11
+ title?: string;
12
+ onSelect: (selectedPath: string) => void;
13
+ onCancel: () => void;
14
+ }
15
+
16
+ interface FileItem {
17
+ name: string;
18
+ isDirectory: boolean;
19
+ path: string;
20
+ size?: number;
21
+ mtime?: Date;
22
+ }
23
+
24
+ type ViewMode = "quick" | "browse" | "input";
25
+
26
+ export function FileBrowser({
27
+ mode,
28
+ initialPath,
29
+ extensions = [],
30
+ title,
31
+ onSelect,
32
+ onCancel,
33
+ }: FileBrowserProps) {
34
+ const [currentPath, setCurrentPath] = useState(initialPath || process.cwd());
35
+ const [items, setItems] = useState<FileItem[]>([]);
36
+ const [selectedIndex, setSelectedIndex] = useState(0);
37
+ const [viewMode, setViewMode] = useState<ViewMode>("quick");
38
+ const [inputPath, setInputPath] = useState("");
39
+ const [error, setError] = useState<string | null>(null);
40
+ const [quickLocations, setQuickLocations] = useState<QuickLocation[]>([]);
41
+
42
+ // Load quick locations on mount
43
+ useEffect(() => {
44
+ setQuickLocations(getQuickLocations());
45
+ }, []);
46
+
47
+ // Load directory contents when path changes
48
+ useEffect(() => {
49
+ if (viewMode !== "browse") return;
50
+
51
+ try {
52
+ if (!fs.existsSync(currentPath)) {
53
+ setError(`Path not found: ${currentPath}`);
54
+ setItems([]);
55
+ return;
56
+ }
57
+
58
+ const stat = fs.statSync(currentPath);
59
+ if (!stat.isDirectory()) {
60
+ setError("Not a directory");
61
+ setItems([]);
62
+ return;
63
+ }
64
+
65
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
66
+ const fileItems: FileItem[] = [];
67
+
68
+ // Add parent directory
69
+ const parentPath = path.dirname(currentPath);
70
+ if (parentPath !== currentPath) {
71
+ fileItems.push({
72
+ name: "..",
73
+ isDirectory: true,
74
+ path: parentPath,
75
+ });
76
+ }
77
+
78
+ // Add directories and files
79
+ for (const entry of entries) {
80
+ const fullPath = path.join(currentPath, entry.name);
81
+ const isDir = entry.isDirectory();
82
+
83
+ // Skip hidden files/folders (starting with .)
84
+ if (entry.name.startsWith(".")) continue;
85
+
86
+ // In file mode, filter by extension
87
+ if (mode === "file" && !isDir) {
88
+ if (extensions.length > 0) {
89
+ const ext = path.extname(entry.name).toLowerCase();
90
+ if (!extensions.includes(ext)) continue;
91
+ }
92
+ }
93
+
94
+ // In folder mode, only show directories
95
+ if (mode === "folder" && !isDir) continue;
96
+
97
+ try {
98
+ const stat = fs.statSync(fullPath);
99
+ fileItems.push({
100
+ name: entry.name,
101
+ isDirectory: isDir,
102
+ path: fullPath,
103
+ size: stat.size,
104
+ mtime: stat.mtime,
105
+ });
106
+ } catch {
107
+ // Skip files we can't stat
108
+ }
109
+ }
110
+
111
+ // Sort: directories first, then files
112
+ fileItems.sort((a, b) => {
113
+ if (a.name === "..") return -1;
114
+ if (b.name === "..") return 1;
115
+ if (a.isDirectory && !b.isDirectory) return -1;
116
+ if (!a.isDirectory && b.isDirectory) return 1;
117
+ return a.name.localeCompare(b.name);
118
+ });
119
+
120
+ setItems(fileItems);
121
+ setSelectedIndex(0);
122
+ setError(null);
123
+ } catch (err) {
124
+ setError(`Error reading directory: ${err}`);
125
+ setItems([]);
126
+ }
127
+ }, [currentPath, viewMode, mode, extensions]);
128
+
129
+ useInput((input, key) => {
130
+ // Handle escape
131
+ if (key.escape) {
132
+ if (viewMode === "input") {
133
+ setViewMode("quick");
134
+ setInputPath("");
135
+ } else {
136
+ onCancel();
137
+ }
138
+ return;
139
+ }
140
+
141
+ // Handle input mode
142
+ if (viewMode === "input") {
143
+ if (key.return) {
144
+ // Try to navigate to input path
145
+ const trimmed = inputPath.trim();
146
+ if (trimmed) {
147
+ if (fs.existsSync(trimmed)) {
148
+ const stat = fs.statSync(trimmed);
149
+ if (mode === "folder" && stat.isDirectory()) {
150
+ onSelect(trimmed);
151
+ } else if (mode === "file" && stat.isFile()) {
152
+ onSelect(trimmed);
153
+ } else if (stat.isDirectory()) {
154
+ setCurrentPath(trimmed);
155
+ setViewMode("browse");
156
+ } else {
157
+ setError("Invalid path for this mode");
158
+ }
159
+ } else {
160
+ setError("Path does not exist");
161
+ }
162
+ }
163
+ return;
164
+ }
165
+
166
+ if (key.backspace || key.delete) {
167
+ setInputPath(prev => prev.slice(0, -1));
168
+ return;
169
+ }
170
+
171
+ if (input && !key.ctrl && !key.meta) {
172
+ setInputPath(prev => prev + input);
173
+ return;
174
+ }
175
+ return;
176
+ }
177
+
178
+ // Handle quick location mode
179
+ if (viewMode === "quick") {
180
+ // Number keys for quick locations
181
+ const num = parseInt(input, 10);
182
+ if (!isNaN(num) && num >= 1 && num <= quickLocations.length) {
183
+ const loc = quickLocations[num - 1];
184
+ if (mode === "folder") {
185
+ onSelect(loc.path);
186
+ } else {
187
+ setCurrentPath(loc.path);
188
+ setViewMode("browse");
189
+ }
190
+ return;
191
+ }
192
+
193
+ // 'p' for paste path
194
+ if (input.toLowerCase() === "p") {
195
+ setViewMode("input");
196
+ return;
197
+ }
198
+
199
+ // 'b' for browse
200
+ if (input.toLowerCase() === "b") {
201
+ setViewMode("browse");
202
+ return;
203
+ }
204
+
205
+ // Arrow keys to navigate quick locations
206
+ if (key.upArrow) {
207
+ setSelectedIndex(prev => Math.max(0, prev - 1));
208
+ }
209
+ if (key.downArrow) {
210
+ setSelectedIndex(prev => Math.min(quickLocations.length - 1, prev + 1));
211
+ }
212
+ if (key.return) {
213
+ const loc = quickLocations[selectedIndex];
214
+ if (loc) {
215
+ if (mode === "folder") {
216
+ onSelect(loc.path);
217
+ } else {
218
+ setCurrentPath(loc.path);
219
+ setViewMode("browse");
220
+ }
221
+ }
222
+ }
223
+ return;
224
+ }
225
+
226
+ // Handle browse mode
227
+ if (viewMode === "browse") {
228
+ if (key.upArrow) {
229
+ setSelectedIndex(prev => Math.max(0, prev - 1));
230
+ }
231
+ if (key.downArrow) {
232
+ setSelectedIndex(prev => Math.min(items.length - 1, prev + 1));
233
+ }
234
+ if (key.return) {
235
+ const item = items[selectedIndex];
236
+ if (!item) return;
237
+
238
+ if (item.isDirectory) {
239
+ if (mode === "folder") {
240
+ // In folder mode, enter selects current folder
241
+ // Navigate into with right arrow
242
+ onSelect(item.path === path.dirname(currentPath) ? currentPath : item.path);
243
+ } else {
244
+ // In file mode, navigate into directory
245
+ setCurrentPath(item.path);
246
+ }
247
+ } else {
248
+ // Select file
249
+ onSelect(item.path);
250
+ }
251
+ }
252
+ if (key.rightArrow) {
253
+ const item = items[selectedIndex];
254
+ if (item?.isDirectory && item.name !== "..") {
255
+ setCurrentPath(item.path);
256
+ }
257
+ }
258
+ if (key.leftArrow) {
259
+ const parent = path.dirname(currentPath);
260
+ if (parent !== currentPath) {
261
+ setCurrentPath(parent);
262
+ }
263
+ }
264
+
265
+ // 'p' for paste path
266
+ if (input.toLowerCase() === "p") {
267
+ setViewMode("input");
268
+ return;
269
+ }
270
+
271
+ // 'q' for quick locations
272
+ if (input.toLowerCase() === "q") {
273
+ setViewMode("quick");
274
+ return;
275
+ }
276
+ }
277
+ });
278
+
279
+ const formatSize = (bytes?: number): string => {
280
+ if (bytes === undefined) return "";
281
+ if (bytes < 1024) return `${bytes}B`;
282
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
283
+ return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
284
+ };
285
+
286
+ const truncatePath = (p: string, maxLen: number): string => {
287
+ if (p.length <= maxLen) return p;
288
+ return "..." + p.slice(-(maxLen - 3));
289
+ };
290
+
291
+ return (
292
+ <Box
293
+ flexDirection="column"
294
+ borderStyle="round"
295
+ borderColor="gray"
296
+ paddingX={2}
297
+ paddingY={1}
298
+ >
299
+ {/* Title */}
300
+ <Box marginBottom={1}>
301
+ <Text bold color="cyan">
302
+ {title || (mode === "folder" ? "Select Folder" : "Select File")}
303
+ </Text>
304
+ </Box>
305
+
306
+ {/* Current path */}
307
+ <Box marginBottom={1}>
308
+ <Text dimColor>Path: </Text>
309
+ <Text>{truncatePath(currentPath, 50)}</Text>
310
+ </Box>
311
+
312
+ {/* Error */}
313
+ {error && (
314
+ <Box marginBottom={1}>
315
+ <Text color="red">✗ {error}</Text>
316
+ </Box>
317
+ )}
318
+
319
+ {/* Quick Locations View */}
320
+ {viewMode === "quick" && (
321
+ <Box flexDirection="column">
322
+ <Text dimColor>Quick Locations:</Text>
323
+ {quickLocations.map((loc, index) => (
324
+ <Box key={loc.path}>
325
+ <Text color={index === selectedIndex ? "yellow" : "white"}>
326
+ {index === selectedIndex ? "> " : " "}
327
+ [{index + 1}] {loc.label}
328
+ </Text>
329
+ <Text dimColor> ({truncatePath(loc.path, 30)})</Text>
330
+ </Box>
331
+ ))}
332
+ <Box marginTop={1}>
333
+ <Text dimColor>[1-{quickLocations.length}] Select [B] Browse [P] Paste path [Esc] Cancel</Text>
334
+ </Box>
335
+ </Box>
336
+ )}
337
+
338
+ {/* Browse View */}
339
+ {viewMode === "browse" && (
340
+ <Box flexDirection="column">
341
+ <Box flexDirection="column" height={10}>
342
+ {items.slice(
343
+ Math.max(0, selectedIndex - 4),
344
+ Math.max(10, selectedIndex + 6)
345
+ ).map((item, displayIndex) => {
346
+ const actualIndex = Math.max(0, selectedIndex - 4) + displayIndex;
347
+ const isSelected = actualIndex === selectedIndex;
348
+
349
+ return (
350
+ <Box key={item.path}>
351
+ <Text color={isSelected ? "yellow" : "white"}>
352
+ {isSelected ? "> " : " "}
353
+ {item.isDirectory ? "📁 " : "📄 "}
354
+ {item.name}
355
+ </Text>
356
+ {!item.isDirectory && (
357
+ <Text dimColor> ({formatSize(item.size)})</Text>
358
+ )}
359
+ </Box>
360
+ );
361
+ })}
362
+ </Box>
363
+ <Box marginTop={1}>
364
+ <Text dimColor>
365
+ [↑↓] Navigate [Enter] Select [←→] Navigate dirs [Q] Quick [P] Paste [Esc] Cancel
366
+ </Text>
367
+ </Box>
368
+ </Box>
369
+ )}
370
+
371
+ {/* Input View */}
372
+ {viewMode === "input" && (
373
+ <Box flexDirection="column">
374
+ <Text dimColor>Paste or type path:</Text>
375
+ <Box
376
+ borderStyle="single"
377
+ borderColor="yellow"
378
+ paddingX={1}
379
+ marginY={1}
380
+ >
381
+ <Text>
382
+ {inputPath || " "}
383
+ <Text color="yellow">▌</Text>
384
+ </Text>
385
+ </Box>
386
+ <Box>
387
+ <Text dimColor>[Enter] Confirm [Esc] Back</Text>
388
+ </Box>
389
+ </Box>
390
+ )}
391
+ </Box>
392
+ );
393
+ }