projax 3.3.57 → 3.3.59

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 (80) hide show
  1. package/dist/electron/script-runner.js +52 -20
  2. package/dist/index.js +1 -1
  3. package/dist/prxi.js +846 -111
  4. package/dist/prxi.tsx +1236 -181
  5. package/dist/script-runner.js +52 -20
  6. package/package.json +1 -1
  7. package/coverage/base.css +0 -224
  8. package/coverage/block-navigation.js +0 -87
  9. package/coverage/core-bridge.ts.html +0 -292
  10. package/coverage/favicon.png +0 -0
  11. package/coverage/index.html +0 -191
  12. package/coverage/lcov-report/base.css +0 -224
  13. package/coverage/lcov-report/block-navigation.js +0 -87
  14. package/coverage/lcov-report/core-bridge.ts.html +0 -292
  15. package/coverage/lcov-report/favicon.png +0 -0
  16. package/coverage/lcov-report/index.html +0 -191
  17. package/coverage/lcov-report/port-extractor.ts.html +0 -1174
  18. package/coverage/lcov-report/port-scanner.ts.html +0 -301
  19. package/coverage/lcov-report/port-utils.ts.html +0 -670
  20. package/coverage/lcov-report/prettify.css +0 -1
  21. package/coverage/lcov-report/prettify.js +0 -2
  22. package/coverage/lcov-report/script-runner.ts.html +0 -3346
  23. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  24. package/coverage/lcov-report/sorter.js +0 -210
  25. package/coverage/lcov-report/test-parser.ts.html +0 -799
  26. package/coverage/lcov.info +0 -1338
  27. package/coverage/port-extractor.ts.html +0 -1174
  28. package/coverage/port-scanner.ts.html +0 -301
  29. package/coverage/port-utils.ts.html +0 -670
  30. package/coverage/prettify.css +0 -1
  31. package/coverage/prettify.js +0 -2
  32. package/coverage/script-runner.ts.html +0 -3346
  33. package/coverage/sort-arrow-sprite.png +0 -0
  34. package/coverage/sorter.js +0 -210
  35. package/coverage/test-parser.ts.html +0 -799
  36. package/dist/__tests__/core-bridge.test.d.ts +0 -1
  37. package/dist/__tests__/core-bridge.test.js +0 -135
  38. package/dist/__tests__/port-extractor.test.d.ts +0 -1
  39. package/dist/__tests__/port-extractor.test.js +0 -407
  40. package/dist/__tests__/port-scanner.test.d.ts +0 -1
  41. package/dist/__tests__/port-scanner.test.js +0 -170
  42. package/dist/__tests__/port-utils.test.d.ts +0 -1
  43. package/dist/__tests__/port-utils.test.js +0 -127
  44. package/dist/__tests__/script-runner.test.d.ts +0 -1
  45. package/dist/__tests__/script-runner.test.js +0 -491
  46. package/dist/__tests__/test-parser.test.d.ts +0 -1
  47. package/dist/__tests__/test-parser.test.js +0 -276
  48. package/dist/api/__tests__/database.test.d.ts +0 -2
  49. package/dist/api/__tests__/database.test.d.ts.map +0 -1
  50. package/dist/api/__tests__/database.test.js +0 -485
  51. package/dist/api/__tests__/database.test.js.map +0 -1
  52. package/dist/api/__tests__/routes.test.d.ts +0 -2
  53. package/dist/api/__tests__/routes.test.d.ts.map +0 -1
  54. package/dist/api/__tests__/routes.test.js +0 -484
  55. package/dist/api/__tests__/routes.test.js.map +0 -1
  56. package/dist/api/__tests__/scanner.test.d.ts +0 -2
  57. package/dist/api/__tests__/scanner.test.d.ts.map +0 -1
  58. package/dist/api/__tests__/scanner.test.js +0 -403
  59. package/dist/api/__tests__/scanner.test.js.map +0 -1
  60. package/dist/core/__tests__/database.test.d.ts +0 -1
  61. package/dist/core/__tests__/database.test.js +0 -557
  62. package/dist/core/__tests__/detector.test.d.ts +0 -1
  63. package/dist/core/__tests__/detector.test.js +0 -375
  64. package/dist/core/__tests__/index.test.d.ts +0 -1
  65. package/dist/core/__tests__/index.test.js +0 -469
  66. package/dist/core/__tests__/scanner.test.d.ts +0 -1
  67. package/dist/core/__tests__/scanner.test.js +0 -406
  68. package/dist/core/__tests__/settings.test.d.ts +0 -1
  69. package/dist/core/__tests__/settings.test.js +0 -280
  70. package/dist/electron/core/__tests__/database.test.d.ts +0 -1
  71. package/dist/electron/core/__tests__/database.test.js +0 -557
  72. package/dist/electron/core/__tests__/detector.test.d.ts +0 -1
  73. package/dist/electron/core/__tests__/detector.test.js +0 -375
  74. package/dist/electron/core/__tests__/index.test.d.ts +0 -1
  75. package/dist/electron/core/__tests__/index.test.js +0 -469
  76. package/dist/electron/core/__tests__/scanner.test.d.ts +0 -1
  77. package/dist/electron/core/__tests__/scanner.test.js +0 -406
  78. package/dist/electron/core/__tests__/settings.test.d.ts +0 -1
  79. package/dist/electron/core/__tests__/settings.test.js +0 -280
  80. package/jest.config.js +0 -26
package/dist/prxi.tsx CHANGED
@@ -1,15 +1,33 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { render, Box, Text, useInput, useApp, useFocus, useFocusManager } from 'ink';
3
+
4
+ // Handle EPIPE errors gracefully to prevent crashes when output streams close
5
+ process.stdout.on('error', (err: NodeJS.ErrnoException) => {
6
+ if (err.code === 'EPIPE') {
7
+ // Output stream closed, exit gracefully
8
+ process.exit(0);
9
+ }
10
+ });
11
+ process.stderr.on('error', (err: NodeJS.ErrnoException) => {
12
+ if (err.code === 'EPIPE') {
13
+ process.exit(0);
14
+ }
15
+ });
16
+
3
17
  import {
4
18
  getDatabaseManager,
5
19
  getAllProjects,
6
20
  scanProject,
21
+ addProject,
22
+ removeProject,
23
+ getCurrentBranch,
7
24
  Project,
8
- } from '../../cli/src/core-bridge';
9
- import { getProjectScripts, getRunningProcessesClean, runScript, runScriptInBackground, stopScript } from '../../cli/src/script-runner';
25
+ } from './core-bridge';
26
+ import { getProjectScripts, getRunningProcessesClean, runScript, runScriptInBackground, stopScript } from './script-runner';
10
27
  import { spawn } from 'child_process';
11
28
  import * as path from 'path';
12
29
  import * as os from 'os';
30
+ import * as fs from 'fs';
13
31
 
14
32
  // Color scheme matching desktop app
