projax 3.3.58 → 3.3.63
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/README.md +10 -1
- package/dist/electron/preload.d.ts +1 -0
- package/dist/electron/renderer/assets/index-CmtZriN5.js +66 -0
- package/dist/electron/renderer/index.html +1 -1
- package/dist/electron/script-runner.js +52 -20
- package/dist/index.js +14 -10
- package/dist/prxi.js +877 -109
- package/dist/prxi.tsx +1249 -177
- 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,215 @@ 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
|
+
|
|
129
406
|
interface ErrorModalProps {
|
|
130
407
|
message: string;
|
|
131
408
|
onClose: () => void;
|
|
@@ -240,15 +517,21 @@ interface ProjectListProps {
|
|
|
240
517
|
isFocused: boolean;
|
|
241
518
|
height: number;
|
|
242
519
|
scrollOffset: number;
|
|
520
|
+
gitBranches: Map<number, string | null>;
|
|
521
|
+
filterType: FilterType;
|
|
522
|
+
sortType: SortType;
|
|
243
523
|
}
|
|
244
524
|
|
|
245
|
-
const ProjectListComponent: React.FC<ProjectListProps> = ({
|
|
246
|
-
projects,
|
|
247
|
-
selectedIndex,
|
|
248
|
-
runningProcesses,
|
|
525
|
+
const ProjectListComponent: React.FC<ProjectListProps> = ({
|
|
526
|
+
projects,
|
|
527
|
+
selectedIndex,
|
|
528
|
+
runningProcesses,
|
|
249
529
|
isFocused,
|
|
250
530
|
height,
|
|
251
531
|
scrollOffset,
|
|
532
|
+
gitBranches,
|
|
533
|
+
filterType,
|
|
534
|
+
sortType,
|
|
252
535
|
}) => {
|
|
253
536
|
const { focus } = useFocus({ id: 'projectList' });
|
|
254
537
|
|
|
@@ -264,51 +547,72 @@ const ProjectListComponent: React.FC<ProjectListProps> = ({
|
|
|
264
547
|
const hasMoreBelow = endIndex < projects.length;
|
|
265
548
|
|
|
266
549
|
return (
|
|
267
|
-
<Box
|
|
268
|
-
flexDirection="column"
|
|
269
|
-
width="35%"
|
|
550
|
+
<Box
|
|
551
|
+
flexDirection="column"
|
|
552
|
+
width="35%"
|
|
270
553
|
height={height}
|
|
271
|
-
borderStyle="round"
|
|
272
|
-
borderColor={isFocused ? colors.accentCyan : colors.borderColor}
|
|
554
|
+
borderStyle="round"
|
|
555
|
+
borderColor={isFocused ? colors.accentCyan : colors.borderColor}
|
|
273
556
|
padding={1}
|
|
274
557
|
flexShrink={0}
|
|
275
558
|
flexGrow={0}
|
|
276
559
|
>
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
|
|
560
|
+
<Box flexDirection="column">
|
|
561
|
+
<Text bold color={colors.textPrimary}>
|
|
562
|
+
Projects ({projects.length})
|
|
563
|
+
</Text>
|
|
564
|
+
<Text color={colors.textTertiary}>
|
|
565
|
+
<Text color={colors.accentPurple}>{FILTER_LABELS[filterType]}</Text>
|
|
566
|
+
{' | '}
|
|
567
|
+
<Text color={colors.accentOrange}>{SORT_LABELS[sortType]}</Text>
|
|
568
|
+
</Text>
|
|
569
|
+
</Box>
|
|
280
570
|
<Box flexDirection="column" flexGrow={1}>
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
571
|
+
{projects.length === 0 ? (
|
|
572
|
+
<Text color={colors.textTertiary}>No projects found</Text>
|
|
573
|
+
) : (
|
|
284
574
|
<>
|
|
285
575
|
{hasMoreAbove && (
|
|
286
576
|
<Text color={colors.textTertiary}>↑ {startIndex} more above</Text>
|
|
287
577
|
)}
|
|
288
578
|
{visibleProjects.map((project, localIndex) => {
|
|
289
579
|
const index = startIndex + localIndex;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
580
|
+
const isSelected = index === selectedIndex;
|
|
581
|
+
|
|
582
|
+
// Check if this project has running scripts
|
|
583
|
+
const projectRunning = runningProcesses.filter(
|
|
584
|
+
(p: any) => p.projectPath === project.path
|
|
585
|
+
);
|
|
586
|
+
const hasRunningScripts = projectRunning.length > 0;
|
|
587
|
+
|
|
588
|
+
// Get git branch
|
|
589
|
+
const branch = gitBranches.get(project.id);
|
|
590
|
+
const isMainBranch = branch === 'main' || branch === 'master';
|
|
591
|
+
|
|
592
|
+
return (
|
|
593
|
+
<Box key={project.id} flexDirection="column">
|
|
594
|
+
<Text color={isSelected ? colors.accentCyan : colors.textPrimary} bold={isSelected}>
|
|
595
|
+
{isSelected ? '▶ ' : ' '}
|
|
596
|
+
{hasRunningScripts && <Text color={colors.accentGreen}>● </Text>}
|
|
597
|
+
{truncateText(project.name, 22)}
|
|
598
|
+
{hasRunningScripts && <Text color={colors.accentGreen}> ({projectRunning.length})</Text>}
|
|
599
|
+
</Text>
|
|
600
|
+
{branch && (
|
|
601
|
+
<Text color={colors.textTertiary}>
|
|
602
|
+
{' '}
|
|
603
|
+
<Text color={isMainBranch ? colors.accentGreen : colors.accentBlue}>
|
|
604
|
+
{truncateText(branch, 18)}
|
|
605
|
+
</Text>
|
|
606
|
+
</Text>
|
|
607
|
+
)}
|
|
608
|
+
</Box>
|
|
609
|
+
);
|
|
306
610
|
})}
|
|
307
611
|
{hasMoreBelow && (
|
|
308
612
|
<Text color={colors.textTertiary}>↓ {projects.length - endIndex} more below</Text>
|
|
309
613
|
)}
|
|
310
614
|
</>
|
|
311
|
-
|
|
615
|
+
)}
|
|
312
616
|
</Box>
|
|
313
617
|
</Box>
|
|
314
618
|
);
|
|
@@ -326,11 +630,12 @@ interface ProjectDetailsProps {
|
|
|
326
630
|
onTagRemove?: (tag: string) => void;
|
|
327
631
|
height: number;
|
|
328
632
|
scrollOffset: number;
|
|
633
|
+
gitBranch: string | null;
|
|
329
634
|
}
|
|
330
635
|
|
|
331
|
-
const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
332
|
-
project,
|
|
333
|
-
runningProcesses,
|
|
636
|
+
const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
637
|
+
project,
|
|
638
|
+
runningProcesses,
|
|
334
639
|
isFocused,
|
|
335
640
|
editingName,
|
|
336
641
|
editingDescription,
|
|
@@ -340,15 +645,18 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
|
340
645
|
onTagRemove,
|
|
341
646
|
height,
|
|
342
647
|
scrollOffset,
|
|
648
|
+
gitBranch,
|
|
343
649
|
}) => {
|
|
344
650
|
const { focus } = useFocus({ id: 'projectDetails' });
|
|
345
651
|
const [scripts, setScripts] = useState<any>(null);
|
|
346
652
|
const [ports, setPorts] = useState<any[]>([]);
|
|
653
|
+
const [npmPackage, setNpmPackage] = useState<string | null>(null);
|
|
347
654
|
|
|
348
655
|
useEffect(() => {
|
|
349
656
|
if (!project) {
|
|
350
657
|
setScripts(null);
|
|
351
658
|
setPorts([]);
|
|
659
|
+
setNpmPackage(null);
|
|
352
660
|
return;
|
|
353
661
|
}
|
|
354
662
|
|
|
@@ -368,6 +676,25 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
|
368
676
|
} catch (error) {
|
|
369
677
|
setPorts([]);
|
|
370
678
|
}
|
|
679
|
+
|
|
680
|
+
// Check if npm package (live registry check)
|
|
681
|
+
setNpmPackage(null);
|
|
682
|
+
try {
|
|
683
|
+
const packageJsonPath = path.join(project.path, 'package.json');
|
|
684
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
685
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
686
|
+
if (pkg.name && !pkg.private) {
|
|
687
|
+
fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`, {
|
|
688
|
+
method: 'HEAD',
|
|
689
|
+
signal: AbortSignal.timeout(2000),
|
|
690
|
+
})
|
|
691
|
+
.then(res => {
|
|
692
|
+
if (res.ok) setNpmPackage(pkg.name);
|
|
693
|
+
})
|
|
694
|
+
.catch(() => {});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} catch {}
|
|
371
698
|
}, [project]);
|
|
372
699
|
|
|
373
700
|
if (!project) {
|
|
@@ -464,19 +791,35 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
|
464
791
|
<Text>Ports: <Text color={colors.accentCyan}>{ports.length}</Text></Text>
|
|
465
792
|
<Text> | </Text>
|
|
466
793
|
<Text>Scripts: <Text color={colors.accentCyan}>{scripts?.scripts?.size || 0}</Text></Text>
|
|
794
|
+
{npmPackage && (
|
|
795
|
+
<>
|
|
796
|
+
<Text> | </Text>
|
|
797
|
+
<Text>NPM: <Text color="#f85149">{npmPackage}</Text></Text>
|
|
798
|
+
</>
|
|
799
|
+
)}
|
|
467
800
|
</Box>
|
|
468
801
|
);
|
|
469
802
|
|
|
470
803
|
contentLines.push(<Text key="spacer2"> </Text>);
|
|
471
804
|
|
|
805
|
+
// Git branch
|
|
806
|
+
if (gitBranch) {
|
|
807
|
+
const isMainBranch = gitBranch === 'main' || gitBranch === 'master';
|
|
808
|
+
contentLines.push(
|
|
809
|
+
<Text key="git-branch">
|
|
810
|
+
Branch: <Text color={isMainBranch ? colors.accentGreen : colors.accentBlue}>{gitBranch}</Text>
|
|
811
|
+
</Text>
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
472
815
|
if (project.framework) {
|
|
473
816
|
contentLines.push(
|
|
474
817
|
<Text key="framework">
|
|
475
|
-
|
|
476
|
-
|
|
818
|
+
Framework: <Text color={colors.accentCyan}>{project.framework}</Text>
|
|
819
|
+
</Text>
|
|
477
820
|
);
|
|
478
821
|
}
|
|
479
|
-
|
|
822
|
+
|
|
480
823
|
contentLines.push(
|
|
481
824
|
<Text key="last-scanned">Last Scanned: {lastScanned}</Text>
|
|
482
825
|
);
|
|
@@ -508,14 +851,14 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
|
508
851
|
contentLines.push(<Text key="spacer4"> </Text>);
|
|
509
852
|
}
|
|
510
853
|
|
|
511
|
-
// Scripts
|
|
854
|
+
// Scripts - show all, let virtual scrolling handle visibility
|
|
512
855
|
if (scripts && scripts.scripts && scripts.scripts.size > 0) {
|
|
513
856
|
contentLines.push(
|
|
514
857
|
<Text key="scripts-header" bold>
|
|
515
858
|
Available Scripts (<Text color={colors.accentCyan}>{scripts.scripts.size}</Text>):
|
|
516
859
|
</Text>
|
|
517
860
|
);
|
|
518
|
-
Array.from(scripts.scripts.entries() as IterableIterator<[string, any]>).
|
|
861
|
+
Array.from(scripts.scripts.entries() as IterableIterator<[string, any]>).forEach(([name, script]) => {
|
|
519
862
|
contentLines.push(
|
|
520
863
|
<Text key={`script-${name}`}>
|
|
521
864
|
{' '}
|
|
@@ -525,22 +868,17 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
|
525
868
|
</Text>
|
|
526
869
|
);
|
|
527
870
|
});
|
|
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
871
|
contentLines.push(<Text key="spacer5"> </Text>);
|
|
534
872
|
}
|
|
535
873
|
|
|
536
|
-
// Ports
|
|
874
|
+
// Ports - show all, let virtual scrolling handle visibility
|
|
537
875
|
if (ports.length > 0) {
|
|
538
876
|
contentLines.push(
|
|
539
877
|
<Text key="ports-header" bold>
|
|
540
878
|
Detected Ports (<Text color={colors.accentCyan}>{ports.length}</Text>):
|
|
541
879
|
</Text>
|
|
542
880
|
);
|
|
543
|
-
ports.
|
|
881
|
+
ports.forEach((port: any) => {
|
|
544
882
|
contentLines.push(
|
|
545
883
|
<Text key={`port-${port.id}`}>
|
|
546
884
|
{' '}Port <Text color={colors.accentCyan}>{port.port}</Text>
|
|
@@ -548,11 +886,6 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
|
548
886
|
</Text>
|
|
549
887
|
);
|
|
550
888
|
});
|
|
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
889
|
contentLines.push(<Text key="spacer6"> </Text>);
|
|
557
890
|
}
|
|
558
891
|
|
|
@@ -571,12 +904,12 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
|
571
904
|
const hasMoreBelow = endIndex < contentLines.length;
|
|
572
905
|
|
|
573
906
|
return (
|
|
574
|
-
<Box
|
|
575
|
-
flexDirection="column"
|
|
907
|
+
<Box
|
|
908
|
+
flexDirection="column"
|
|
576
909
|
width="65%"
|
|
577
910
|
height={height}
|
|
578
|
-
borderStyle="round"
|
|
579
|
-
borderColor={isFocused ? colors.accentCyan : colors.borderColor}
|
|
911
|
+
borderStyle="round"
|
|
912
|
+
borderColor={isFocused ? colors.accentCyan : colors.borderColor}
|
|
580
913
|
padding={1}
|
|
581
914
|
flexShrink={0}
|
|
582
915
|
flexGrow={0}
|
|
@@ -588,7 +921,118 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
|
588
921
|
{visibleContent}
|
|
589
922
|
{hasMoreBelow && (
|
|
590
923
|
<Text color={colors.textTertiary}>↓ {contentLines.length - endIndex} more below</Text>
|
|
924
|
+
)}
|
|
925
|
+
</Box>
|
|
926
|
+
</Box>
|
|
927
|
+
);
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
// Terminal Output Panel for showing live logs
|
|
931
|
+
interface TerminalOutputPanelProps {
|
|
932
|
+
processes: any[];
|
|
933
|
+
selectedPid: number | null;
|
|
934
|
+
height: number;
|
|
935
|
+
onSelectProcess: (pid: number) => void;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const TerminalOutputPanel: React.FC<TerminalOutputPanelProps> = ({
|
|
939
|
+
processes,
|
|
940
|
+
selectedPid,
|
|
941
|
+
height,
|
|
942
|
+
onSelectProcess,
|
|
943
|
+
}) => {
|
|
944
|
+
const [logs, setLogs] = useState<string[]>([]);
|
|
945
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
946
|
+
|
|
947
|
+
// Find the selected process or default to first
|
|
948
|
+
const activeProcess = selectedPid
|
|
949
|
+
? processes.find((p: any) => p.pid === selectedPid)
|
|
950
|
+
: processes[0];
|
|
951
|
+
|
|
952
|
+
useEffect(() => {
|
|
953
|
+
if (!activeProcess?.logFile) {
|
|
954
|
+
setLogs(['No active process selected']);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Read initial logs
|
|
959
|
+
try {
|
|
960
|
+
if (fs.existsSync(activeProcess.logFile)) {
|
|
961
|
+
const content = fs.readFileSync(activeProcess.logFile, 'utf-8');
|
|
962
|
+
const lines = content.split('\n').slice(-50); // Last 50 lines
|
|
963
|
+
setLogs(lines);
|
|
964
|
+
setScrollOffset(Math.max(0, lines.length - 10));
|
|
965
|
+
} else {
|
|
966
|
+
setLogs(['Log file not found']);
|
|
967
|
+
}
|
|
968
|
+
} catch {
|
|
969
|
+
setLogs(['Error reading logs']);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Watch for changes
|
|
973
|
+
let watcher: fs.FSWatcher | null = null;
|
|
974
|
+
try {
|
|
975
|
+
watcher = fs.watch(activeProcess.logFile, () => {
|
|
976
|
+
try {
|
|
977
|
+
const content = fs.readFileSync(activeProcess.logFile, 'utf-8');
|
|
978
|
+
const lines = content.split('\n').slice(-100);
|
|
979
|
+
setLogs(lines);
|
|
980
|
+
// Auto-scroll to bottom
|
|
981
|
+
setScrollOffset(Math.max(0, lines.length - 10));
|
|
982
|
+
} catch {
|
|
983
|
+
// Ignore read errors during watch
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
} catch {
|
|
987
|
+
// Ignore watch errors
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return () => {
|
|
991
|
+
if (watcher) {
|
|
992
|
+
watcher.close();
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}, [activeProcess?.logFile, activeProcess?.pid]);
|
|
996
|
+
|
|
997
|
+
const visibleLines = logs.slice(scrollOffset, scrollOffset + height - 4);
|
|
998
|
+
const hasMoreAbove = scrollOffset > 0;
|
|
999
|
+
const hasMoreBelow = scrollOffset + height - 4 < logs.length;
|
|
1000
|
+
|
|
1001
|
+
return (
|
|
1002
|
+
<Box
|
|
1003
|
+
flexDirection="column"
|
|
1004
|
+
width="30%"
|
|
1005
|
+
height={height}
|
|
1006
|
+
borderStyle="round"
|
|
1007
|
+
borderColor={colors.accentGreen}
|
|
1008
|
+
padding={1}
|
|
1009
|
+
flexShrink={0}
|
|
1010
|
+
>
|
|
1011
|
+
<Box flexDirection="row" justifyContent="space-between">
|
|
1012
|
+
<Text bold color={colors.accentGreen}>Terminal Output</Text>
|
|
1013
|
+
{processes.length > 1 && (
|
|
1014
|
+
<Text color={colors.textTertiary}>
|
|
1015
|
+
[{processes.findIndex((p: any) => p.pid === activeProcess?.pid) + 1}/{processes.length}]
|
|
1016
|
+
</Text>
|
|
1017
|
+
)}
|
|
1018
|
+
</Box>
|
|
1019
|
+
{activeProcess && (
|
|
1020
|
+
<Text color={colors.textSecondary}>
|
|
1021
|
+
{truncateText(activeProcess.scriptName, 20)} (PID: {activeProcess.pid})
|
|
1022
|
+
</Text>
|
|
591
1023
|
)}
|
|
1024
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
1025
|
+
{hasMoreAbove && (
|
|
1026
|
+
<Text color={colors.textTertiary}>↑ more above</Text>
|
|
1027
|
+
)}
|
|
1028
|
+
{visibleLines.map((line, idx) => (
|
|
1029
|
+
<Text key={idx} color={colors.textPrimary}>
|
|
1030
|
+
{truncateText(line, 40)}
|
|
1031
|
+
</Text>
|
|
1032
|
+
))}
|
|
1033
|
+
{hasMoreBelow && (
|
|
1034
|
+
<Text color={colors.textTertiary}>↓ more below</Text>
|
|
1035
|
+
)}
|
|
592
1036
|
</Box>
|
|
593
1037
|
</Box>
|
|
594
1038
|
);
|
|
@@ -601,23 +1045,29 @@ interface StatusBarProps {
|
|
|
601
1045
|
|
|
602
1046
|
const StatusBar: React.FC<StatusBarProps> = ({ focusedPanel, selectedProject }) => {
|
|
603
1047
|
if (focusedPanel === 'list') {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
<Text color={colors.accentCyan}>
|
|
611
|
-
|
|
612
|
-
|
|
1048
|
+
return (
|
|
1049
|
+
<Box flexDirection="column">
|
|
1050
|
+
<Box>
|
|
1051
|
+
<Text color={colors.accentGreen}>● API</Text>
|
|
1052
|
+
<Text color={colors.textSecondary}> | </Text>
|
|
1053
|
+
<Text color={colors.textSecondary}>Focus: </Text>
|
|
1054
|
+
<Text color={colors.accentCyan}>List</Text>
|
|
1055
|
+
</Box>
|
|
1056
|
+
<Box>
|
|
1057
|
+
<Text bold>a</Text>
|
|
1058
|
+
<Text color={colors.textSecondary}> Add | </Text>
|
|
613
1059
|
<Text bold>/</Text>
|
|
614
1060
|
<Text color={colors.textSecondary}> Search | </Text>
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
1061
|
+
<Text bold>F</Text>
|
|
1062
|
+
<Text color={colors.textSecondary}> Filter | </Text>
|
|
1063
|
+
<Text bold>S</Text>
|
|
1064
|
+
<Text color={colors.textSecondary}> Sort | </Text>
|
|
1065
|
+
<Text bold>↑↓</Text>
|
|
1066
|
+
<Text color={colors.textSecondary}> Nav | </Text>
|
|
1067
|
+
<Text bold>Tab</Text>
|
|
618
1068
|
<Text color={colors.textSecondary}> Switch | </Text>
|
|
619
|
-
<Text bold>
|
|
620
|
-
<Text color={colors.textSecondary}>
|
|
1069
|
+
<Text bold>T</Text>
|
|
1070
|
+
<Text color={colors.textSecondary}> Terminal | </Text>
|
|
621
1071
|
<Text bold>?</Text>
|
|
622
1072
|
<Text color={colors.textSecondary}> Help | </Text>
|
|
623
1073
|
<Text bold>q</Text>
|
|
@@ -638,29 +1088,21 @@ const StatusBar: React.FC<StatusBarProps> = ({ focusedPanel, selectedProject })
|
|
|
638
1088
|
{selectedProject && (
|
|
639
1089
|
<>
|
|
640
1090
|
<Text color={colors.textSecondary}> | </Text>
|
|
641
|
-
<Text color={colors.textPrimary}>{selectedProject.name}</Text>
|
|
1091
|
+
<Text color={colors.textPrimary}>{truncateText(selectedProject.name, 20)}</Text>
|
|
642
1092
|
</>
|
|
643
1093
|
)}
|
|
644
1094
|
</Box>
|
|
645
1095
|
<Box>
|
|
646
|
-
<Text bold>↑↓/kj</Text>
|
|
647
|
-
<Text color={colors.textSecondary}> Scroll | </Text>
|
|
648
1096
|
<Text bold>e</Text>
|
|
649
1097
|
<Text color={colors.textSecondary}> Edit | </Text>
|
|
650
1098
|
<Text bold>t</Text>
|
|
651
1099
|
<Text color={colors.textSecondary}> Tags | </Text>
|
|
652
1100
|
<Text bold>o</Text>
|
|
653
1101
|
<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>
|
|
1102
|
+
<Text bold>r</Text>
|
|
1103
|
+
<Text color={colors.textSecondary}> Run | </Text>
|
|
658
1104
|
<Text bold>s</Text>
|
|
659
1105
|
<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
1106
|
<Text bold>x</Text>
|
|
665
1107
|
<Text color={colors.textSecondary}> Stop | </Text>
|
|
666
1108
|
<Text bold>d</Text>
|
|
@@ -668,9 +1110,7 @@ const StatusBar: React.FC<StatusBarProps> = ({ focusedPanel, selectedProject })
|
|
|
668
1110
|
<Text bold>Tab</Text>
|
|
669
1111
|
<Text color={colors.textSecondary}> Switch | </Text>
|
|
670
1112
|
<Text bold>?</Text>
|
|
671
|
-
<Text color={colors.textSecondary}> Help
|
|
672
|
-
<Text bold>q</Text>
|
|
673
|
-
<Text color={colors.textSecondary}> Quit</Text>
|
|
1113
|
+
<Text color={colors.textSecondary}> Help</Text>
|
|
674
1114
|
</Box>
|
|
675
1115
|
</Box>
|
|
676
1116
|
);
|
|
@@ -705,7 +1145,17 @@ const App: React.FC = () => {
|
|
|
705
1145
|
const [error, setError] = useState<string | null>(null);
|
|
706
1146
|
const [runningProcesses, setRunningProcesses] = useState<any[]>([]);
|
|
707
1147
|
const [focusedPanel, setFocusedPanel] = useState<'list' | 'details'>('list');
|
|
708
|
-
|
|
1148
|
+
|
|
1149
|
+
// View state
|
|
1150
|
+
const [currentView, setCurrentView] = useState<ViewType>('projects');
|
|
1151
|
+
|
|
1152
|
+
// Git branches
|
|
1153
|
+
const [gitBranches, setGitBranches] = useState<Map<number, string | null>>(new Map());
|
|
1154
|
+
|
|
1155
|
+
// Filter and sort state
|
|
1156
|
+
const [filterType, setFilterType] = useState<FilterType>('all');
|
|
1157
|
+
const [sortType, setSortType] = useState<SortType>('name-asc');
|
|
1158
|
+
|
|
709
1159
|
// Editing state
|
|
710
1160
|
const [editingName, setEditingName] = useState(false);
|
|
711
1161
|
const [editingDescription, setEditingDescription] = useState(false);
|
|
@@ -713,43 +1163,108 @@ const App: React.FC = () => {
|
|
|
713
1163
|
const [editInput, setEditInput] = useState('');
|
|
714
1164
|
const [showUrls, setShowUrls] = useState(false);
|
|
715
1165
|
const [allTags, setAllTags] = useState<string[]>([]);
|
|
716
|
-
|
|
1166
|
+
|
|
1167
|
+
// Modal state
|
|
1168
|
+
const [showAddProjectModal, setShowAddProjectModal] = useState(false);
|
|
1169
|
+
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
|
|
1170
|
+
|
|
717
1171
|
// Search state
|
|
718
1172
|
const [showSearch, setShowSearch] = useState(false);
|
|
719
1173
|
const [searchQuery, setSearchQuery] = useState('');
|
|
720
1174
|
const [listScrollOffset, setListScrollOffset] = useState(0);
|
|
721
1175
|
const [detailsScrollOffset, setDetailsScrollOffset] = useState(0);
|
|
722
|
-
|
|
1176
|
+
|
|
723
1177
|
// Script selection state
|
|
724
1178
|
const [showScriptModal, setShowScriptModal] = useState(false);
|
|
725
1179
|
const [scriptModalData, setScriptModalData] = useState<{ scripts: Map<string, any>; projectName: string; projectPath: string } | null>(null);
|
|
726
|
-
|
|
1180
|
+
|
|
1181
|
+
// Workspace state
|
|
1182
|
+
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
|
1183
|
+
const [selectedWorkspaceIndex, setSelectedWorkspaceIndex] = useState(0);
|
|
1184
|
+
|
|
1185
|
+
// Terminal panel state
|
|
1186
|
+
const [showTerminalPanel, setShowTerminalPanel] = useState(false);
|
|
1187
|
+
const [terminalLogs, setTerminalLogs] = useState<string[]>([]);
|
|
1188
|
+
const [selectedProcessPid, setSelectedProcessPid] = useState<number | null>(null);
|
|
1189
|
+
|
|
1190
|
+
// Settings state
|
|
1191
|
+
const [settings, setSettings] = useState<AppSettings>({
|
|
1192
|
+
editor: { type: 'vscode' },
|
|
1193
|
+
browser: { type: 'chrome' },
|
|
1194
|
+
});
|
|
1195
|
+
const [settingsSection, setSettingsSection] = useState<'editor' | 'browser'>('editor');
|
|
1196
|
+
const [settingsOptionIndex, setSettingsOptionIndex] = useState(0);
|
|
1197
|
+
const settingsEditorOptions: Array<'vscode' | 'cursor' | 'windsurf' | 'zed' | 'custom'> = [
|
|
1198
|
+
'vscode', 'cursor', 'windsurf', 'zed', 'custom',
|
|
1199
|
+
];
|
|
1200
|
+
const settingsBrowserOptions: Array<'chrome' | 'firefox' | 'safari' | 'edge' | 'custom'> = [
|
|
1201
|
+
'chrome', 'firefox', 'safari', 'edge', 'custom',
|
|
1202
|
+
];
|
|
1203
|
+
|
|
727
1204
|
// Get terminal dimensions
|
|
728
1205
|
const terminalHeight = process.stdout.rows || 24;
|
|
729
|
-
const availableHeight = terminalHeight -
|
|
1206
|
+
const availableHeight = terminalHeight - 4; // Subtract status bar (increased for view indicator)
|
|
730
1207
|
|
|
731
1208
|
useEffect(() => {
|
|
732
1209
|
loadProjects();
|
|
733
1210
|
loadRunningProcesses();
|
|
734
1211
|
loadAllTags();
|
|
735
|
-
|
|
736
|
-
//
|
|
1212
|
+
|
|
1213
|
+
// Load settings
|
|
1214
|
+
try {
|
|
1215
|
+
const settingsPath = path.join(os.homedir(), '.projax', 'settings.json');
|
|
1216
|
+
if (fs.existsSync(settingsPath)) {
|
|
1217
|
+
const data = fs.readFileSync(settingsPath, 'utf-8');
|
|
1218
|
+
setSettings(JSON.parse(data));
|
|
1219
|
+
}
|
|
1220
|
+
} catch {
|
|
1221
|
+
// Use defaults
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Refresh running processes and git branches every 5 seconds
|
|
737
1225
|
const interval = setInterval(() => {
|
|
738
1226
|
loadRunningProcesses();
|
|
739
1227
|
}, 5000);
|
|
740
|
-
|
|
1228
|
+
|
|
741
1229
|
return () => clearInterval(interval);
|
|
742
1230
|
}, []);
|
|
743
1231
|
|
|
744
|
-
//
|
|
1232
|
+
// Load git branches when projects change
|
|
745
1233
|
useEffect(() => {
|
|
746
|
-
|
|
1234
|
+
if (allProjects.length > 0) {
|
|
1235
|
+
loadGitBranches();
|
|
1236
|
+
}
|
|
1237
|
+
}, [allProjects]);
|
|
1238
|
+
|
|
1239
|
+
const loadGitBranches = async () => {
|
|
1240
|
+
const branches = new Map<number, string | null>();
|
|
1241
|
+
for (const project of allProjects) {
|
|
1242
|
+
try {
|
|
1243
|
+
const branch = getCurrentBranch(project.path);
|
|
1244
|
+
branches.set(project.id, branch);
|
|
1245
|
+
} catch {
|
|
1246
|
+
branches.set(project.id, null);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
setGitBranches(branches);
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
// Reset editing state and scroll when project changes
|
|
1253
|
+
useEffect(() => {
|
|
1254
|
+
setEditingName(false);
|
|
747
1255
|
setEditingDescription(false);
|
|
748
1256
|
setEditingTags(false);
|
|
749
1257
|
setEditInput('');
|
|
750
1258
|
setDetailsScrollOffset(0); // Reset scroll when switching projects
|
|
751
1259
|
}, [selectedIndex]);
|
|
752
1260
|
|
|
1261
|
+
// Load workspaces when switching to workspaces view
|
|
1262
|
+
useEffect(() => {
|
|
1263
|
+
if (currentView === 'workspaces' && workspaces.length === 0) {
|
|
1264
|
+
loadWorkspacesFromApi();
|
|
1265
|
+
}
|
|
1266
|
+
}, [currentView]);
|
|
1267
|
+
|
|
753
1268
|
// Update scroll offset when selected index changes
|
|
754
1269
|
useEffect(() => {
|
|
755
1270
|
const visibleHeight = Math.max(1, availableHeight - 3);
|
|
@@ -782,32 +1297,85 @@ const App: React.FC = () => {
|
|
|
782
1297
|
const loadProjects = () => {
|
|
783
1298
|
const loadedProjects = getAllProjects();
|
|
784
1299
|
setAllProjects(loadedProjects);
|
|
785
|
-
|
|
1300
|
+
applyFilterAndSort(loadedProjects, searchQuery, filterType, sortType);
|
|
786
1301
|
};
|
|
787
1302
|
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1303
|
+
const applyFilterAndSort = (
|
|
1304
|
+
projectsToFilter: Project[],
|
|
1305
|
+
query: string,
|
|
1306
|
+
filter: FilterType,
|
|
1307
|
+
sort: SortType
|
|
1308
|
+
) => {
|
|
1309
|
+
let filtered = projectsToFilter;
|
|
1310
|
+
|
|
1311
|
+
// Apply search query with filter type
|
|
1312
|
+
if (query.trim()) {
|
|
1313
|
+
const q = query.toLowerCase();
|
|
1314
|
+
filtered = projectsToFilter.filter(project => {
|
|
1315
|
+
switch (filter) {
|
|
1316
|
+
case 'name':
|
|
1317
|
+
return fuzzyMatch(q, project.name);
|
|
1318
|
+
case 'path':
|
|
1319
|
+
return fuzzyMatch(q, project.path);
|
|
1320
|
+
case 'tags':
|
|
1321
|
+
return project.tags?.some((tag: string) => fuzzyMatch(q, tag)) || false;
|
|
1322
|
+
case 'ports': {
|
|
1323
|
+
// Check if project has ports matching the query
|
|
1324
|
+
const db = getDatabaseManager();
|
|
1325
|
+
const ports = db.getProjectPorts(project.id);
|
|
1326
|
+
return ports.some((p: any) => p.port.toString().includes(q));
|
|
1327
|
+
}
|
|
1328
|
+
case 'running': {
|
|
1329
|
+
const isRunning = runningProcesses.some((p: any) => p.projectPath === project.path);
|
|
1330
|
+
return (q === 'running' || q === 'yes' || q === 'true') ? isRunning : !isRunning;
|
|
1331
|
+
}
|
|
1332
|
+
case 'all':
|
|
1333
|
+
default:
|
|
1334
|
+
return (
|
|
1335
|
+
fuzzyMatch(q, project.name) ||
|
|
1336
|
+
(project.description ? fuzzyMatch(q, project.description) : false) ||
|
|
1337
|
+
fuzzyMatch(q, project.path) ||
|
|
1338
|
+
project.tags?.some((tag: string) => fuzzyMatch(q, tag)) ||
|
|
1339
|
+
false
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
792
1343
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1344
|
+
|
|
1345
|
+
// Apply sorting
|
|
1346
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
1347
|
+
switch (sort) {
|
|
1348
|
+
case 'name-asc':
|
|
1349
|
+
return a.name.localeCompare(b.name);
|
|
1350
|
+
case 'name-desc':
|
|
1351
|
+
return b.name.localeCompare(a.name);
|
|
1352
|
+
case 'recent':
|
|
1353
|
+
return (b.last_scanned || 0) - (a.last_scanned || 0);
|
|
1354
|
+
case 'oldest':
|
|
1355
|
+
return (a.created_at || 0) - (b.created_at || 0);
|
|
1356
|
+
case 'running': {
|
|
1357
|
+
const aRunning = runningProcesses.filter((p: any) => p.projectPath === a.path).length;
|
|
1358
|
+
const bRunning = runningProcesses.filter((p: any) => p.projectPath === b.path).length;
|
|
1359
|
+
return bRunning - aRunning;
|
|
1360
|
+
}
|
|
1361
|
+
default:
|
|
1362
|
+
return 0;
|
|
1363
|
+
}
|
|
801
1364
|
});
|
|
802
|
-
|
|
803
|
-
setProjects(
|
|
804
|
-
|
|
1365
|
+
|
|
1366
|
+
setProjects(sorted);
|
|
1367
|
+
|
|
805
1368
|
// Adjust selected index if current selection is out of bounds
|
|
806
|
-
if (selectedIndex >=
|
|
807
|
-
setSelectedIndex(Math.max(0,
|
|
1369
|
+
if (selectedIndex >= sorted.length) {
|
|
1370
|
+
setSelectedIndex(Math.max(0, sorted.length - 1));
|
|
808
1371
|
}
|
|
809
1372
|
};
|
|
810
1373
|
|
|
1374
|
+
// Re-apply filter/sort when dependencies change
|
|
1375
|
+
useEffect(() => {
|
|
1376
|
+
applyFilterAndSort(allProjects, searchQuery, filterType, sortType);
|
|
1377
|
+
}, [filterType, sortType, runningProcesses]);
|
|
1378
|
+
|
|
811
1379
|
const loadRunningProcesses = async () => {
|
|
812
1380
|
try {
|
|
813
1381
|
const processes = await getRunningProcessesClean();
|
|
@@ -937,11 +1505,11 @@ const App: React.FC = () => {
|
|
|
937
1505
|
// Handler for script selection
|
|
938
1506
|
const handleScriptSelect = async (scriptName: string, background: boolean) => {
|
|
939
1507
|
if (!scriptModalData) return;
|
|
940
|
-
|
|
1508
|
+
|
|
941
1509
|
setShowScriptModal(false);
|
|
942
1510
|
setIsLoading(true);
|
|
943
1511
|
setLoadingMessage(`Running ${scriptName}${background ? ' in background' : ''}...`);
|
|
944
|
-
|
|
1512
|
+
|
|
945
1513
|
try {
|
|
946
1514
|
if (background) {
|
|
947
1515
|
await runScriptInBackground(scriptModalData.projectPath, scriptModalData.projectName, scriptName, [], false);
|
|
@@ -959,40 +1527,184 @@ const App: React.FC = () => {
|
|
|
959
1527
|
}
|
|
960
1528
|
};
|
|
961
1529
|
|
|
1530
|
+
// Handler for adding a project
|
|
1531
|
+
const handleAddProject = async (projectPath: string, projectName?: string) => {
|
|
1532
|
+
setShowAddProjectModal(false);
|
|
1533
|
+
setIsLoading(true);
|
|
1534
|
+
setLoadingMessage('Adding project...');
|
|
1535
|
+
|
|
1536
|
+
try {
|
|
1537
|
+
const name = projectName || path.basename(projectPath);
|
|
1538
|
+
const db = getDatabaseManager();
|
|
1539
|
+
const project = db.addProject(name, projectPath);
|
|
1540
|
+
|
|
1541
|
+
// Scan for tests
|
|
1542
|
+
setLoadingMessage('Scanning for tests...');
|
|
1543
|
+
await scanProject(project.id);
|
|
1544
|
+
|
|
1545
|
+
// Scan for ports
|
|
1546
|
+
setLoadingMessage('Scanning for ports...');
|
|
1547
|
+
try {
|
|
1548
|
+
const { scanProjectPorts } = await import('./port-scanner');
|
|
1549
|
+
await scanProjectPorts(project.id);
|
|
1550
|
+
} catch {
|
|
1551
|
+
// Ignore port scanning errors
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
loadProjects();
|
|
1555
|
+
setIsLoading(false);
|
|
1556
|
+
|
|
1557
|
+
// Select the newly added project
|
|
1558
|
+
const newProjects = getAllProjects();
|
|
1559
|
+
const newIndex = newProjects.findIndex((p: Project) => p.id === project.id);
|
|
1560
|
+
if (newIndex >= 0) {
|
|
1561
|
+
setSelectedIndex(newIndex);
|
|
1562
|
+
}
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
setIsLoading(false);
|
|
1565
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
// Handler for deleting a project
|
|
1570
|
+
const handleDeleteProject = () => {
|
|
1571
|
+
if (!selectedProject) return;
|
|
1572
|
+
|
|
1573
|
+
setShowConfirmDelete(false);
|
|
1574
|
+
setIsLoading(true);
|
|
1575
|
+
setLoadingMessage(`Deleting ${selectedProject.name}...`);
|
|
1576
|
+
|
|
1577
|
+
setTimeout(async () => {
|
|
1578
|
+
try {
|
|
1579
|
+
const db = getDatabaseManager();
|
|
1580
|
+
db.removeProject(selectedProject.id);
|
|
1581
|
+
loadProjects();
|
|
1582
|
+
if (selectedIndex >= projects.length - 1) {
|
|
1583
|
+
setSelectedIndex(Math.max(0, projects.length - 2));
|
|
1584
|
+
}
|
|
1585
|
+
setIsLoading(false);
|
|
1586
|
+
} catch (err) {
|
|
1587
|
+
setIsLoading(false);
|
|
1588
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1589
|
+
}
|
|
1590
|
+
}, 100);
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
// Cycle filter type
|
|
1594
|
+
const cycleFilterType = () => {
|
|
1595
|
+
const currentIndex = FILTER_TYPES.indexOf(filterType);
|
|
1596
|
+
const nextIndex = (currentIndex + 1) % FILTER_TYPES.length;
|
|
1597
|
+
setFilterType(FILTER_TYPES[nextIndex]);
|
|
1598
|
+
};
|
|
1599
|
+
|
|
1600
|
+
// Cycle sort type
|
|
1601
|
+
const cycleSortType = () => {
|
|
1602
|
+
const currentIndex = SORT_TYPES.indexOf(sortType);
|
|
1603
|
+
const nextIndex = (currentIndex + 1) % SORT_TYPES.length;
|
|
1604
|
+
setSortType(SORT_TYPES[nextIndex]);
|
|
1605
|
+
};
|
|
1606
|
+
|
|
962
1607
|
useInput((input: string, key: any) => {
|
|
1608
|
+
if (key.mouse) {
|
|
1609
|
+
const { x, y, wheelDown, wheelUp, left } = key.mouse;
|
|
1610
|
+
const { columns: width } = process.stdout;
|
|
1611
|
+
|
|
1612
|
+
// Assuming project list is on the left 35% and details on the right.
|
|
1613
|
+
const projectListWidth = Math.floor(width * 0.35);
|
|
1614
|
+
|
|
1615
|
+
if (x < projectListWidth) {
|
|
1616
|
+
// Mouse is over the project list
|
|
1617
|
+
if (wheelUp) {
|
|
1618
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
if (wheelDown) {
|
|
1622
|
+
setSelectedIndex((prev) => Math.min(projects.length - 1, prev + 1));
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
if (left) {
|
|
1626
|
+
// It's a click, so we need to calculate which item was clicked.
|
|
1627
|
+
// This is an approximation based on the known layout of the ProjectListComponent.
|
|
1628
|
+
const listTopBorder = 1;
|
|
1629
|
+
const listPadding = 1;
|
|
1630
|
+
const listHeaderHeight = 2; // "Projects (...)" + "Filter | Sort"
|
|
1631
|
+
const listStartY = listTopBorder + listPadding + listHeaderHeight;
|
|
1632
|
+
|
|
1633
|
+
const scrollIndicatorHeight = listScrollOffset > 0 ? 1 : 0;
|
|
1634
|
+
const firstItemY = listStartY + scrollIndicatorHeight;
|
|
1635
|
+
|
|
1636
|
+
const clickYInList = y - firstItemY;
|
|
1637
|
+
|
|
1638
|
+
if (clickYInList >= 0) {
|
|
1639
|
+
let cumulativeHeight = 0;
|
|
1640
|
+
const visibleProjects = projects.slice(listScrollOffset);
|
|
1641
|
+
|
|
1642
|
+
for (let i = 0; i < visibleProjects.length; i++) {
|
|
1643
|
+
const project = visibleProjects[i];
|
|
1644
|
+
const branch = gitBranches.get(project.id);
|
|
1645
|
+
const itemHeight = branch ? 2 : 1;
|
|
1646
|
+
|
|
1647
|
+
if (clickYInList >= cumulativeHeight && clickYInList < cumulativeHeight + itemHeight) {
|
|
1648
|
+
const newIndex = listScrollOffset + i;
|
|
1649
|
+
if (newIndex < projects.length) {
|
|
1650
|
+
setSelectedIndex(newIndex);
|
|
1651
|
+
setFocusedPanel('list');
|
|
1652
|
+
}
|
|
1653
|
+
break;
|
|
1654
|
+
}
|
|
1655
|
+
cumulativeHeight += itemHeight;
|
|
1656
|
+
if (cumulativeHeight > availableHeight) {
|
|
1657
|
+
break;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
} else {
|
|
1664
|
+
// Mouse is over the details panel
|
|
1665
|
+
if (wheelUp) {
|
|
1666
|
+
setDetailsScrollOffset(prev => Math.max(0, prev - 1));
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
if (wheelDown) {
|
|
1670
|
+
setDetailsScrollOffset(prev => prev + 1);
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
963
1675
|
// Handle search mode
|
|
964
1676
|
if (showSearch) {
|
|
965
1677
|
if (key.escape) {
|
|
966
1678
|
setShowSearch(false);
|
|
967
1679
|
setSearchQuery('');
|
|
968
|
-
|
|
1680
|
+
applyFilterAndSort(allProjects, '', filterType, sortType);
|
|
969
1681
|
return;
|
|
970
1682
|
}
|
|
971
|
-
|
|
1683
|
+
|
|
972
1684
|
if (key.return) {
|
|
973
1685
|
setShowSearch(false);
|
|
974
1686
|
return;
|
|
975
1687
|
}
|
|
976
|
-
|
|
1688
|
+
|
|
977
1689
|
if (key.backspace || key.delete) {
|
|
978
1690
|
const newQuery = searchQuery.slice(0, -1);
|
|
979
1691
|
setSearchQuery(newQuery);
|
|
980
|
-
|
|
1692
|
+
applyFilterAndSort(allProjects, newQuery, filterType, sortType);
|
|
981
1693
|
return;
|
|
982
1694
|
}
|
|
983
1695
|
|
|
984
1696
|
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
985
1697
|
const newQuery = searchQuery + input;
|
|
986
1698
|
setSearchQuery(newQuery);
|
|
987
|
-
|
|
1699
|
+
applyFilterAndSort(allProjects, newQuery, filterType, sortType);
|
|
988
1700
|
return;
|
|
989
1701
|
}
|
|
990
|
-
|
|
1702
|
+
|
|
991
1703
|
return;
|
|
992
1704
|
}
|
|
993
|
-
|
|
1705
|
+
|
|
994
1706
|
// Don't process input if modal is showing
|
|
995
|
-
if (showHelp || isLoading || error || showUrls || showScriptModal) {
|
|
1707
|
+
if (showHelp || isLoading || error || showUrls || showScriptModal || showAddProjectModal || showConfirmDelete) {
|
|
996
1708
|
// Handle URLs modal
|
|
997
1709
|
if (showUrls && (key.escape || key.return || input === 'q' || input === 'u')) {
|
|
998
1710
|
setShowUrls(false);
|
|
@@ -1001,6 +1713,104 @@ const App: React.FC = () => {
|
|
|
1001
1713
|
return;
|
|
1002
1714
|
}
|
|
1003
1715
|
|
|
1716
|
+
// Handle navigation in workspaces view
|
|
1717
|
+
if (currentView === 'workspaces') {
|
|
1718
|
+
if (key.upArrow || input === 'k') {
|
|
1719
|
+
setSelectedWorkspaceIndex((prev) => Math.max(0, prev - 1));
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
if (key.downArrow || input === 'j') {
|
|
1723
|
+
setSelectedWorkspaceIndex((prev) => Math.min(workspaces.length - 1, prev + 1));
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// Handle navigation in processes view
|
|
1729
|
+
if (currentView === 'processes') {
|
|
1730
|
+
if (input === 'x' && runningProcesses.length > 0) {
|
|
1731
|
+
// Stop all processes (or could select one)
|
|
1732
|
+
setIsLoading(true);
|
|
1733
|
+
setLoadingMessage('Stopping processes...');
|
|
1734
|
+
setTimeout(async () => {
|
|
1735
|
+
try {
|
|
1736
|
+
for (const proc of runningProcesses) {
|
|
1737
|
+
await stopScript(proc.pid);
|
|
1738
|
+
}
|
|
1739
|
+
await loadRunningProcesses();
|
|
1740
|
+
setIsLoading(false);
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
setIsLoading(false);
|
|
1743
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1744
|
+
}
|
|
1745
|
+
}, 100);
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// Handle navigation in settings view
|
|
1751
|
+
if (currentView === 'settings') {
|
|
1752
|
+
if (key.tab) {
|
|
1753
|
+
setSettingsSection((prev) => (prev === 'editor' ? 'browser' : 'editor'));
|
|
1754
|
+
setSettingsOptionIndex(0);
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
if (key.upArrow || input === 'k') {
|
|
1758
|
+
setSettingsOptionIndex((prev) => Math.max(0, prev - 1));
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (key.downArrow || input === 'j') {
|
|
1762
|
+
const maxIndex = settingsSection === 'editor' ? settingsEditorOptions.length - 1 : settingsBrowserOptions.length - 1;
|
|
1763
|
+
setSettingsOptionIndex((prev) => Math.min(maxIndex, prev + 1));
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
if (input === ' ' || key.return) {
|
|
1767
|
+
// Select option and save
|
|
1768
|
+
const newSettings = { ...settings };
|
|
1769
|
+
if (settingsSection === 'editor') {
|
|
1770
|
+
newSettings.editor = { type: settingsEditorOptions[settingsOptionIndex] };
|
|
1771
|
+
} else {
|
|
1772
|
+
newSettings.browser = { type: settingsBrowserOptions[settingsOptionIndex] };
|
|
1773
|
+
}
|
|
1774
|
+
setSettings(newSettings);
|
|
1775
|
+
// Save to file
|
|
1776
|
+
try {
|
|
1777
|
+
const settingsDir = path.join(os.homedir(), '.projax');
|
|
1778
|
+
if (!fs.existsSync(settingsDir)) {
|
|
1779
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
1780
|
+
}
|
|
1781
|
+
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
1782
|
+
fs.writeFileSync(settingsPath, JSON.stringify(newSettings, null, 2));
|
|
1783
|
+
} catch {
|
|
1784
|
+
// Ignore save errors
|
|
1785
|
+
}
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// Global navigation - number keys for view switching
|
|
1791
|
+
if (input === '1') {
|
|
1792
|
+
setCurrentView('projects');
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
if (input === '2') {
|
|
1796
|
+
setCurrentView('workspaces');
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
if (input === '3') {
|
|
1800
|
+
setCurrentView('processes');
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
if (input === '4') {
|
|
1804
|
+
setCurrentView('settings');
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// Terminal panel toggle
|
|
1809
|
+
if (input === 'T') {
|
|
1810
|
+
setShowTerminalPanel(prev => !prev);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1004
1814
|
// Search shortcut
|
|
1005
1815
|
if (input === '/') {
|
|
1006
1816
|
setShowSearch(true);
|
|
@@ -1008,6 +1818,38 @@ const App: React.FC = () => {
|
|
|
1008
1818
|
return;
|
|
1009
1819
|
}
|
|
1010
1820
|
|
|
1821
|
+
// Add project shortcut (in projects view)
|
|
1822
|
+
if (input === 'a' && currentView === 'projects') {
|
|
1823
|
+
setShowAddProjectModal(true);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// Filter cycle shortcut
|
|
1828
|
+
if (input === 'F' && currentView === 'projects') {
|
|
1829
|
+
cycleFilterType();
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// Sort cycle shortcut
|
|
1834
|
+
if (input === 'S' && currentView === 'projects') {
|
|
1835
|
+
cycleSortType();
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// Refresh git branches
|
|
1840
|
+
if (input === 'g' && currentView === 'projects') {
|
|
1841
|
+
loadGitBranches();
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// Full refresh
|
|
1846
|
+
if (input === 'R') {
|
|
1847
|
+
loadProjects();
|
|
1848
|
+
loadRunningProcesses();
|
|
1849
|
+
loadGitBranches();
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1011
1853
|
// Handle editing modes
|
|
1012
1854
|
if (editingName || editingDescription || editingTags) {
|
|
1013
1855
|
if (key.escape) {
|
|
@@ -1176,24 +2018,9 @@ const App: React.FC = () => {
|
|
|
1176
2018
|
return;
|
|
1177
2019
|
}
|
|
1178
2020
|
|
|
1179
|
-
// Delete project
|
|
2021
|
+
// Delete project (with confirmation)
|
|
1180
2022
|
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);
|
|
2023
|
+
setShowConfirmDelete(true);
|
|
1197
2024
|
return;
|
|
1198
2025
|
}
|
|
1199
2026
|
}
|
|
@@ -1295,6 +2122,7 @@ const App: React.FC = () => {
|
|
|
1295
2122
|
);
|
|
1296
2123
|
}
|
|
1297
2124
|
|
|
2125
|
+
|
|
1298
2126
|
if (isLoading) {
|
|
1299
2127
|
return (
|
|
1300
2128
|
<Box flexDirection="column" padding={1}>
|
|
@@ -1375,6 +2203,30 @@ const App: React.FC = () => {
|
|
|
1375
2203
|
);
|
|
1376
2204
|
}
|
|
1377
2205
|
|
|
2206
|
+
if (showAddProjectModal) {
|
|
2207
|
+
return (
|
|
2208
|
+
<Box flexDirection="column" padding={1}>
|
|
2209
|
+
<AddProjectModal
|
|
2210
|
+
onAdd={handleAddProject}
|
|
2211
|
+
onCancel={() => setShowAddProjectModal(false)}
|
|
2212
|
+
/>
|
|
2213
|
+
</Box>
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
if (showConfirmDelete && selectedProject) {
|
|
2218
|
+
return (
|
|
2219
|
+
<Box flexDirection="column" padding={1}>
|
|
2220
|
+
<ConfirmModal
|
|
2221
|
+
title="Delete Project"
|
|
2222
|
+
message={`Are you sure you want to remove "${selectedProject.name}" from the dashboard?`}
|
|
2223
|
+
onConfirm={handleDeleteProject}
|
|
2224
|
+
onCancel={() => setShowConfirmDelete(false)}
|
|
2225
|
+
/>
|
|
2226
|
+
</Box>
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
|
|
1378
2230
|
if (showScriptModal && scriptModalData) {
|
|
1379
2231
|
return (
|
|
1380
2232
|
<Box flexDirection="column" padding={1}>
|
|
@@ -1403,33 +2255,253 @@ const App: React.FC = () => {
|
|
|
1403
2255
|
}
|
|
1404
2256
|
};
|
|
1405
2257
|
|
|
1406
|
-
|
|
1407
|
-
|
|
2258
|
+
// Render Projects view
|
|
2259
|
+
const renderProjectsView = () => (
|
|
2260
|
+
<Box flexDirection="row" height={availableHeight} flexGrow={0} flexShrink={0}>
|
|
2261
|
+
<ProjectListComponent
|
|
2262
|
+
projects={projects}
|
|
2263
|
+
selectedIndex={selectedIndex}
|
|
2264
|
+
runningProcesses={runningProcesses}
|
|
2265
|
+
isFocused={focusedPanel === 'list'}
|
|
2266
|
+
height={availableHeight}
|
|
2267
|
+
scrollOffset={listScrollOffset}
|
|
2268
|
+
gitBranches={gitBranches}
|
|
2269
|
+
filterType={filterType}
|
|
2270
|
+
sortType={sortType}
|
|
2271
|
+
/>
|
|
2272
|
+
<Box width={1} />
|
|
2273
|
+
<ProjectDetailsComponent
|
|
2274
|
+
project={selectedProject}
|
|
2275
|
+
runningProcesses={runningProcesses}
|
|
2276
|
+
isFocused={focusedPanel === 'details'}
|
|
2277
|
+
editingName={editingName}
|
|
2278
|
+
editingDescription={editingDescription}
|
|
2279
|
+
editingTags={editingTags}
|
|
2280
|
+
editInput={editInput}
|
|
2281
|
+
allTags={allTags}
|
|
2282
|
+
onTagRemove={handleTagRemove}
|
|
2283
|
+
height={availableHeight}
|
|
2284
|
+
scrollOffset={detailsScrollOffset}
|
|
2285
|
+
gitBranch={selectedProject ? gitBranches.get(selectedProject.id) || null : null}
|
|
2286
|
+
/>
|
|
2287
|
+
{showTerminalPanel && (
|
|
2288
|
+
<>
|
|
2289
|
+
<Box width={1} />
|
|
2290
|
+
<TerminalOutputPanel
|
|
2291
|
+
processes={runningProcesses}
|
|
2292
|
+
selectedPid={selectedProcessPid}
|
|
2293
|
+
height={availableHeight}
|
|
2294
|
+
onSelectProcess={(pid) => setSelectedProcessPid(pid)}
|
|
2295
|
+
/>
|
|
2296
|
+
</>
|
|
2297
|
+
)}
|
|
2298
|
+
</Box>
|
|
2299
|
+
);
|
|
2300
|
+
|
|
2301
|
+
// Render Workspaces view
|
|
2302
|
+
const renderWorkspacesView = () => (
|
|
1408
2303
|
<Box flexDirection="row" height={availableHeight} flexGrow={0} flexShrink={0}>
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
isFocused={focusedPanel === 'list'}
|
|
2304
|
+
{/* Workspace List */}
|
|
2305
|
+
<Box
|
|
2306
|
+
flexDirection="column"
|
|
2307
|
+
width="35%"
|
|
1414
2308
|
height={availableHeight}
|
|
1415
|
-
|
|
1416
|
-
|
|
2309
|
+
borderStyle="round"
|
|
2310
|
+
borderColor={colors.accentCyan}
|
|
2311
|
+
padding={1}
|
|
2312
|
+
>
|
|
2313
|
+
<Text bold color={colors.textPrimary}>
|
|
2314
|
+
Workspaces ({workspaces.length})
|
|
2315
|
+
</Text>
|
|
2316
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
2317
|
+
{workspaces.length === 0 ? (
|
|
2318
|
+
<Text color={colors.textTertiary}>No workspaces found</Text>
|
|
2319
|
+
) : (
|
|
2320
|
+
workspaces.map((ws, index) => {
|
|
2321
|
+
const isSelected = index === selectedWorkspaceIndex;
|
|
2322
|
+
return (
|
|
2323
|
+
<Text key={ws.id} color={isSelected ? colors.accentCyan : colors.textPrimary} bold={isSelected}>
|
|
2324
|
+
{isSelected ? '▶ ' : ' '}{truncateText(ws.name, 25)}
|
|
2325
|
+
</Text>
|
|
2326
|
+
);
|
|
2327
|
+
})
|
|
2328
|
+
)}
|
|
2329
|
+
</Box>
|
|
2330
|
+
</Box>
|
|
1417
2331
|
<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}
|
|
2332
|
+
{/* Workspace Details */}
|
|
2333
|
+
<Box
|
|
2334
|
+
flexDirection="column"
|
|
2335
|
+
width="65%"
|
|
1428
2336
|
height={availableHeight}
|
|
1429
|
-
|
|
1430
|
-
|
|
2337
|
+
borderStyle="round"
|
|
2338
|
+
borderColor={colors.borderColor}
|
|
2339
|
+
padding={1}
|
|
2340
|
+
>
|
|
2341
|
+
{workspaces[selectedWorkspaceIndex] ? (
|
|
2342
|
+
<>
|
|
2343
|
+
<Text bold color={colors.accentCyan}>
|
|
2344
|
+
{workspaces[selectedWorkspaceIndex].name}
|
|
2345
|
+
</Text>
|
|
2346
|
+
<Text> </Text>
|
|
2347
|
+
{workspaces[selectedWorkspaceIndex].description && (
|
|
2348
|
+
<Text color={colors.textSecondary}>
|
|
2349
|
+
{workspaces[selectedWorkspaceIndex].description}
|
|
2350
|
+
</Text>
|
|
2351
|
+
)}
|
|
2352
|
+
<Text> </Text>
|
|
2353
|
+
<Text color={colors.textTertiary}>
|
|
2354
|
+
Path: {getDisplayPath(workspaces[selectedWorkspaceIndex].workspace_file_path)}
|
|
2355
|
+
</Text>
|
|
2356
|
+
</>
|
|
2357
|
+
) : (
|
|
2358
|
+
<Text color={colors.textTertiary}>Select a workspace</Text>
|
|
2359
|
+
)}
|
|
2360
|
+
</Box>
|
|
1431
2361
|
</Box>
|
|
1432
|
-
|
|
2362
|
+
);
|
|
2363
|
+
|
|
2364
|
+
// Load workspaces from API
|
|
2365
|
+
const loadWorkspacesFromApi = async () => {
|
|
2366
|
+
try {
|
|
2367
|
+
// Try common API ports
|
|
2368
|
+
const ports = [38124, 38125, 38126, 38127, 38128, 3001];
|
|
2369
|
+
let apiBaseUrl = '';
|
|
2370
|
+
|
|
2371
|
+
for (const port of ports) {
|
|
2372
|
+
try {
|
|
2373
|
+
const response = await fetch(`http://localhost:${port}/health`, {
|
|
2374
|
+
signal: AbortSignal.timeout(500),
|
|
2375
|
+
});
|
|
2376
|
+
if (response.ok) {
|
|
2377
|
+
apiBaseUrl = `http://localhost:${port}/api`;
|
|
2378
|
+
break;
|
|
2379
|
+
}
|
|
2380
|
+
} catch {
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
if (!apiBaseUrl) {
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
const response = await fetch(`${apiBaseUrl}/workspaces`);
|
|
2390
|
+
if (response.ok) {
|
|
2391
|
+
const ws = (await response.json()) as Workspace[];
|
|
2392
|
+
setWorkspaces(ws);
|
|
2393
|
+
}
|
|
2394
|
+
} catch {
|
|
2395
|
+
// Ignore workspace loading errors
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
|
|
2399
|
+
// Render Processes view placeholder
|
|
2400
|
+
const renderProcessesView = () => (
|
|
2401
|
+
<Box flexDirection="column" padding={2}>
|
|
2402
|
+
<Text bold color={colors.accentCyan}>Running Processes ({runningProcesses.length})</Text>
|
|
2403
|
+
<Text> </Text>
|
|
2404
|
+
{runningProcesses.length === 0 ? (
|
|
2405
|
+
<Text color={colors.textTertiary}>No running processes</Text>
|
|
2406
|
+
) : (
|
|
2407
|
+
runningProcesses.map((proc: any) => {
|
|
2408
|
+
const uptime = Math.floor((Date.now() - proc.startedAt) / 1000);
|
|
2409
|
+
const minutes = Math.floor(uptime / 60);
|
|
2410
|
+
const seconds = uptime % 60;
|
|
2411
|
+
const uptimeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
2412
|
+
return (
|
|
2413
|
+
<Text key={proc.pid} color={colors.textPrimary}>
|
|
2414
|
+
<Text color={colors.accentGreen}>●</Text> PID {proc.pid}: {proc.projectName} ({proc.scriptName}) - {uptimeStr}
|
|
2415
|
+
</Text>
|
|
2416
|
+
);
|
|
2417
|
+
})
|
|
2418
|
+
)}
|
|
2419
|
+
<Text> </Text>
|
|
2420
|
+
<Text color={colors.textTertiary}>Press 1 to return to Projects</Text>
|
|
2421
|
+
</Box>
|
|
2422
|
+
);
|
|
2423
|
+
|
|
2424
|
+
// Render Settings view
|
|
2425
|
+
const renderSettingsView = () => {
|
|
2426
|
+
const currentOptions = settingsSection === 'editor' ? settingsEditorOptions : settingsBrowserOptions;
|
|
2427
|
+
const currentValue = settingsSection === 'editor' ? settings.editor.type : settings.browser.type;
|
|
2428
|
+
|
|
2429
|
+
return (
|
|
2430
|
+
<Box flexDirection="column" padding={2}>
|
|
2431
|
+
<Text bold color={colors.accentCyan}>Settings</Text>
|
|
2432
|
+
<Text> </Text>
|
|
2433
|
+
|
|
2434
|
+
{/* Section tabs */}
|
|
2435
|
+
<Box>
|
|
2436
|
+
<Text
|
|
2437
|
+
color={settingsSection === 'editor' ? colors.accentCyan : colors.textTertiary}
|
|
2438
|
+
bold={settingsSection === 'editor'}
|
|
2439
|
+
>
|
|
2440
|
+
[Editor]
|
|
2441
|
+
</Text>
|
|
2442
|
+
<Text> </Text>
|
|
2443
|
+
<Text
|
|
2444
|
+
color={settingsSection === 'browser' ? colors.accentCyan : colors.textTertiary}
|
|
2445
|
+
bold={settingsSection === 'browser'}
|
|
2446
|
+
>
|
|
2447
|
+
[Browser]
|
|
2448
|
+
</Text>
|
|
2449
|
+
</Box>
|
|
2450
|
+
<Text> </Text>
|
|
2451
|
+
|
|
2452
|
+
{/* Options */}
|
|
2453
|
+
{currentOptions.map((option, index) => {
|
|
2454
|
+
const isSelected = index === settingsOptionIndex;
|
|
2455
|
+
const isActive = option === currentValue;
|
|
2456
|
+
return (
|
|
2457
|
+
<Text key={option} color={isSelected ? colors.accentCyan : colors.textPrimary} bold={isSelected}>
|
|
2458
|
+
{isSelected ? '▶ ' : ' '}
|
|
2459
|
+
{isActive ? '● ' : '○ '}
|
|
2460
|
+
{option.charAt(0).toUpperCase() + option.slice(1)}
|
|
2461
|
+
</Text>
|
|
2462
|
+
);
|
|
2463
|
+
})}
|
|
2464
|
+
|
|
2465
|
+
<Text> </Text>
|
|
2466
|
+
<Text color={colors.textTertiary}>Tab: switch section | ↑↓/jk: select | Space/Enter: choose</Text>
|
|
2467
|
+
</Box>
|
|
2468
|
+
);
|
|
2469
|
+
};
|
|
2470
|
+
|
|
2471
|
+
return (
|
|
2472
|
+
<Box flexDirection="column" height={terminalHeight}>
|
|
2473
|
+
{/* View indicator bar */}
|
|
2474
|
+
<Box paddingX={1} height={1}>
|
|
2475
|
+
<Text color={currentView === 'projects' ? colors.accentCyan : colors.textTertiary}>
|
|
2476
|
+
[1] Projects
|
|
2477
|
+
</Text>
|
|
2478
|
+
<Text> </Text>
|
|
2479
|
+
<Text color={currentView === 'workspaces' ? colors.accentCyan : colors.textTertiary}>
|
|
2480
|
+
[2] Workspaces
|
|
2481
|
+
</Text>
|
|
2482
|
+
<Text> </Text>
|
|
2483
|
+
<Text color={currentView === 'processes' ? colors.accentCyan : colors.textTertiary}>
|
|
2484
|
+
[3] Processes
|
|
2485
|
+
</Text>
|
|
2486
|
+
<Text> </Text>
|
|
2487
|
+
<Text color={currentView === 'settings' ? colors.accentCyan : colors.textTertiary}>
|
|
2488
|
+
[4] Settings
|
|
2489
|
+
</Text>
|
|
2490
|
+
{showTerminalPanel && (
|
|
2491
|
+
<>
|
|
2492
|
+
<Text> | </Text>
|
|
2493
|
+
<Text color={colors.accentGreen}>Terminal [T]</Text>
|
|
2494
|
+
</>
|
|
2495
|
+
)}
|
|
2496
|
+
</Box>
|
|
2497
|
+
|
|
2498
|
+
{/* Main content based on current view */}
|
|
2499
|
+
{currentView === 'projects' && renderProjectsView()}
|
|
2500
|
+
{currentView === 'workspaces' && renderWorkspacesView()}
|
|
2501
|
+
{currentView === 'processes' && renderProcessesView()}
|
|
2502
|
+
{currentView === 'settings' && renderSettingsView()}
|
|
2503
|
+
|
|
2504
|
+
{/* Status bar */}
|
|
1433
2505
|
<Box paddingX={1} borderStyle="single" borderColor={colors.borderColor} flexShrink={0} height={3}>
|
|
1434
2506
|
<StatusBar focusedPanel={focusedPanel} selectedProject={selectedProject} />
|
|
1435
2507
|
</Box>
|