projax 3.3.58 → 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.
- package/dist/electron/script-runner.js +52 -20
- package/dist/index.js +1 -1
- package/dist/prxi.js +844 -109
- package/dist/prxi.tsx +1234 -179
- package/dist/script-runner.js +52 -20
- package/package.json +1 -1
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/core-bridge.ts.html +0 -292
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -191
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/core-bridge.ts.html +0 -292
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -191
- package/coverage/lcov-report/port-extractor.ts.html +0 -1174
- package/coverage/lcov-report/port-scanner.ts.html +0 -301
- package/coverage/lcov-report/port-utils.ts.html +0 -670
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/script-runner.ts.html +0 -3346
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/test-parser.ts.html +0 -799
- package/coverage/lcov.info +0 -1338
- package/coverage/port-extractor.ts.html +0 -1174
- package/coverage/port-scanner.ts.html +0 -301
- package/coverage/port-utils.ts.html +0 -670
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/script-runner.ts.html +0 -3346
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/test-parser.ts.html +0 -799
- package/dist/__tests__/core-bridge.test.d.ts +0 -1
- package/dist/__tests__/core-bridge.test.js +0 -135
- package/dist/__tests__/port-extractor.test.d.ts +0 -1
- package/dist/__tests__/port-extractor.test.js +0 -407
- package/dist/__tests__/port-scanner.test.d.ts +0 -1
- package/dist/__tests__/port-scanner.test.js +0 -170
- package/dist/__tests__/port-utils.test.d.ts +0 -1
- package/dist/__tests__/port-utils.test.js +0 -127
- package/dist/__tests__/script-runner.test.d.ts +0 -1
- package/dist/__tests__/script-runner.test.js +0 -491
- package/dist/__tests__/test-parser.test.d.ts +0 -1
- package/dist/__tests__/test-parser.test.js +0 -276
- package/dist/api/__tests__/database.test.d.ts +0 -2
- package/dist/api/__tests__/database.test.d.ts.map +0 -1
- package/dist/api/__tests__/database.test.js +0 -485
- package/dist/api/__tests__/database.test.js.map +0 -1
- package/dist/api/__tests__/routes.test.d.ts +0 -2
- package/dist/api/__tests__/routes.test.d.ts.map +0 -1
- package/dist/api/__tests__/routes.test.js +0 -484
- package/dist/api/__tests__/routes.test.js.map +0 -1
- package/dist/api/__tests__/scanner.test.d.ts +0 -2
- package/dist/api/__tests__/scanner.test.d.ts.map +0 -1
- package/dist/api/__tests__/scanner.test.js +0 -403
- package/dist/api/__tests__/scanner.test.js.map +0 -1
- package/dist/core/__tests__/database.test.d.ts +0 -1
- package/dist/core/__tests__/database.test.js +0 -557
- package/dist/core/__tests__/detector.test.d.ts +0 -1
- package/dist/core/__tests__/detector.test.js +0 -375
- package/dist/core/__tests__/index.test.d.ts +0 -1
- package/dist/core/__tests__/index.test.js +0 -469
- package/dist/core/__tests__/scanner.test.d.ts +0 -1
- package/dist/core/__tests__/scanner.test.js +0 -406
- package/dist/core/__tests__/settings.test.d.ts +0 -1
- package/dist/core/__tests__/settings.test.js +0 -280
- package/dist/electron/core/__tests__/database.test.d.ts +0 -1
- package/dist/electron/core/__tests__/database.test.js +0 -557
- package/dist/electron/core/__tests__/detector.test.d.ts +0 -1
- package/dist/electron/core/__tests__/detector.test.js +0 -375
- package/dist/electron/core/__tests__/index.test.d.ts +0 -1
- package/dist/electron/core/__tests__/index.test.js +0 -469
- package/dist/electron/core/__tests__/scanner.test.d.ts +0 -1
- package/dist/electron/core/__tests__/scanner.test.js +0 -406
- package/dist/electron/core/__tests__/settings.test.d.ts +0 -1
- package/dist/electron/core/__tests__/settings.test.js +0 -280
- 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
25
|
} from './core-bridge';
|
|
9
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={
|
|
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>
|
|
77
|
-
<Text>
|
|
78
|
-
<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}>
|
|
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>
|
|
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}>
|
|
85
|
-
<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
|
|
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}>> </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}>> </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
|
-
<
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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]>).
|
|
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.
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
603
|
-
|
|
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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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>
|
|
620
|
-
<Text color={colors.textSecondary}>
|
|
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>
|
|
655
|
-
<Text color={colors.textSecondary}>
|
|
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
|
|
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 -
|
|
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
|
-
|
|
1427
|
+
applyFilterAndSort(loadedProjects, searchQuery, filterType, sortType);
|
|
786
1428
|
};
|
|
787
1429
|
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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(
|
|
804
|
-
|
|
1492
|
+
|
|
1493
|
+
setProjects(sorted);
|
|
1494
|
+
|
|
805
1495
|
// Adjust selected index if current selection is out of bounds
|
|
806
|
-
if (selectedIndex >=
|
|
807
|
-
setSelectedIndex(Math.max(0,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1407
|
-
|
|
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
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
isFocused={focusedPanel === 'list'}
|
|
2335
|
+
{/* Workspace List */}
|
|
2336
|
+
<Box
|
|
2337
|
+
flexDirection="column"
|
|
2338
|
+
width="35%"
|
|
1414
2339
|
height={availableHeight}
|
|
1415
|
-
|
|
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
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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
|
-
|
|
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>
|