15
33
  const colors = {
@@ -49,6 +67,52 @@ function truncateText(text: string, maxLength: number): string {
49
67
  return text.substring(0, maxLength - 3) + '...';
50
68
  }
51
69
 
70
+ // Type definitions for views and filters
71
+ type ViewType = 'projects' | 'workspaces' | 'processes' | 'settings';
72
+ type FilterType = 'all' | 'name' | 'path' | 'ports' | 'tags' | 'running';
73
+ type SortType = 'name-asc' | 'name-desc' | 'recent' | 'oldest' | 'running';
74
+
75
+ const FILTER_TYPES: FilterType[] = ['all', 'name', 'path', 'ports', 'tags', 'running'];
76
+ const SORT_TYPES: SortType[] = ['name-asc', 'name-desc', 'recent', 'oldest', 'running'];
77
+
78
+ const FILTER_LABELS: Record<FilterType, string> = {
79
+ 'all': 'All',
80
+ 'name': 'Name',
81
+ 'path': 'Path',
82
+ 'ports': 'Ports',
83
+ 'tags': 'Tags',
84
+ 'running': 'Running',
85
+ };
86
+
87
+ const SORT_LABELS: Record<SortType, string> = {
88
+ 'name-asc': 'Name A-Z',
89
+ 'name-desc': 'Name Z-A',
90
+ 'recent': 'Recently Scanned',
91
+ 'oldest': 'Oldest First',
92
+ 'running': 'Running First',
93
+ };
94
+
95
+ // Workspace type
96
+ interface Workspace {
97
+ id: number;
98
+ name: string;
99
+ description?: string;
100
+ workspace_file_path: string;
101
+ created_at: number;
102
+ }
103
+
104
+ // Settings type
105
+ interface AppSettings {
106
+ editor: {
107
+ type: 'vscode' | 'cursor' | 'windsurf' | 'zed' | 'custom';
108
+ customPath?: string;
109
+ };
110
+ browser: {
111
+ type: 'chrome' | 'firefox' | 'safari' | 'edge' | 'custom';
112
+ customPath?: string;
113
+ };
114
+ }
115
+
52
116
  interface HelpModalProps {
53
117
  onClose: () => void;
54
118
  }
@@ -66,23 +130,31 @@ const HelpModal: React.FC<HelpModalProps> = ({ onClose }) => {
66
130
  borderStyle="round"
67
131
  borderColor={colors.accentCyan}
68
132
  padding={1}
69
- width={70}
133
+ width={75}
70
134
  >
71
135
  <Text bold color={colors.accentCyan}>
72
136
  PROJAX Terminal UI - Help
73
137
  </Text>
74
138
  <Text> </Text>
75
- <Text color={colors.accentCyan}>Navigation:</Text>
76
- <Text> ↑/k Move up in project list</Text>
77
- <Text> ↓/j Move down in project list</Text>
78
- <Text> Tab/←→ Switch between list and details</Text>
139
+ <Text color={colors.accentCyan}>View Navigation:</Text>
140
+ <Text> 1 Projects view</Text>
141
+ <Text> 2 Workspaces view</Text>
142
+ <Text> 3 Global processes view</Text>
143
+ <Text> 4 Settings</Text>
144
+ <Text> T Toggle terminal output panel</Text>
79
145
  <Text> </Text>
80
- <Text color={colors.accentCyan}>List Panel Actions:</Text>
146
+ <Text color={colors.accentCyan}>Projects View - Navigation:</Text>
147
+ <Text> ↑/k Move up in list</Text>
148
+ <Text> ↓/j Move down in list</Text>
149
+ <Text> Tab/←→ Switch between list and details</Text>
81
150
  <Text> / Search projects (fuzzy search)</Text>
82
- <Text> s Scan selected project for tests</Text>
151
+ <Text> F Cycle filter type (all/name/path/ports/tags/running)</Text>
152
+ <Text> S Cycle sort order (name/recent/oldest/running)</Text>
153
+ <Text> g Refresh git branches</Text>
154
+ <Text> R Full refresh (projects + branches + processes)</Text>
83
155
  <Text> </Text>
84
- <Text color={colors.accentCyan}>Details Panel Actions:</Text>
85
- <Text> ↑↓/kj Scroll details</Text>
156
+ <Text color={colors.accentCyan}>Projects View - Actions:</Text>
157
+ <Text> a Add new project</Text>
86
158
  <Text> e Edit project name</Text>
87
159
  <Text> t Add/edit tags</Text>
88
160
  <Text> o Open project in editor</Text>
@@ -90,16 +162,12 @@ const HelpModal: React.FC<HelpModalProps> = ({ onClose }) => {
90
162
  <Text> u Show detected URLs</Text>
91
163
  <Text> s Scan project for tests</Text>
92
164
  <Text> p Scan ports for project</Text>
93
- <Text> r Show scripts (use CLI to run)</Text>
165
+ <Text> r Run scripts (select from list)</Text>
94
166
  <Text> x Stop all scripts for project</Text>
95
- <Text> d Delete project</Text>
96
- <Text> </Text>
97
- <Text color={colors.accentCyan}>Editing:</Text>
98
- <Text> Enter Save changes</Text>
99
- <Text> Esc Cancel editing</Text>
167
+ <Text> d Delete project (with confirmation)</Text>
100
168
  <Text> </Text>
101
169
  <Text color={colors.accentCyan}>General:</Text>
102
- <Text> q/Esc Quit</Text>
170
+ <Text> q/Esc Quit (or close modal)</Text>
103
171
  <Text> ? Show this help</Text>
104
172
  <Text> </Text>
105
173
  <Text color={colors.textSecondary}>Press any key to close...</Text>
@@ -126,6 +194,364 @@ const LoadingModal: React.FC<LoadingModalProps> = ({ message }) => {
126
194
  );
127
195
  };
128
196
 
197
+ // Confirmation Modal
198
+ interface ConfirmModalProps {
199
+ title: string;
200
+ message: string;
201
+ onConfirm: () => void;
202
+ onCancel: () => void;
203
+ }
204
+
205
+ const ConfirmModal: React.FC<ConfirmModalProps> = ({ title, message, onConfirm, onCancel }) => {
206
+ const [selected, setSelected] = useState<'yes' | 'no'>('no');
207
+
208
+ useInput((input: string, key: any) => {
209
+ if (key.escape || input === 'n') {
210
+ onCancel();
211
+ return;
212
+ }
213
+ if (key.return) {
214
+ if (selected === 'yes') {
215
+ onConfirm();
216
+ } else {
217
+ onCancel();
218
+ }
219
+ return;
220
+ }
221
+ if (input === 'y') {
222
+ onConfirm();
223
+ return;
224
+ }
225
+ if (key.leftArrow || key.rightArrow || input === 'h' || input === 'l') {
226
+ setSelected(prev => prev === 'yes' ? 'no' : 'yes');
227
+ }
228
+ });
229
+
230
+ return (
231
+ <Box
232
+ flexDirection="column"
233
+ borderStyle="round"
234
+ borderColor={colors.accentOrange}
235
+ padding={1}
236
+ width={60}
237
+ >
238
+ <Text bold color={colors.accentOrange}>{title}</Text>
239
+ <Text> </Text>
240
+ <Text>{message}</Text>
241
+ <Text> </Text>
242
+ <Box>
243
+ <Text color={selected === 'yes' ? colors.accentCyan : colors.textSecondary}>
244
+ {selected === 'yes' ? '▶ ' : ' '}Yes
245
+ </Text>
246
+ <Text> </Text>
247
+ <Text color={selected === 'no' ? colors.accentCyan : colors.textSecondary}>
248
+ {selected === 'no' ? '▶ ' : ' '}No
249
+ </Text>
250
+ </Box>
251
+ <Text> </Text>
252
+ <Text color={colors.textTertiary}>y/n or ←→ to select, Enter to confirm</Text>
253
+ </Box>
254
+ );
255
+ };
256
+
257
+ // Add Project Modal
258
+ interface AddProjectModalProps {
259
+ onAdd: (projectPath: string, projectName?: string) => void;
260
+ onCancel: () => void;
261
+ }
262
+
263
+ const AddProjectModal: React.FC<AddProjectModalProps> = ({ onAdd, onCancel }) => {
264
+ const [step, setStep] = useState<'path' | 'name'>('path');
265
+ const [pathInput, setPathInput] = useState('');
266
+ const [nameInput, setNameInput] = useState('');
267
+ const [error, setError] = useState<string | null>(null);
268
+
269
+ useInput((input: string, key: any) => {
270
+ if (key.escape) {
271
+ onCancel();
272
+ return;
273
+ }
274
+
275
+ if (step === 'path') {
276
+ if (key.return) {
277
+ // Validate path
278
+ const resolvedPath = pathInput.startsWith('~')
279
+ ? path.join(os.homedir(), pathInput.slice(1))
280
+ : path.resolve(pathInput);
281
+
282
+ if (!fs.existsSync(resolvedPath)) {
283
+ setError('Path does not exist');
284
+ return;
285
+ }
286
+
287
+ if (!fs.statSync(resolvedPath).isDirectory()) {
288
+ setError('Path is not a directory');
289
+ return;
290
+ }
291
+
292
+ // Check if project already exists
293
+ const db = getDatabaseManager();
294
+ const existing = db.getProjectByPath(resolvedPath);
295
+ if (existing) {
296
+ setError(`Project already exists: ${existing.name}`);
297
+ return;
298
+ }
299
+
300
+ setError(null);
301
+ setNameInput(path.basename(resolvedPath));
302
+ setStep('name');
303
+ return;
304
+ }
305
+
306
+ if (key.backspace || key.delete) {
307
+ setPathInput(prev => prev.slice(0, -1));
308
+ setError(null);
309
+ return;
310
+ }
311
+
312
+ if (key.tab) {
313
+ // Tab completion - get directories in current path
314
+ try {
315
+ const currentPath = pathInput.startsWith('~')
316
+ ? path.join(os.homedir(), pathInput.slice(1))
317
+ : pathInput || '.';
318
+ const dir = path.dirname(currentPath);
319
+ const base = path.basename(currentPath);
320
+
321
+ if (fs.existsSync(dir)) {
322
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
323
+ .filter(e => e.isDirectory() && e.name.startsWith(base) && !e.name.startsWith('.'))
324
+ .map(e => e.name);
325
+
326
+ if (entries.length === 1) {
327
+ const completed = path.join(dir, entries[0]) + '/';
328
+ setPathInput(completed.replace(os.homedir(), '~'));
329
+ }
330
+ }
331
+ } catch {
332
+ // Ignore tab completion errors
333
+ }
334
+ return;
335
+ }
336
+
337
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
338
+ setPathInput(prev => prev + input);
339
+ setError(null);
340
+ }
341
+ } else if (step === 'name') {
342
+ if (key.return) {
343
+ const resolvedPath = pathInput.startsWith('~')
344
+ ? path.join(os.homedir(), pathInput.slice(1))
345
+ : path.resolve(pathInput);
346
+ onAdd(resolvedPath, nameInput.trim() || undefined);
347
+ return;
348
+ }
349
+
350
+ if (key.backspace || key.delete) {
351
+ setNameInput(prev => prev.slice(0, -1));
352
+ return;
353
+ }
354
+
355
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
356
+ setNameInput(prev => prev + input);
357
+ }
358
+ }
359
+ });
360
+
361
+ return (
362
+ <Box
363
+ flexDirection="column"
364
+ borderStyle="round"
365
+ borderColor={colors.accentCyan}
366
+ padding={1}
367
+ width={70}
368
+ >
369
+ <Text bold color={colors.accentCyan}>Add Project</Text>
370
+ <Text> </Text>
371
+
372
+ {step === 'path' && (
373
+ <>
374
+ <Text>Enter project path:</Text>
375
+ <Box>
376
+ <Text color={colors.accentGreen}>&gt; </Text>
377
+ <Text>{pathInput}</Text>
378
+ <Text color={colors.textTertiary}>_</Text>
379
+ </Box>
380
+ {error && (
381
+ <Text color="#f85149">{error}</Text>
382
+ )}
383
+ <Text> </Text>
384
+ <Text color={colors.textSecondary}>Tab: autocomplete | Enter: next | Esc: cancel</Text>
385
+ </>
386
+ )}
387
+
388
+ {step === 'name' && (
389
+ <>
390
+ <Text color={colors.textSecondary}>Path: {pathInput}</Text>
391
+ <Text> </Text>
392
+ <Text>Enter project name:</Text>
393
+ <Box>
394
+ <Text color={colors.accentGreen}>&gt; </Text>
395
+ <Text>{nameInput}</Text>
396
+ <Text color={colors.textTertiary}>_</Text>
397
+ </Box>
398
+ <Text> </Text>
399
+ <Text color={colors.textSecondary}>Enter: add project | Esc: cancel</Text>
400
+ </>
401
+ )}
402
+ </Box>
403
+ );
404
+ };
405
+
406
+ // Settings Modal
407
+ interface SettingsModalProps {
408
+ onClose: () => void;
409
+ }
410
+
411
+ const SettingsModal: React.FC<SettingsModalProps> = ({ onClose }) => {
412
+ const [settings, setSettings] = useState<AppSettings>({
413
+ editor: { type: 'vscode' },
414
+ browser: { type: 'chrome' },
415
+ });
416
+ const [selectedSection, setSelectedSection] = useState<'editor' | 'browser'>('editor');
417
+ const [selectedOptionIndex, setSelectedOptionIndex] = useState(0);
418
+
419
+ const editorOptions: Array<'vscode' | 'cursor' | 'windsurf' | 'zed' | 'custom'> = [
420
+ 'vscode',
421
+ 'cursor',
422
+ 'windsurf',
423
+ 'zed',
424
+ 'custom',
425
+ ];
426
+ const browserOptions: Array<'chrome' | 'firefox' | 'safari' | 'edge' | 'custom'> = [
427
+ 'chrome',
428
+ 'firefox',
429
+ 'safari',
430
+ 'edge',
431
+ 'custom',
432
+ ];
433
+
434
+ useEffect(() => {
435
+ // Load settings
436
+ try {
437
+ const settingsPath = path.join(os.homedir(), '.projax', 'settings.json');
438
+ if (fs.existsSync(settingsPath)) {
439
+ const data = fs.readFileSync(settingsPath, 'utf-8');
440
+ setSettings(JSON.parse(data));
441
+ }
442
+ } catch {
443
+ // Use defaults
444
+ }
445
+ }, []);
446
+
447
+ const saveSettings = () => {
448
+ try {
449
+ const settingsDir = path.join(os.homedir(), '.projax');
450
+ if (!fs.existsSync(settingsDir)) {
451
+ fs.mkdirSync(settingsDir, { recursive: true });
452
+ }
453
+ const settingsPath = path.join(settingsDir, 'settings.json');
454
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
455
+ onClose();
456
+ } catch {
457
+ // Ignore save errors
458
+ }
459
+ };
460
+
461
+ useInput((input: string, key: any) => {
462
+ if (key.escape || input === 'q') {
463
+ onClose();
464
+ return;
465
+ }
466
+
467
+ if (key.return) {
468
+ saveSettings();
469
+ return;
470
+ }
471
+
472
+ if (key.tab) {
473
+ setSelectedSection((prev) => (prev === 'editor' ? 'browser' : 'editor'));
474
+ setSelectedOptionIndex(0);
475
+ return;
476
+ }
477
+
478
+ if (key.upArrow || input === 'k') {
479
+ setSelectedOptionIndex((prev) => Math.max(0, prev - 1));
480
+ return;
481
+ }
482
+
483
+ if (key.downArrow || input === 'j') {
484
+ const maxIndex = selectedSection === 'editor' ? editorOptions.length - 1 : browserOptions.length - 1;
485
+ setSelectedOptionIndex((prev) => Math.min(maxIndex, prev + 1));
486
+ return;
487
+ }
488
+
489
+ if (input === ' ' || key.return) {
490
+ if (selectedSection === 'editor') {
491
+ setSettings({
492
+ ...settings,
493
+ editor: { type: editorOptions[selectedOptionIndex] },
494
+ });
495
+ } else {
496
+ setSettings({
497
+ ...settings,
498
+ browser: { type: browserOptions[selectedOptionIndex] },
499
+ });
500
+ }
501
+ }
502
+ });
503
+
504
+ const currentOptions = selectedSection === 'editor' ? editorOptions : browserOptions;
505
+ const currentValue = selectedSection === 'editor' ? settings.editor.type : settings.browser.type;
506
+
507
+ return (
508
+ <Box
509
+ flexDirection="column"
510
+ borderStyle="round"
511
+ borderColor={colors.accentCyan}
512
+ padding={1}
513
+ width={60}
514
+ >
515
+ <Text bold color={colors.accentCyan}>Settings</Text>
516
+ <Text> </Text>
517
+
518
+ {/* Section tabs */}
519
+ <Box>
520
+ <Text
521
+ color={selectedSection === 'editor' ? colors.accentCyan : colors.textTertiary}
522
+ bold={selectedSection === 'editor'}
523
+ >
524
+ [Editor]
525
+ </Text>
526
+ <Text> </Text>
527
+ <Text
528
+ color={selectedSection === 'browser' ? colors.accentCyan : colors.textTertiary}
529
+ bold={selectedSection === 'browser'}
530
+ >
531
+ [Browser]
532
+ </Text>
533
+ </Box>
534
+ <Text> </Text>
535
+
536
+ {/* Options */}
537
+ {currentOptions.map((option, index) => {
538
+ const isSelected = index === selectedOptionIndex;
539
+ const isActive = option === currentValue;
540
+ return (
541
+ <Text key={option} color={isSelected ? colors.accentCyan : colors.textPrimary} bold={isSelected}>
542
+ {isSelected ? '▶ ' : ' '}
543
+ {isActive ? '● ' : '○ '}
544
+ {option.charAt(0).toUpperCase() + option.slice(1)}
545
+ </Text>
546
+ );
547
+ })}
548
+
549
+ <Text> </Text>
550
+ <Text color={colors.textSecondary}>Tab: switch section | ↑↓: select | Space: choose | Enter: save | Esc: close</Text>
551
+ </Box>
552
+ );
553
+ };
554
+
129
555
  interface ErrorModalProps {
130
556
  message: string;
131
557
  onClose: () => void;
@@ -240,15 +666,21 @@ interface ProjectListProps {
240
666
  isFocused: boolean;
241
667
  height: number;
242
668
  scrollOffset: number;
669
+ gitBranches: Map<number, string | null>;
670
+ filterType: FilterType;
671
+ sortType: SortType;
243
672
  }
244
673
 
245
- const ProjectListComponent: React.FC<ProjectListProps> = ({
246
- projects,
247
- selectedIndex,
248
- runningProcesses,
674
+ const ProjectListComponent: React.FC<ProjectListProps> = ({
675
+ projects,
676
+ selectedIndex,
677
+ runningProcesses,
249
678
  isFocused,
250
679
  height,
251
680
  scrollOffset,
681
+ gitBranches,
682
+ filterType,
683
+ sortType,
252
684
  }) => {
253
685
  const { focus } = useFocus({ id: 'projectList' });
254
686
 
@@ -264,51 +696,72 @@ const ProjectListComponent: React.FC<ProjectListProps> = ({
264
696
  const hasMoreBelow = endIndex < projects.length;
265
697
 
266
698
  return (
267
- <Box
268
- flexDirection="column"
269
- width="35%"
699
+ <Box
700
+ flexDirection="column"
701
+ width="35%"
270
702
  height={height}
271
- borderStyle="round"
272
- borderColor={isFocused ? colors.accentCyan : colors.borderColor}
703
+ borderStyle="round"
704
+ borderColor={isFocused ? colors.accentCyan : colors.borderColor}
273
705
  padding={1}
274
706
  flexShrink={0}
275
707
  flexGrow={0}
276
708
  >
277
- <Text bold color={colors.textPrimary}>
278
- Projects ({projects.length})
279
- </Text>
709
+ <Box flexDirection="column">
710
+ <Text bold color={colors.textPrimary}>
711
+ Projects ({projects.length})
712
+ </Text>
713
+ <Text color={colors.textTertiary}>
714
+ <Text color={colors.accentPurple}>{FILTER_LABELS[filterType]}</Text>
715
+ {' | '}
716
+ <Text color={colors.accentOrange}>{SORT_LABELS[sortType]}</Text>
717
+ </Text>
718
+ </Box>
280
719
  <Box flexDirection="column" flexGrow={1}>
281
- {projects.length === 0 ? (
282
- <Text color={colors.textTertiary}>No projects found</Text>
283
- ) : (
720
+ {projects.length === 0 ? (
721
+ <Text color={colors.textTertiary}>No projects found</Text>
722
+ ) : (
284
723
  <>
285
724
  {hasMoreAbove && (
286
725
  <Text color={colors.textTertiary}>↑ {startIndex} more above</Text>
287
726
  )}
288
727
  {visibleProjects.map((project, localIndex) => {
289
728
  const index = startIndex + localIndex;
290
- const isSelected = index === selectedIndex;
291
-
292
- // Check if this project has running scripts
293
- const projectRunning = runningProcesses.filter(
294
- (p: any) => p.projectPath === project.path
295
- );
296
- const hasRunningScripts = projectRunning.length > 0;
297
-
298
- return (
299
- <Text key={project.id} color={isSelected ? colors.accentCyan : colors.textPrimary} bold={isSelected}>
300
- {isSelected ? '▶ ' : ' '}
301
- {hasRunningScripts && <Text color={colors.accentGreen}>● </Text>}
302
- {truncateText(project.name, 30)}
303
- {hasRunningScripts && <Text color={colors.accentGreen}> ({projectRunning.length})</Text>}
304
- </Text>
305
- );
729
+ const isSelected = index === selectedIndex;
730
+
731
+ // Check if this project has running scripts
732
+ const projectRunning = runningProcesses.filter(
733
+ (p: any) => p.projectPath === project.path
734
+ );
735
+ const hasRunningScripts = projectRunning.length > 0;
736
+
737
+ // Get git branch
738
+ const branch = gitBranches.get(project.id);
739
+ const isMainBranch = branch === 'main' || branch === 'master';
740
+
741
+ return (
742
+ <Box key={project.id} flexDirection="column">
743
+ <Text color={isSelected ? colors.accentCyan : colors.textPrimary} bold={isSelected}>
744
+ {isSelected ? '▶ ' : ' '}
745
+ {hasRunningScripts && <Text color={colors.accentGreen}>● </Text>}
746
+ {truncateText(project.name, 22)}
747
+ {hasRunningScripts && <Text color={colors.accentGreen}> ({projectRunning.length})</Text>}
748
+ </Text>
749
+ {branch && (
750
+ <Text color={colors.textTertiary}>
751
+ {' '}
752
+ <Text color={isMainBranch ? colors.accentGreen : colors.accentBlue}>
753
+ {truncateText(branch, 18)}
754
+ </Text>
755
+ </Text>
756
+ )}
757
+ </Box>
758
+ );
306
759
  })}
307
760
  {hasMoreBelow && (
308
761
  <Text color={colors.textTertiary}>↓ {projects.length - endIndex} more below</Text>
309
762
  )}
310
763
  </>
311
- )}
764
+ )}
312
765
  </Box>
313
766
  </Box>
314
767
  );
@@ -326,11 +779,12 @@ interface ProjectDetailsProps {
326
779
  onTagRemove?: (tag: string) => void;
327
780
  height: number;
328
781
  scrollOffset: number;
782
+ gitBranch: string | null;
329
783
  }
330
784
 
331
- const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
332
- project,
333
- runningProcesses,
785
+ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
786
+ project,
787
+ runningProcesses,
334
788
  isFocused,
335
789
  editingName,
336
790
  editingDescription,
@@ -340,15 +794,18 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
340
794
  onTagRemove,
341
795
  height,
342
796
  scrollOffset,
797
+ gitBranch,
343
798
  }) => {
344
799
  const { focus } = useFocus({ id: 'projectDetails' });
345
800
  const [scripts, setScripts] = useState<any>(null);
346
801
  const [ports, setPorts] = useState<any[]>([]);
802
+ const [npmPackage, setNpmPackage] = useState<string | null>(null);
347
803
 
348
804
  useEffect(() => {
349
805
  if (!project) {
350
806
  setScripts(null);
351
807
  setPorts([]);
808
+ setNpmPackage(null);
352
809
  return;
353
810
  }
354
811
 
@@ -368,6 +825,25 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
368
825
  } catch (error) {
369
826
  setPorts([]);
370
827
  }
828
+
829
+ // Check if npm package (live registry check)
830
+ setNpmPackage(null);
831
+ try {
832
+ const packageJsonPath = path.join(project.path, 'package.json');
833
+ if (fs.existsSync(packageJsonPath)) {
834
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
835
+ if (pkg.name && !pkg.private) {
836
+ fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`, {
837
+ method: 'HEAD',
838
+ signal: AbortSignal.timeout(2000),
839
+ })
840
+ .then(res => {
841
+ if (res.ok) setNpmPackage(pkg.name);
842
+ })
843
+ .catch(() => {});
844
+ }
845
+ }
846
+ } catch {}
371
847
  }, [project]);
372
848
 
373
849
  if (!project) {
@@ -464,19 +940,35 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
464
940
  <Text>Ports: <Text color={colors.accentCyan}>{ports.length}</Text></Text>
465
941
  <Text> | </Text>
466
942
  <Text>Scripts: <Text color={colors.accentCyan}>{scripts?.scripts?.size || 0}</Text></Text>
943
+ {npmPackage && (
944
+ <>
945
+ <Text> | </Text>
946
+ <Text>NPM: <Text color="#f85149">{npmPackage}</Text></Text>
947
+ </>
948
+ )}
467
949
  </Box>
468
950
  );
469
951
 
470
952
  contentLines.push(<Text key="spacer2"> </Text>);
471
953
 
954
+ // Git branch
955
+ if (gitBranch) {
956
+ const isMainBranch = gitBranch === 'main' || gitBranch === 'master';
957
+ contentLines.push(
958
+ <Text key="git-branch">
959
+ Branch: <Text color={isMainBranch ? colors.accentGreen : colors.accentBlue}>{gitBranch}</Text>
960
+ </Text>
961
+ );
962
+ }
963
+
472
964
  if (project.framework) {
473
965
  contentLines.push(
474
966
  <Text key="framework">
475
- Framework: <Text color={colors.accentCyan}>{project.framework}</Text>
476
- </Text>
967
+ Framework: <Text color={colors.accentCyan}>{project.framework}</Text>
968
+ </Text>
477
969
  );
478
970
  }
479
-
971
+
480
972
  contentLines.push(
481
973
  <Text key="last-scanned">Last Scanned: {lastScanned}</Text>
482
974
  );
@@ -508,14 +1000,14 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
508
1000
  contentLines.push(<Text key="spacer4"> </Text>);
509
1001
  }
510
1002
 
511
- // Scripts
1003
+ // Scripts - show all, let virtual scrolling handle visibility
512
1004
  if (scripts && scripts.scripts && scripts.scripts.size > 0) {
513
1005
  contentLines.push(
514
1006
  <Text key="scripts-header" bold>
515
1007
  Available Scripts (<Text color={colors.accentCyan}>{scripts.scripts.size}</Text>):
516
1008
  </Text>
517
1009
  );
518
- Array.from(scripts.scripts.entries() as IterableIterator<[string, any]>).slice(0, 5).forEach(([name, script]) => {
1010
+ Array.from(scripts.scripts.entries() as IterableIterator<[string, any]>).forEach(([name, script]) => {
519
1011
  contentLines.push(
520
1012
  <Text key={`script-${name}`}>
521
1013
  {' '}
@@ -525,22 +1017,17 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
525
1017
  </Text>
526
1018
  );
527
1019
  });
528
- if (scripts.scripts.size > 5) {
529
- contentLines.push(
530
- <Text key="scripts-more" color={colors.textTertiary}> ... and {scripts.scripts.size - 5} more</Text>
531
- );
532
- }
533
1020
  contentLines.push(<Text key="spacer5"> </Text>);
534
1021
  }
535
1022
 
536
- // Ports
1023
+ // Ports - show all, let virtual scrolling handle visibility
537
1024
  if (ports.length > 0) {
538
1025
  contentLines.push(
539
1026
  <Text key="ports-header" bold>
540
1027
  Detected Ports (<Text color={colors.accentCyan}>{ports.length}</Text>):
541
1028
  </Text>
542
1029
  );
543
- ports.slice(0, 5).forEach((port: any) => {
1030
+ ports.forEach((port: any) => {
544
1031
  contentLines.push(
545
1032
  <Text key={`port-${port.id}`}>
546
1033
  {' '}Port <Text color={colors.accentCyan}>{port.port}</Text>
@@ -548,11 +1035,6 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
548
1035
  </Text>
549
1036
  );
550
1037
  });
551
- if (ports.length > 5) {
552
- contentLines.push(
553
- <Text key="ports-more" color={colors.textTertiary}> ... and {ports.length - 5} more</Text>
554
- );
555
- }
556
1038
  contentLines.push(<Text key="spacer6"> </Text>);
557
1039
  }
558
1040
 
@@ -571,12 +1053,12 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
571
1053
  const hasMoreBelow = endIndex < contentLines.length;
572
1054
 
573
1055
  return (
574
- <Box
575
- flexDirection="column"
1056
+ <Box
1057
+ flexDirection="column"
576
1058
  width="65%"
577
1059
  height={height}
578
- borderStyle="round"
579
- borderColor={isFocused ? colors.accentCyan : colors.borderColor}
1060
+ borderStyle="round"
1061
+ borderColor={isFocused ? colors.accentCyan : colors.borderColor}
580
1062
  padding={1}
581
1063
  flexShrink={0}
582
1064
  flexGrow={0}
@@ -588,36 +1070,153 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
588
1070
  {visibleContent}
589
1071
  {hasMoreBelow && (
590
1072
  <Text color={colors.textTertiary}>↓ {contentLines.length - endIndex} more below</Text>
591
- )}
1073
+ )}
592
1074
  </Box>
593
1075
  </Box>
594
1076
  );
595
1077
  };
596
1078
 
597
- interface StatusBarProps {
598
- focusedPanel: 'list' | 'details';
599
- selectedProject: Project | null;
1079
+ // Terminal Output Panel for showing live logs
1080
+ interface TerminalOutputPanelProps {
1081
+ processes: any[];
1082
+ selectedPid: number | null;
1083
+ height: number;
1084
+ onSelectProcess: (pid: number) => void;
600
1085
  }
601
1086
 
602
- const StatusBar: React.FC<StatusBarProps> = ({ focusedPanel, selectedProject }) => {
603
- if (focusedPanel === 'list') {
1087
+ const TerminalOutputPanel: React.FC<TerminalOutputPanelProps> = ({
1088
+ processes,
1089
+ selectedPid,
1090
+ height,
1091
+ onSelectProcess,
1092
+ }) => {
1093
+ const [logs, setLogs] = useState<string[]>([]);
1094
+ const [scrollOffset, setScrollOffset] = useState(0);
1095
+
1096
+ // Find the selected process or default to first
1097
+ const activeProcess = selectedPid
1098
+ ? processes.find((p: any) => p.pid === selectedPid)
1099
+ : processes[0];
1100
+
1101
+ useEffect(() => {
1102
+ if (!activeProcess?.logFile) {
1103
+ setLogs(['No active process selected']);
1104
+ return;
1105
+ }
1106
+
1107
+ // Read initial logs
1108
+ try {
1109
+ if (fs.existsSync(activeProcess.logFile)) {
1110
+ const content = fs.readFileSync(activeProcess.logFile, 'utf-8');
1111
+ const lines = content.split('\n').slice(-50); // Last 50 lines
1112
+ setLogs(lines);
1113
+ setScrollOffset(Math.max(0, lines.length - 10));
1114
+ } else {
1115
+ setLogs(['Log file not found']);
1116
+ }
1117
+ } catch {
1118
+ setLogs(['Error reading logs']);
1119
+ }
1120
+
1121
+ // Watch for changes
1122
+ let watcher: fs.FSWatcher | null = null;
1123
+ try {
1124
+ watcher = fs.watch(activeProcess.logFile, () => {
1125
+ try {
1126
+ const content = fs.readFileSync(activeProcess.logFile, 'utf-8');
1127
+ const lines = content.split('\n').slice(-100);
1128
+ setLogs(lines);
1129
+ // Auto-scroll to bottom
1130
+ setScrollOffset(Math.max(0, lines.length - 10));
1131
+ } catch {
1132
+ // Ignore read errors during watch
1133
+ }
1134
+ });
1135
+ } catch {
1136
+ // Ignore watch errors
1137
+ }
1138
+
1139
+ return () => {
1140
+ if (watcher) {
1141
+ watcher.close();
1142
+ }
1143
+ };
1144
+ }, [activeProcess?.logFile, activeProcess?.pid]);
1145
+
1146
+ const visibleLines = logs.slice(scrollOffset, scrollOffset + height - 4);
1147
+ const hasMoreAbove = scrollOffset > 0;
1148
+ const hasMoreBelow = scrollOffset + height - 4 < logs.length;
1149
+
604
1150
  return (
605
- <Box flexDirection="column">
606
- <Box>
607
- <Text color={colors.accentGreen}>● API</Text>
608
- <Text color={colors.textSecondary}> | </Text>
609
- <Text color={colors.textSecondary}>Focus: </Text>
610
- <Text color={colors.accentCyan}>Projects</Text>
1151
+ <Box
1152
+ flexDirection="column"
1153
+ width="30%"
1154
+ height={height}
1155
+ borderStyle="round"
1156
+ borderColor={colors.accentGreen}
1157
+ padding={1}
1158
+ flexShrink={0}
1159
+ >
1160
+ <Box flexDirection="row" justifyContent="space-between">
1161
+ <Text bold color={colors.accentGreen}>Terminal Output</Text>
1162
+ {processes.length > 1 && (
1163
+ <Text color={colors.textTertiary}>
1164
+ [{processes.findIndex((p: any) => p.pid === activeProcess?.pid) + 1}/{processes.length}]
1165
+ </Text>
1166
+ )}
611
1167
  </Box>
612
- <Box>
1168
+ {activeProcess && (
1169
+ <Text color={colors.textSecondary}>
1170
+ {truncateText(activeProcess.scriptName, 20)} (PID: {activeProcess.pid})
1171
+ </Text>
1172
+ )}
1173
+ <Box flexDirection="column" flexGrow={1} marginTop={1}>
1174
+ {hasMoreAbove && (
1175
+ <Text color={colors.textTertiary}>↑ more above</Text>
1176
+ )}
1177
+ {visibleLines.map((line, idx) => (
1178
+ <Text key={idx} color={colors.textPrimary}>
1179
+ {truncateText(line, 40)}
1180
+ </Text>
1181
+ ))}
1182
+ {hasMoreBelow && (
1183
+ <Text color={colors.textTertiary}>↓ more below</Text>
1184
+ )}
1185
+ </Box>
1186
+ </Box>
1187
+ );
1188
+ };
1189
+
1190
+ interface StatusBarProps {
1191
+ focusedPanel: 'list' | 'details';
1192
+ selectedProject: Project | null;
1193
+ }
1194
+
1195
+ const StatusBar: React.FC<StatusBarProps> = ({ focusedPanel, selectedProject }) => {
1196
+ if (focusedPanel === 'list') {
1197
+ return (
1198
+ <Box flexDirection="column">
1199
+ <Box>
1200
+ <Text color={colors.accentGreen}>● API</Text>
1201
+ <Text color={colors.textSecondary}> | </Text>
1202
+ <Text color={colors.textSecondary}>Focus: </Text>
1203
+ <Text color={colors.accentCyan}>List</Text>
1204
+ </Box>
1205
+ <Box>
1206
+ <Text bold>a</Text>
1207
+ <Text color={colors.textSecondary}> Add | </Text>
613
1208
  <Text bold>/</Text>
614
1209
  <Text color={colors.textSecondary}> Search | </Text>
615
- <Text bold>↑↓/kj</Text>
616
- <Text color={colors.textSecondary}> Navigate | </Text>
617
- <Text bold>Tab/←→</Text>
1210
+ <Text bold>F</Text>
1211
+ <Text color={colors.textSecondary}> Filter | </Text>
1212
+ <Text bold>S</Text>
1213
+ <Text color={colors.textSecondary}> Sort | </Text>
1214
+ <Text bold>↑↓</Text>
1215
+ <Text color={colors.textSecondary}> Nav | </Text>
1216
+ <Text bold>Tab</Text>
618
1217
  <Text color={colors.textSecondary}> Switch | </Text>
619
- <Text bold>s</Text>
620
- <Text color={colors.textSecondary}> Scan | </Text>
1218
+ <Text bold>T</Text>
1219
+ <Text color={colors.textSecondary}> Terminal | </Text>
621
1220
  <Text bold>?</Text>
622
1221
  <Text color={colors.textSecondary}> Help | </Text>
623
1222
  <Text bold>q</Text>
@@ -638,29 +1237,21 @@ const StatusBar: React.FC<StatusBarProps> = ({ focusedPanel, selectedProject })
638
1237
  {selectedProject && (
639
1238
  <>
640
1239
  <Text color={colors.textSecondary}> | </Text>
641
- <Text color={colors.textPrimary}>{selectedProject.name}</Text>
1240
+ <Text color={colors.textPrimary}>{truncateText(selectedProject.name, 20)}</Text>
642
1241
  </>
643
1242
  )}
644
1243
  </Box>
645
1244
  <Box>
646
- <Text bold>↑↓/kj</Text>
647
- <Text color={colors.textSecondary}> Scroll | </Text>
648
1245
  <Text bold>e</Text>
649
1246
  <Text color={colors.textSecondary}> Edit | </Text>
650
1247
  <Text bold>t</Text>
651
1248
  <Text color={colors.textSecondary}> Tags | </Text>
652
1249
  <Text bold>o</Text>
653
1250
  <Text color={colors.textSecondary}> Editor | </Text>
654
- <Text bold>f</Text>
655
- <Text color={colors.textSecondary}> Files | </Text>
656
- <Text bold>u</Text>
657
- <Text color={colors.textSecondary}> URLs | </Text>
1251
+ <Text bold>r</Text>
1252
+ <Text color={colors.textSecondary}> Run | </Text>
658
1253
  <Text bold>s</Text>
659
1254
  <Text color={colors.textSecondary}> Scan | </Text>
660
- <Text bold>p</Text>
661
- <Text color={colors.textSecondary}> Ports | </Text>
662
- <Text bold>r</Text>
663
- <Text color={colors.textSecondary}> Scripts | </Text>
664
1255
  <Text bold>x</Text>
665
1256
  <Text color={colors.textSecondary}> Stop | </Text>
666
1257
  <Text bold>d</Text>
@@ -668,9 +1259,7 @@ const StatusBar: React.FC<StatusBarProps> = ({ focusedPanel, selectedProject })
668
1259
  <Text bold>Tab</Text>
669
1260
  <Text color={colors.textSecondary}> Switch | </Text>
670
1261
  <Text bold>?</Text>
671
- <Text color={colors.textSecondary}> Help | </Text>
672
- <Text bold>q</Text>
673
- <Text color={colors.textSecondary}> Quit</Text>
1262
+ <Text color={colors.textSecondary}> Help</Text>
674
1263
  </Box>
675
1264
  </Box>
676
1265
  );
@@ -705,7 +1294,17 @@ const App: React.FC = () => {
705
1294
  const [error, setError] = useState<string | null>(null);
706
1295
  const [runningProcesses, setRunningProcesses] = useState<any[]>([]);
707
1296
  const [focusedPanel, setFocusedPanel] = useState<'list' | 'details'>('list');
708
-
1297
+
1298
+ // View state
1299
+ const [currentView, setCurrentView] = useState<ViewType>('projects');
1300
+
1301
+ // Git branches
1302
+ const [gitBranches, setGitBranches] = useState<Map<number, string | null>>(new Map());
1303
+
1304
+ // Filter and sort state
1305
+ const [filterType, setFilterType] = useState<FilterType>('all');
1306
+ const [sortType, setSortType] = useState<SortType>('name-asc');
1307
+
709
1308
  // Editing state
710
1309
  const [editingName, setEditingName] = useState(false);
711
1310
  const [editingDescription, setEditingDescription] = useState(false);
@@ -713,34 +1312,70 @@ const App: React.FC = () => {
713
1312
  const [editInput, setEditInput] = useState('');
714
1313
  const [showUrls, setShowUrls] = useState(false);
715
1314
  const [allTags, setAllTags] = useState<string[]>([]);
716
-
1315
+
1316
+ // Modal state
1317
+ const [showAddProjectModal, setShowAddProjectModal] = useState(false);
1318
+ const [showConfirmDelete, setShowConfirmDelete] = useState(false);
1319
+
717
1320
  // Search state
718
1321
  const [showSearch, setShowSearch] = useState(false);
719
1322
  const [searchQuery, setSearchQuery] = useState('');
720
1323
  const [listScrollOffset, setListScrollOffset] = useState(0);
721
1324
  const [detailsScrollOffset, setDetailsScrollOffset] = useState(0);
722
-
1325
+
723
1326
  // Script selection state
724
1327
  const [showScriptModal, setShowScriptModal] = useState(false);
725
1328
  const [scriptModalData, setScriptModalData] = useState<{ scripts: Map<string, any>; projectName: string; projectPath: string } | null>(null);
726
-
1329
+
1330
+ // Workspace state
1331
+ const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
1332
+ const [selectedWorkspaceIndex, setSelectedWorkspaceIndex] = useState(0);
1333
+
1334
+ // Terminal panel state
1335
+ const [showTerminalPanel, setShowTerminalPanel] = useState(false);
1336
+ const [terminalLogs, setTerminalLogs] = useState<string[]>([]);
1337
+ const [selectedProcessPid, setSelectedProcessPid] = useState<number | null>(null);
1338
+
1339
+ // Settings state
1340
+ const [showSettings, setShowSettings] = useState(false);
1341
+
727
1342
  // Get terminal dimensions
728
1343
  const terminalHeight = process.stdout.rows || 24;
729
- const availableHeight = terminalHeight - 3; // Subtract status bar
1344
+ const availableHeight = terminalHeight - 4; // Subtract status bar (increased for view indicator)
730
1345
 
731
1346
  useEffect(() => {
732
1347
  loadProjects();
733
1348
  loadRunningProcesses();
734
1349
  loadAllTags();
735
-
736
- // Refresh running processes every 5 seconds
1350
+
1351
+ // Refresh running processes and git branches every 5 seconds
737
1352
  const interval = setInterval(() => {
738
1353
  loadRunningProcesses();
739
1354
  }, 5000);
740
-
1355
+
741
1356
  return () => clearInterval(interval);
742
1357
  }, []);
743
1358
 
1359
+ // Load git branches when projects change
1360
+ useEffect(() => {
1361
+ if (allProjects.length > 0) {
1362
+ loadGitBranches();
1363
+ }
1364
+ }, [allProjects]);
1365
+
1366
+ const loadGitBranches = async () => {
1367
+ const branches = new Map<number, string | null>();
1368
+ for (const project of allProjects) {
1369
+ try {
1370
+ const branch = getCurrentBranch(project.path);
1371
+ branches.set(project.id, branch);
1372
+ } catch {
1373
+ branches.set(project.id, null);
1374
+ }
1375
+ }
1376
+ setGitBranches(branches);
1377
+ };
1378
+
744
1379
  // Reset editing state and scroll when project changes
745
1380
  useEffect(() => {
746
1381
  setEditingName(false);
@@ -750,6 +1385,13 @@ const App: React.FC = () => {
750
1385
  setDetailsScrollOffset(0); // Reset scroll when switching projects
751
1386
  }, [selectedIndex]);
752
1387
 
1388
+ // Load workspaces when switching to workspaces view
1389
+ useEffect(() => {
1390
+ if (currentView === 'workspaces' && workspaces.length === 0) {
1391
+ loadWorkspacesFromApi();
1392
+ }
1393
+ }, [currentView]);
1394
+
753
1395
  // Update scroll offset when selected index changes
754
1396
  useEffect(() => {
755
1397
  const visibleHeight = Math.max(1, availableHeight - 3);
@@ -782,32 +1424,85 @@ const App: React.FC = () => {
782
1424
  const loadProjects = () => {
783
1425
  const loadedProjects = getAllProjects();
784
1426
  setAllProjects(loadedProjects);
785
- filterProjects(loadedProjects, searchQuery);
1427
+ applyFilterAndSort(loadedProjects, searchQuery, filterType, sortType);
786
1428
  };
787
1429
 
788
- const filterProjects = (projectsToFilter: Project[], query: string) => {
789
- if (!query.trim()) {
790
- setProjects(projectsToFilter);
791
- return;
1430
+ const applyFilterAndSort = (
1431
+ projectsToFilter: Project[],
1432
+ query: string,
1433
+ filter: FilterType,
1434
+ sort: SortType
1435
+ ) => {
1436
+ let filtered = projectsToFilter;
1437
+
1438
+ // Apply search query with filter type
1439
+ if (query.trim()) {
1440
+ const q = query.toLowerCase();
1441
+ filtered = projectsToFilter.filter(project => {
1442
+ switch (filter) {
1443
+ case 'name':
1444
+ return fuzzyMatch(q, project.name);
1445
+ case 'path':
1446
+ return fuzzyMatch(q, project.path);
1447
+ case 'tags':
1448
+ return project.tags?.some((tag: string) => fuzzyMatch(q, tag)) || false;
1449
+ case 'ports': {
1450
+ // Check if project has ports matching the query
1451
+ const db = getDatabaseManager();
1452
+ const ports = db.getProjectPorts(project.id);
1453
+ return ports.some((p: any) => p.port.toString().includes(q));
1454
+ }
1455
+ case 'running': {
1456
+ const isRunning = runningProcesses.some((p: any) => p.projectPath === project.path);
1457
+ return (q === 'running' || q === 'yes' || q === 'true') ? isRunning : !isRunning;
1458
+ }
1459
+ case 'all':
1460
+ default:
1461
+ return (
1462
+ fuzzyMatch(q, project.name) ||
1463
+ (project.description ? fuzzyMatch(q, project.description) : false) ||
1464
+ fuzzyMatch(q, project.path) ||
1465
+ project.tags?.some((tag: string) => fuzzyMatch(q, tag)) ||
1466
+ false
1467
+ );
1468
+ }
1469
+ });
792
1470
  }
793
-
794
- const filtered = projectsToFilter.filter(project => {
795
- const nameMatch = fuzzyMatch(query, project.name);
796
- const descMatch = project.description ? fuzzyMatch(query, project.description) : false;
797
- const pathMatch = fuzzyMatch(query, project.path);
798
- const tagsMatch = project.tags?.some((tag: string) => fuzzyMatch(query, tag)) || false;
799
-
800
- return nameMatch || descMatch || pathMatch || tagsMatch;
1471
+
1472
+ // Apply sorting
1473
+ const sorted = [...filtered].sort((a, b) => {
1474
+ switch (sort) {
1475
+ case 'name-asc':
1476
+ return a.name.localeCompare(b.name);
1477
+ case 'name-desc':
1478
+ return b.name.localeCompare(a.name);
1479
+ case 'recent':
1480
+ return (b.last_scanned || 0) - (a.last_scanned || 0);
1481
+ case 'oldest':
1482
+ return (a.created_at || 0) - (b.created_at || 0);
1483
+ case 'running': {
1484
+ const aRunning = runningProcesses.filter((p: any) => p.projectPath === a.path).length;
1485
+ const bRunning = runningProcesses.filter((p: any) => p.projectPath === b.path).length;
1486
+ return bRunning - aRunning;
1487
+ }
1488
+ default:
1489
+ return 0;
1490
+ }
801
1491
  });
802
-
803
- setProjects(filtered);
804
-
1492
+
1493
+ setProjects(sorted);
1494
+
805
1495
  // Adjust selected index if current selection is out of bounds
806
- if (selectedIndex >= filtered.length) {
807
- setSelectedIndex(Math.max(0, filtered.length - 1));
1496
+ if (selectedIndex >= sorted.length) {
1497
+ setSelectedIndex(Math.max(0, sorted.length - 1));
808
1498
  }
809
1499
  };
810
1500
 
1501
+ // Re-apply filter/sort when dependencies change
1502
+ useEffect(() => {
1503
+ applyFilterAndSort(allProjects, searchQuery, filterType, sortType);
1504
+ }, [filterType, sortType, runningProcesses]);
1505
+
811
1506
  const loadRunningProcesses = async () => {
812
1507
  try {
813
1508
  const processes = await getRunningProcessesClean();
@@ -937,11 +1632,11 @@ const App: React.FC = () => {
937
1632
  // Handler for script selection
938
1633
  const handleScriptSelect = async (scriptName: string, background: boolean) => {
939
1634
  if (!scriptModalData) return;
940
-
1635
+
941
1636
  setShowScriptModal(false);
942
1637
  setIsLoading(true);
943
1638
  setLoadingMessage(`Running ${scriptName}${background ? ' in background' : ''}...`);
944
-
1639
+
945
1640
  try {
946
1641
  if (background) {
947
1642
  await runScriptInBackground(scriptModalData.projectPath, scriptModalData.projectName, scriptName, [], false);
@@ -959,40 +1654,117 @@ const App: React.FC = () => {
959
1654
  }
960
1655
  };
961
1656
 
1657
+ // Handler for adding a project
1658
+ const handleAddProject = async (projectPath: string, projectName?: string) => {
1659
+ setShowAddProjectModal(false);
1660
+ setIsLoading(true);
1661
+ setLoadingMessage('Adding project...');
1662
+
1663
+ try {
1664
+ const name = projectName || path.basename(projectPath);
1665
+ const db = getDatabaseManager();
1666
+ const project = db.addProject(name, projectPath);
1667
+
1668
+ // Scan for tests
1669
+ setLoadingMessage('Scanning for tests...');
1670
+ await scanProject(project.id);
1671
+
1672
+ // Scan for ports
1673
+ setLoadingMessage('Scanning for ports...');
1674
+ try {
1675
+ const { scanProjectPorts } = await import('./port-scanner');
1676
+ await scanProjectPorts(project.id);
1677
+ } catch {
1678
+ // Ignore port scanning errors
1679
+ }
1680
+
1681
+ loadProjects();
1682
+ setIsLoading(false);
1683
+
1684
+ // Select the newly added project
1685
+ const newProjects = getAllProjects();
1686
+ const newIndex = newProjects.findIndex((p: Project) => p.id === project.id);
1687
+ if (newIndex >= 0) {
1688
+ setSelectedIndex(newIndex);
1689
+ }
1690
+ } catch (err) {
1691
+ setIsLoading(false);
1692
+ setError(err instanceof Error ? err.message : String(err));
1693
+ }
1694
+ };
1695
+
1696
+ // Handler for deleting a project
1697
+ const handleDeleteProject = () => {
1698
+ if (!selectedProject) return;
1699
+
1700
+ setShowConfirmDelete(false);
1701
+ setIsLoading(true);
1702
+ setLoadingMessage(`Deleting ${selectedProject.name}...`);
1703
+
1704
+ setTimeout(async () => {
1705
+ try {
1706
+ const db = getDatabaseManager();
1707
+ db.removeProject(selectedProject.id);
1708
+ loadProjects();
1709
+ if (selectedIndex >= projects.length - 1) {
1710
+ setSelectedIndex(Math.max(0, projects.length - 2));
1711
+ }
1712
+ setIsLoading(false);
1713
+ } catch (err) {
1714
+ setIsLoading(false);
1715
+ setError(err instanceof Error ? err.message : String(err));
1716
+ }
1717
+ }, 100);
1718
+ };
1719
+
1720
+ // Cycle filter type
1721
+ const cycleFilterType = () => {
1722
+ const currentIndex = FILTER_TYPES.indexOf(filterType);
1723
+ const nextIndex = (currentIndex + 1) % FILTER_TYPES.length;
1724
+ setFilterType(FILTER_TYPES[nextIndex]);
1725
+ };
1726
+
1727
+ // Cycle sort type
1728
+ const cycleSortType = () => {
1729
+ const currentIndex = SORT_TYPES.indexOf(sortType);
1730
+ const nextIndex = (currentIndex + 1) % SORT_TYPES.length;
1731
+ setSortType(SORT_TYPES[nextIndex]);
1732
+ };
1733
+
962
1734
  useInput((input: string, key: any) => {
963
1735
  // Handle search mode
964
1736
  if (showSearch) {
965
1737
  if (key.escape) {
966
1738
  setShowSearch(false);
967
1739
  setSearchQuery('');
968
- filterProjects(allProjects, '');
1740
+ applyFilterAndSort(allProjects, '', filterType, sortType);
969
1741
  return;
970
1742
  }
971
-
1743
+
972
1744
  if (key.return) {
973
1745
  setShowSearch(false);
974
1746
  return;
975
1747
  }
976
-
1748
+
977
1749
  if (key.backspace || key.delete) {
978
1750
  const newQuery = searchQuery.slice(0, -1);
979
1751
  setSearchQuery(newQuery);
980
- filterProjects(allProjects, newQuery);
1752
+ applyFilterAndSort(allProjects, newQuery, filterType, sortType);
981
1753
  return;
982
1754
  }
983
1755
 
984
1756
  if (input && input.length === 1 && !key.ctrl && !key.meta) {
985
1757
  const newQuery = searchQuery + input;
986
1758
  setSearchQuery(newQuery);
987
- filterProjects(allProjects, newQuery);
1759
+ applyFilterAndSort(allProjects, newQuery, filterType, sortType);
988
1760
  return;
989
1761
  }
990
-
1762
+
991
1763
  return;
992
1764
  }
993
-
1765
+
994
1766
  // Don't process input if modal is showing
995
- if (showHelp || isLoading || error || showUrls || showScriptModal) {
1767
+ if (showHelp || isLoading || error || showUrls || showScriptModal || showAddProjectModal || showConfirmDelete || showSettings) {
996
1768
  // Handle URLs modal
997
1769
  if (showUrls && (key.escape || key.return || input === 'q' || input === 'u')) {
998
1770
  setShowUrls(false);
@@ -1001,6 +1773,65 @@ const App: React.FC = () => {
1001
1773
  return;
1002
1774
  }
1003
1775
 
1776
+ // Handle navigation in workspaces view
1777
+ if (currentView === 'workspaces') {
1778
+ if (key.upArrow || input === 'k') {
1779
+ setSelectedWorkspaceIndex((prev) => Math.max(0, prev - 1));
1780
+ return;
1781
+ }
1782
+ if (key.downArrow || input === 'j') {
1783
+ setSelectedWorkspaceIndex((prev) => Math.min(workspaces.length - 1, prev + 1));
1784
+ return;
1785
+ }
1786
+ }
1787
+
1788
+ // Handle navigation in processes view
1789
+ if (currentView === 'processes') {
1790
+ if (input === 'x' && runningProcesses.length > 0) {
1791
+ // Stop all processes (or could select one)
1792
+ setIsLoading(true);
1793
+ setLoadingMessage('Stopping processes...');
1794
+ setTimeout(async () => {
1795
+ try {
1796
+ for (const proc of runningProcesses) {
1797
+ await stopScript(proc.pid);
1798
+ }
1799
+ await loadRunningProcesses();
1800
+ setIsLoading(false);
1801
+ } catch (err) {
1802
+ setIsLoading(false);
1803
+ setError(err instanceof Error ? err.message : String(err));
1804
+ }
1805
+ }, 100);
1806
+ return;
1807
+ }
1808
+ }
1809
+
1810
+ // Global navigation - number keys for view switching
1811
+ if (input === '1') {
1812
+ setCurrentView('projects');
1813
+ return;
1814
+ }
1815
+ if (input === '2') {
1816
+ setCurrentView('workspaces');
1817
+ return;
1818
+ }
1819
+ if (input === '3') {
1820
+ setCurrentView('processes');
1821
+ return;
1822
+ }
1823
+ if (input === '4') {
1824
+ setCurrentView('settings');
1825
+ setShowSettings(true);
1826
+ return;
1827
+ }
1828
+
1829
+ // Terminal panel toggle
1830
+ if (input === 'T') {
1831
+ setShowTerminalPanel(prev => !prev);
1832
+ return;
1833
+ }
1834
+
1004
1835
  // Search shortcut
1005
1836
  if (input === '/') {
1006
1837
  setShowSearch(true);
@@ -1008,6 +1839,38 @@ const App: React.FC = () => {
1008
1839
  return;
1009
1840
  }
1010
1841
 
1842
+ // Add project shortcut (in projects view)
1843
+ if (input === 'a' && currentView === 'projects') {
1844
+ setShowAddProjectModal(true);
1845
+ return;
1846
+ }
1847
+
1848
+ // Filter cycle shortcut
1849
+ if (input === 'F' && currentView === 'projects') {
1850
+ cycleFilterType();
1851
+ return;
1852
+ }
1853
+
1854
+ // Sort cycle shortcut
1855
+ if (input === 'S' && currentView === 'projects') {
1856
+ cycleSortType();
1857
+ return;
1858
+ }
1859
+
1860
+ // Refresh git branches
1861
+ if (input === 'g' && currentView === 'projects') {
1862
+ loadGitBranches();
1863
+ return;
1864
+ }
1865
+
1866
+ // Full refresh
1867
+ if (input === 'R') {
1868
+ loadProjects();
1869
+ loadRunningProcesses();
1870
+ loadGitBranches();
1871
+ return;
1872
+ }
1873
+
1011
1874
  // Handle editing modes
1012
1875
  if (editingName || editingDescription || editingTags) {
1013
1876
  if (key.escape) {
@@ -1176,24 +2039,9 @@ const App: React.FC = () => {
1176
2039
  return;
1177
2040
  }
1178
2041
 
1179
- // Delete project
2042
+ // Delete project (with confirmation)
1180
2043
  if (input === 'd') {
1181
- setIsLoading(true);
1182
- setLoadingMessage(`Deleting ${selectedProject.name}...`);
1183
- setTimeout(async () => {
1184
- try {
1185
- const db = getDatabaseManager();
1186
- db.removeProject(selectedProject.id);
1187
- loadProjects();
1188
- if (selectedIndex >= projects.length - 1) {
1189
- setSelectedIndex(Math.max(0, projects.length - 2));
1190
- }
1191
- setIsLoading(false);
1192
- } catch (err) {
1193
- setIsLoading(false);
1194
- setError(err instanceof Error ? err.message : String(err));
1195
- }
1196
- }, 100);
2044
+ setShowConfirmDelete(true);
1197
2045
  return;
1198
2046
  }
1199
2047
  }
@@ -1295,6 +2143,17 @@ const App: React.FC = () => {
1295
2143
  );
1296
2144
  }
1297
2145
 
2146
+ if (showSettings) {
2147
+ return (
2148
+ <Box flexDirection="column" padding={1}>
2149
+ <SettingsModal onClose={() => {
2150
+ setShowSettings(false);
2151
+ setCurrentView('projects');
2152
+ }} />
2153
+ </Box>
2154
+ );
2155
+ }
2156
+
1298
2157
  if (isLoading) {
1299
2158
  return (
1300
2159
  <Box flexDirection="column" padding={1}>
@@ -1375,6 +2234,30 @@ const App: React.FC = () => {
1375
2234
  );
1376
2235
  }
1377
2236
 
2237
+ if (showAddProjectModal) {
2238
+ return (
2239
+ <Box flexDirection="column" padding={1}>
2240
+ <AddProjectModal
2241
+ onAdd={handleAddProject}
2242
+ onCancel={() => setShowAddProjectModal(false)}
2243
+ />
2244
+ </Box>
2245
+ );
2246
+ }
2247
+
2248
+ if (showConfirmDelete && selectedProject) {
2249
+ return (
2250
+ <Box flexDirection="column" padding={1}>
2251
+ <ConfirmModal
2252
+ title="Delete Project"
2253
+ message={`Are you sure you want to remove "${selectedProject.name}" from the dashboard?`}
2254
+ onConfirm={handleDeleteProject}
2255
+ onCancel={() => setShowConfirmDelete(false)}
2256
+ />
2257
+ </Box>
2258
+ );
2259
+ }
2260
+
1378
2261
  if (showScriptModal && scriptModalData) {
1379
2262
  return (
1380
2263
  <Box flexDirection="column" padding={1}>
@@ -1403,33 +2286,205 @@ const App: React.FC = () => {
1403
2286
  }
1404
2287
  };
1405
2288
 
1406
- return (
1407
- <Box flexDirection="column" height={terminalHeight}>
2289
+ // Render Projects view
2290
+ const renderProjectsView = () => (
2291
+ <Box flexDirection="row" height={availableHeight} flexGrow={0} flexShrink={0}>
2292
+ <ProjectListComponent
2293
+ projects={projects}
2294
+ selectedIndex={selectedIndex}
2295
+ runningProcesses={runningProcesses}
2296
+ isFocused={focusedPanel === 'list'}
2297
+ height={availableHeight}
2298
+ scrollOffset={listScrollOffset}
2299
+ gitBranches={gitBranches}
2300
+ filterType={filterType}
2301
+ sortType={sortType}
2302
+ />
2303
+ <Box width={1} />
2304
+ <ProjectDetailsComponent
2305
+ project={selectedProject}
2306
+ runningProcesses={runningProcesses}
2307
+ isFocused={focusedPanel === 'details'}
2308
+ editingName={editingName}
2309
+ editingDescription={editingDescription}
2310
+ editingTags={editingTags}
2311
+ editInput={editInput}
2312
+ allTags={allTags}
2313
+ onTagRemove={handleTagRemove}
2314
+ height={availableHeight}
2315
+ scrollOffset={detailsScrollOffset}
2316
+ gitBranch={selectedProject ? gitBranches.get(selectedProject.id) || null : null}
2317
+ />
2318
+ {showTerminalPanel && (
2319
+ <>
2320
+ <Box width={1} />
2321
+ <TerminalOutputPanel
2322
+ processes={runningProcesses}
2323
+ selectedPid={selectedProcessPid}
2324
+ height={availableHeight}
2325
+ onSelectProcess={(pid) => setSelectedProcessPid(pid)}
2326
+ />
2327
+ </>
2328
+ )}
2329
+ </Box>
2330
+ );
2331
+
2332
+ // Render Workspaces view
2333
+ const renderWorkspacesView = () => (
1408
2334
  <Box flexDirection="row" height={availableHeight} flexGrow={0} flexShrink={0}>
1409
- <ProjectListComponent
1410
- projects={projects}
1411
- selectedIndex={selectedIndex}
1412
- runningProcesses={runningProcesses}
1413
- isFocused={focusedPanel === 'list'}
2335
+ {/* Workspace List */}
2336
+ <Box
2337
+ flexDirection="column"
2338
+ width="35%"
1414
2339
  height={availableHeight}
1415
- scrollOffset={listScrollOffset}
1416
- />
2340
+ borderStyle="round"
2341
+ borderColor={colors.accentCyan}
2342
+ padding={1}
2343
+ >
2344
+ <Text bold color={colors.textPrimary}>
2345
+ Workspaces ({workspaces.length})
2346
+ </Text>
2347
+ <Box flexDirection="column" flexGrow={1}>
2348
+ {workspaces.length === 0 ? (
2349
+ <Text color={colors.textTertiary}>No workspaces found</Text>
2350
+ ) : (
2351
+ workspaces.map((ws, index) => {
2352
+ const isSelected = index === selectedWorkspaceIndex;
2353
+ return (
2354
+ <Text key={ws.id} color={isSelected ? colors.accentCyan : colors.textPrimary} bold={isSelected}>
2355
+ {isSelected ? '▶ ' : ' '}{truncateText(ws.name, 25)}
2356
+ </Text>
2357
+ );
2358
+ })
2359
+ )}
2360
+ </Box>
2361
+ </Box>
1417
2362
  <Box width={1} />
1418
- <ProjectDetailsComponent
1419
- project={selectedProject}
1420
- runningProcesses={runningProcesses}
1421
- isFocused={focusedPanel === 'details'}
1422
- editingName={editingName}
1423
- editingDescription={editingDescription}
1424
- editingTags={editingTags}
1425
- editInput={editInput}
1426
- allTags={allTags}
1427
- onTagRemove={handleTagRemove}
2363
+ {/* Workspace Details */}
2364
+ <Box
2365
+ flexDirection="column"
2366
+ width="65%"
1428
2367
  height={availableHeight}
1429
- scrollOffset={detailsScrollOffset}
1430
- />
2368
+ borderStyle="round"
2369
+ borderColor={colors.borderColor}
2370
+ padding={1}
2371
+ >
2372
+ {workspaces[selectedWorkspaceIndex] ? (
2373
+ <>
2374
+ <Text bold color={colors.accentCyan}>
2375
+ {workspaces[selectedWorkspaceIndex].name}
2376
+ </Text>
2377
+ <Text> </Text>
2378
+ {workspaces[selectedWorkspaceIndex].description && (
2379
+ <Text color={colors.textSecondary}>
2380
+ {workspaces[selectedWorkspaceIndex].description}
2381
+ </Text>
2382
+ )}
2383
+ <Text> </Text>
2384
+ <Text color={colors.textTertiary}>
2385
+ Path: {getDisplayPath(workspaces[selectedWorkspaceIndex].workspace_file_path)}
2386
+ </Text>
2387
+ </>
2388
+ ) : (
2389
+ <Text color={colors.textTertiary}>Select a workspace</Text>
2390
+ )}
2391
+ </Box>
1431
2392
  </Box>
1432
-
2393
+ );
2394
+
2395
+ // Load workspaces from API
2396
+ const loadWorkspacesFromApi = async () => {
2397
+ try {
2398
+ // Try common API ports
2399
+ const ports = [38124, 38125, 38126, 38127, 38128, 3001];
2400
+ let apiBaseUrl = '';
2401
+
2402
+ for (const port of ports) {
2403
+ try {
2404
+ const response = await fetch(`http://localhost:${port}/health`, {
2405
+ signal: AbortSignal.timeout(500),
2406
+ });
2407
+ if (response.ok) {
2408
+ apiBaseUrl = `http://localhost:${port}/api`;
2409
+ break;
2410
+ }
2411
+ } catch {
2412
+ continue;
2413
+ }
2414
+ }
2415
+
2416
+ if (!apiBaseUrl) {
2417
+ return;
2418
+ }
2419
+
2420
+ const response = await fetch(`${apiBaseUrl}/workspaces`);
2421
+ if (response.ok) {
2422
+ const ws = (await response.json()) as Workspace[];
2423
+ setWorkspaces(ws);
2424
+ }
2425
+ } catch {
2426
+ // Ignore workspace loading errors
2427
+ }
2428
+ };
2429
+
2430
+ // Render Processes view placeholder
2431
+ const renderProcessesView = () => (
2432
+ <Box flexDirection="column" padding={2}>
2433
+ <Text bold color={colors.accentCyan}>Running Processes ({runningProcesses.length})</Text>
2434
+ <Text> </Text>
2435
+ {runningProcesses.length === 0 ? (
2436
+ <Text color={colors.textTertiary}>No running processes</Text>
2437
+ ) : (
2438
+ runningProcesses.map((proc: any) => {
2439
+ const uptime = Math.floor((Date.now() - proc.startedAt) / 1000);
2440
+ const minutes = Math.floor(uptime / 60);
2441
+ const seconds = uptime % 60;
2442
+ const uptimeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
2443
+ return (
2444
+ <Text key={proc.pid} color={colors.textPrimary}>
2445
+ <Text color={colors.accentGreen}>●</Text> PID {proc.pid}: {proc.projectName} ({proc.scriptName}) - {uptimeStr}
2446
+ </Text>
2447
+ );
2448
+ })
2449
+ )}
2450
+ <Text> </Text>
2451
+ <Text color={colors.textTertiary}>Press 1 to return to Projects</Text>
2452
+ </Box>
2453
+ );
2454
+
2455
+ return (
2456
+ <Box flexDirection="column" height={terminalHeight}>
2457
+ {/* View indicator bar */}
2458
+ <Box paddingX={1} height={1}>
2459
+ <Text color={currentView === 'projects' ? colors.accentCyan : colors.textTertiary}>
2460
+ [1] Projects
2461
+ </Text>
2462
+ <Text> </Text>
2463
+ <Text color={currentView === 'workspaces' ? colors.accentCyan : colors.textTertiary}>
2464
+ [2] Workspaces
2465
+ </Text>
2466
+ <Text> </Text>
2467
+ <Text color={currentView === 'processes' ? colors.accentCyan : colors.textTertiary}>
2468
+ [3] Processes
2469
+ </Text>
2470
+ <Text> </Text>
2471
+ <Text color={currentView === 'settings' ? colors.accentCyan : colors.textTertiary}>
2472
+ [4] Settings
2473
+ </Text>
2474
+ {showTerminalPanel && (
2475
+ <>
2476
+ <Text> | </Text>
2477
+ <Text color={colors.accentGreen}>Terminal [T]</Text>
2478
+ </>
2479
+ )}
2480
+ </Box>
2481
+
2482
+ {/* Main content based on current view */}
2483
+ {currentView === 'projects' && renderProjectsView()}
2484
+ {currentView === 'workspaces' && renderWorkspacesView()}
2485
+ {currentView === 'processes' && renderProcessesView()}
2486
+
2487
+ {/* Status bar */}
1433
2488
  <Box paddingX={1} borderStyle="single" borderColor={colors.borderColor} flexShrink={0} height={3}>
1434
2489
  <StatusBar focusedPanel={focusedPanel} selectedProject={selectedProject} />
1435
2490
  </Box>