projax 3.3.51 → 3.3.53
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 +0 -73
- package/coverage/core-bridge.ts.html +24 -3
- package/coverage/index.html +34 -19
- package/coverage/lcov-report/core-bridge.ts.html +24 -3
- package/coverage/lcov-report/index.html +34 -19
- package/coverage/lcov-report/port-extractor.ts.html +1 -1
- package/coverage/lcov-report/port-scanner.ts.html +3 -3
- package/coverage/lcov-report/port-utils.ts.html +1 -1
- package/coverage/lcov-report/script-runner.ts.html +302 -11
- package/coverage/lcov-report/test-parser.ts.html +799 -0
- package/coverage/lcov.info +270 -49
- package/coverage/port-extractor.ts.html +1 -1
- package/coverage/port-scanner.ts.html +3 -3
- package/coverage/port-utils.ts.html +1 -1
- package/coverage/script-runner.ts.html +302 -11
- package/coverage/test-parser.ts.html +799 -0
- package/dist/__tests__/port-scanner.test.js +17 -7
- package/dist/__tests__/script-runner.test.js +17 -7
- package/dist/api/__tests__/database.test.js +17 -7
- package/dist/api/__tests__/database.test.js.map +1 -1
- package/dist/api/__tests__/routes.test.js +18 -7
- package/dist/api/__tests__/routes.test.js.map +1 -1
- package/dist/api/__tests__/scanner.test.js +18 -7
- package/dist/api/__tests__/scanner.test.js.map +1 -1
- package/dist/api/core-bridge.d.ts +8 -0
- package/dist/api/core-bridge.d.ts.map +1 -0
- package/dist/api/core-bridge.js +86 -0
- package/dist/api/core-bridge.js.map +1 -0
- package/dist/api/database.d.ts +0 -8
- package/dist/api/database.d.ts.map +1 -1
- package/dist/api/database.js +24 -57
- package/dist/api/database.js.map +1 -1
- package/dist/api/index.js +19 -9
- package/dist/api/index.js.map +1 -1
- package/dist/api/migrate.js +1 -2
- package/dist/api/migrate.js.map +1 -1
- package/dist/api/package.json +6 -3
- package/dist/api/routes/backup.d.ts +2 -1
- package/dist/api/routes/backup.d.ts.map +1 -1
- package/dist/api/routes/backup.js +4 -4
- package/dist/api/routes/backup.js.map +1 -1
- package/dist/api/routes/index.d.ts +2 -1
- package/dist/api/routes/index.d.ts.map +1 -1
- package/dist/api/routes/index.js +0 -2
- package/dist/api/routes/index.js.map +1 -1
- package/dist/api/routes/projects.d.ts +2 -1
- package/dist/api/routes/projects.d.ts.map +1 -1
- package/dist/api/routes/projects.js +19 -9
- package/dist/api/routes/projects.js.map +1 -1
- package/dist/api/routes/settings.d.ts +2 -1
- package/dist/api/routes/settings.d.ts.map +1 -1
- package/dist/api/routes/settings.js +22 -57
- package/dist/api/routes/settings.js.map +1 -1
- package/dist/api/routes/workspaces.d.ts +2 -1
- package/dist/api/routes/workspaces.d.ts.map +1 -1
- package/dist/api/routes/workspaces.js +29 -104
- package/dist/api/routes/workspaces.js.map +1 -1
- package/dist/api/services/scanner.js +19 -10
- package/dist/api/services/scanner.js.map +1 -1
- package/dist/api/services/test-parser.js +2 -3
- package/dist/api/services/test-parser.js.map +1 -1
- package/dist/api/types.d.ts +0 -5
- package/dist/api/types.d.ts.map +1 -1
- package/dist/core/__tests__/database.test.js +17 -7
- package/dist/core/__tests__/detector.test.js +17 -7
- package/dist/core/__tests__/index.test.js +18 -7
- package/dist/core/__tests__/scanner.test.js +18 -7
- package/dist/core/__tests__/settings.test.js +18 -7
- package/dist/core/backup-utils.js +20 -11
- package/dist/core/database.js +18 -9
- package/dist/core/detector.js +21 -11
- package/dist/core/git-utils.js +19 -10
- package/dist/core/index.js +5 -5
- package/dist/core/scanner.js +2 -3
- package/dist/core/settings.d.ts +0 -85
- package/dist/core/settings.js +9 -306
- package/dist/core/workspace-utils.js +20 -11
- package/dist/core-bridge.js +22 -8
- package/dist/electron/core/__tests__/database.test.js +17 -7
- package/dist/electron/core/__tests__/detector.test.js +17 -7
- package/dist/electron/core/__tests__/index.test.js +18 -7
- package/dist/electron/core/__tests__/scanner.test.js +18 -7
- package/dist/electron/core/__tests__/settings.test.js +18 -7
- package/dist/electron/core/backup-utils.js +20 -11
- package/dist/electron/core/database.js +18 -9
- package/dist/electron/core/detector.js +21 -11
- package/dist/electron/core/git-utils.js +19 -10
- package/dist/electron/core/index.js +5 -5
- package/dist/electron/core/scanner.js +2 -3
- package/dist/electron/core/settings.d.ts +0 -85
- package/dist/electron/core/settings.js +9 -306
- package/dist/electron/core/workspace-utils.js +20 -11
- package/dist/electron/core.js +22 -8
- package/dist/electron/main.js +143 -444
- package/dist/electron/port-extractor.js +18 -9
- package/dist/electron/port-scanner.js +21 -12
- package/dist/electron/port-utils.js +4 -5
- package/dist/electron/preload.d.ts +2 -13
- package/dist/electron/preload.js +2 -9
- package/dist/electron/renderer/assets/index-BjZn_mEF.js +66 -0
- package/dist/electron/renderer/assets/index-CZmDxbJO.js +66 -0
- package/dist/electron/renderer/assets/{index-DWe2TQFv.css → index-DfocdjIj.css} +1 -1
- package/dist/electron/renderer/index.html +2 -2
- package/dist/electron/script-runner.js +29 -20
- package/dist/index.js +37 -134
- package/dist/port-extractor.js +18 -9
- package/dist/port-scanner.js +21 -12
- package/dist/port-utils.js +4 -5
- package/dist/prxi.d.ts +1 -0
- package/dist/prxi.js +1106 -0
- package/dist/prxi.tsx +6 -6
- package/dist/script-runner.js +29 -20
- package/dist/test-parser.js +2 -3
- package/jest.config.js +8 -0
- package/package.json +9 -6
- package/dist/api/routes/mcp.d.ts +0 -3
- package/dist/api/routes/mcp.d.ts.map +0 -1
- package/dist/api/routes/mcp.js +0 -147
- package/dist/api/routes/mcp.js.map +0 -1
- package/dist/electron/renderer/assets/index-59AhiV_K.css +0 -1
- package/dist/electron/renderer/assets/index-A04svynq.js +0 -62
- package/dist/electron/renderer/assets/index-B-etDnj2.js +0 -64
- package/dist/electron/renderer/assets/index-BGodNljq.js +0 -62
- package/dist/electron/renderer/assets/index-Bx18Cyic.js +0 -64
- package/dist/electron/renderer/assets/index-ByBOaxqv.js +0 -62
- package/dist/electron/renderer/assets/index-ByHY-x-j.js +0 -62
- package/dist/electron/renderer/assets/index-C1SRt6Jx.js +0 -62
- package/dist/electron/renderer/assets/index-C8f5yNYe.js +0 -64
- package/dist/electron/renderer/assets/index-C9Fo49a8.js +0 -61
- package/dist/electron/renderer/assets/index-CGx7K7jh.js +0 -62
- package/dist/electron/renderer/assets/index-CIZ3Wl6c.css +0 -1
- package/dist/electron/renderer/assets/index-CJbsU9y8.css +0 -1
- package/dist/electron/renderer/assets/index-CJrLunKK.js +0 -62
- package/dist/electron/renderer/assets/index-CQTleudf.css +0 -1
- package/dist/electron/renderer/assets/index-CQcilqlv.js +0 -62
- package/dist/electron/renderer/assets/index-CS-85xbL.css +0 -1
- package/dist/electron/renderer/assets/index-CYph0WPA.js +0 -62
- package/dist/electron/renderer/assets/index-C_WSLD6y.css +0 -1
- package/dist/electron/renderer/assets/index-CgB-tTpV.js +0 -62
- package/dist/electron/renderer/assets/index-ChoTzPLo.css +0 -1
- package/dist/electron/renderer/assets/index-CopVNRnR.js +0 -64
- package/dist/electron/renderer/assets/index-D1jmaGv5.css +0 -1
- package/dist/electron/renderer/assets/index-D2AOB6Er.js +0 -62
- package/dist/electron/renderer/assets/index-DAfjuYKX.js +0 -61
- package/dist/electron/renderer/assets/index-DEOOHPEi.css +0 -1
- package/dist/electron/renderer/assets/index-DTtg6XrF.css +0 -1
- package/dist/electron/renderer/assets/index-DUvcepWm.js +0 -64
- package/dist/electron/renderer/assets/index-DVWDlM1D.js +0 -62
- package/dist/electron/renderer/assets/index-DZzB20Xf.css +0 -1
- package/dist/electron/renderer/assets/index-Dk0EQt0u.css +0 -1
- package/dist/electron/renderer/assets/index-DknLdADV.js +0 -63
- package/dist/electron/renderer/assets/index-DocuD8Lk.js +0 -64
- package/dist/electron/renderer/assets/index-DwRy5FqP.js +0 -62
- package/dist/electron/renderer/assets/index-DyU-xfd8.css +0 -1
- package/dist/electron/renderer/assets/index-GwC-JVUy.css +0 -1
- package/dist/electron/renderer/assets/index-JXrtTB1F.js +0 -63
- package/dist/electron/renderer/assets/index-Ocrdv8Lb.css +0 -1
- package/dist/electron/renderer/assets/index-R-HsWJ0K.js +0 -62
- package/dist/electron/renderer/assets/index-Ytah0wbZ.js +0 -62
- package/dist/electron/renderer/assets/index-ZVyXUshO.css +0 -1
- package/dist/electron/renderer/assets/index-Z_8dJn3i.js +0 -62
- package/dist/electron/renderer/assets/index-fehviker.js +0 -63
- package/dist/electron/renderer/assets/index-nts9ST-M.js +0 -62
- package/dist/electron/renderer/assets/index-q8NVIH3g.css +0 -1
- package/dist/electron/renderer/assets/index-thUWIXon.js +0 -62
- package/dist/electron/renderer/assets/index-tuQmrwcm.css +0 -1
- package/dist/prxi/src/index.tsx +0 -1370
package/dist/prxi/src/index.tsx
DELETED
|
@@ -1,1370 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { render, Box, Text, useInput, useApp, useFocus, useFocusManager } from 'ink';
|
|
3
|
-
import {
|
|
4
|
-
getDatabaseManager,
|
|
5
|
-
getAllProjects,
|
|
6
|
-
scanProject,
|
|
7
|
-
Project,
|
|
8
|
-
} from '../../cli/src/core-bridge';
|
|
9
|
-
import { getProjectScripts, getRunningProcessesClean, runScriptInBackground, stopScript } from '../../cli/src/script-runner';
|
|
10
|
-
import { spawn } from 'child_process';
|
|
11
|
-
import * as path from 'path';
|
|
12
|
-
import * as os from 'os';
|
|
13
|
-
|
|
14
|
-
// Color scheme matching desktop app
|
|
15
|
-
const colors = {
|
|
16
|
-
bgPrimary: '#0d1117',
|
|
17
|
-
bgSecondary: '#161b22',
|
|
18
|
-
bgTertiary: '#1c2128',
|
|
19
|
-
bgHover: '#21262d',
|
|
20
|
-
borderColor: '#30363d',
|
|
21
|
-
textPrimary: '#c9d1d9',
|
|
22
|
-
textSecondary: '#8b949e',
|
|
23
|
-
textTertiary: '#6e7681',
|
|
24
|
-
accentCyan: '#39c5cf',
|
|
25
|
-
accentBlue: '#58a6ff',
|
|
26
|
-
accentGreen: '#3fb950',
|
|
27
|
-
accentPurple: '#bc8cff',
|
|
28
|
-
accentOrange: '#ffa657',
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// Helper function to get display path
|
|
32
|
-
function getDisplayPath(fullPath: string): string {
|
|
33
|
-
const parts = fullPath.split('/').filter(Boolean);
|
|
34
|
-
if (parts.length === 0) return fullPath;
|
|
35
|
-
|
|
36
|
-
const lastDir = parts[parts.length - 1];
|
|
37
|
-
|
|
38
|
-
// If last directory is "src", go one up
|
|
39
|
-
if (lastDir === 'src' && parts.length > 1) {
|
|
40
|
-
return parts[parts.length - 2];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return lastDir;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Helper function to truncate text
|
|
47
|
-
function truncateText(text: string, maxLength: number): string {
|
|
48
|
-
if (text.length <= maxLength) return text;
|
|
49
|
-
return text.substring(0, maxLength - 3) + '...';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface HelpModalProps {
|
|
53
|
-
onClose: () => void;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const HelpModal: React.FC<HelpModalProps> = ({ onClose }) => {
|
|
57
|
-
useInput((input: string, key: any) => {
|
|
58
|
-
if (input === 'q' || key.escape || key.return) {
|
|
59
|
-
onClose();
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<Box
|
|
65
|
-
flexDirection="column"
|
|
66
|
-
borderStyle="round"
|
|
67
|
-
borderColor={colors.accentCyan}
|
|
68
|
-
padding={1}
|
|
69
|
-
width={70}
|
|
70
|
-
>
|
|
71
|
-
<Text bold color={colors.accentCyan}>
|
|
72
|
-
PROJAX Terminal UI - Help
|
|
73
|
-
</Text>
|
|
74
|
-
<Text> </Text>
|
|
75
|
-
<Text color={colors.accentCyan}>Navigation:</Text>
|
|
76
|
-
<Text> ↑/k Move up in project list</Text>
|
|
77
|
-
<Text> ↓/j Move down in project list</Text>
|
|
78
|
-
<Text> Tab/←→ Switch between list and details</Text>
|
|
79
|
-
<Text> </Text>
|
|
80
|
-
<Text color={colors.accentCyan}>List Panel Actions:</Text>
|
|
81
|
-
<Text> / Search projects (fuzzy search)</Text>
|
|
82
|
-
<Text> s Scan selected project for tests</Text>
|
|
83
|
-
<Text> </Text>
|
|
84
|
-
<Text color={colors.accentCyan}>Details Panel Actions:</Text>
|
|
85
|
-
<Text> ↑↓/kj Scroll details</Text>
|
|
86
|
-
<Text> e Edit project name</Text>
|
|
87
|
-
<Text> t Add/edit tags</Text>
|
|
88
|
-
<Text> o Open project in editor</Text>
|
|
89
|
-
<Text> f Open project directory</Text>
|
|
90
|
-
<Text> u Show detected URLs</Text>
|
|
91
|
-
<Text> s Scan project for tests</Text>
|
|
92
|
-
<Text> p Scan ports for project</Text>
|
|
93
|
-
<Text> r Show scripts (use CLI to run)</Text>
|
|
94
|
-
<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>
|
|
100
|
-
<Text> </Text>
|
|
101
|
-
<Text color={colors.accentCyan}>General:</Text>
|
|
102
|
-
<Text> q/Esc Quit</Text>
|
|
103
|
-
<Text> ? Show this help</Text>
|
|
104
|
-
<Text> </Text>
|
|
105
|
-
<Text color={colors.textSecondary}>Press any key to close...</Text>
|
|
106
|
-
</Box>
|
|
107
|
-
);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
interface LoadingModalProps {
|
|
111
|
-
message: string;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const LoadingModal: React.FC<LoadingModalProps> = ({ message }) => {
|
|
115
|
-
return (
|
|
116
|
-
<Box
|
|
117
|
-
flexDirection="column"
|
|
118
|
-
borderStyle="round"
|
|
119
|
-
borderColor={colors.accentCyan}
|
|
120
|
-
padding={1}
|
|
121
|
-
width={40}
|
|
122
|
-
>
|
|
123
|
-
<Text>{message}</Text>
|
|
124
|
-
<Text color={colors.textSecondary}>Please wait...</Text>
|
|
125
|
-
</Box>
|
|
126
|
-
);
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
interface ErrorModalProps {
|
|
130
|
-
message: string;
|
|
131
|
-
onClose: () => void;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const ErrorModal: React.FC<ErrorModalProps> = ({ message, onClose }) => {
|
|
135
|
-
useInput((input: string, key: any) => {
|
|
136
|
-
if (key.escape || key.return) {
|
|
137
|
-
onClose();
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<Box
|
|
143
|
-
flexDirection="column"
|
|
144
|
-
borderStyle="round"
|
|
145
|
-
borderColor="#f85149"
|
|
146
|
-
padding={1}
|
|
147
|
-
width={60}
|
|
148
|
-
>
|
|
149
|
-
<Text color="#f85149" bold>
|
|
150
|
-
Error
|
|
151
|
-
</Text>
|
|
152
|
-
<Text> </Text>
|
|
153
|
-
<Text>{message}</Text>
|
|
154
|
-
<Text> </Text>
|
|
155
|
-
<Text color={colors.textSecondary}>Press any key to close...</Text>
|
|
156
|
-
</Box>
|
|
157
|
-
);
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
interface ProjectListProps {
|
|
161
|
-
projects: Project[];
|
|
162
|
-
selectedIndex: number;
|
|
163
|
-
runningProcesses: any[];
|
|
164
|
-
isFocused: boolean;
|
|
165
|
-
height: number;
|
|
166
|
-
scrollOffset: number;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const ProjectListComponent: React.FC<ProjectListProps> = ({
|
|
170
|
-
projects,
|
|
171
|
-
selectedIndex,
|
|
172
|
-
runningProcesses,
|
|
173
|
-
isFocused,
|
|
174
|
-
height,
|
|
175
|
-
scrollOffset,
|
|
176
|
-
}) => {
|
|
177
|
-
const { focus } = useFocus({ id: 'projectList' });
|
|
178
|
-
|
|
179
|
-
// Calculate visible range
|
|
180
|
-
const startIndex = Math.max(0, scrollOffset);
|
|
181
|
-
const hasMoreAbove = startIndex > 0;
|
|
182
|
-
const headerHeight = 1; // "Projects (12)" line
|
|
183
|
-
const paddingHeight = 2; // top + bottom padding
|
|
184
|
-
const scrollIndicatorHeight = (hasMoreAbove ? 1 : 0) + 1; // "more above" + "more below" (always reserve space)
|
|
185
|
-
const visibleHeight = Math.max(5, height - paddingHeight - headerHeight - scrollIndicatorHeight);
|
|
186
|
-
const endIndex = Math.min(projects.length, startIndex + visibleHeight);
|
|
187
|
-
const visibleProjects = projects.slice(startIndex, endIndex);
|
|
188
|
-
const hasMoreBelow = endIndex < projects.length;
|
|
189
|
-
|
|
190
|
-
return (
|
|
191
|
-
<Box
|
|
192
|
-
flexDirection="column"
|
|
193
|
-
width="35%"
|
|
194
|
-
height={height}
|
|
195
|
-
borderStyle="round"
|
|
196
|
-
borderColor={isFocused ? colors.accentCyan : colors.borderColor}
|
|
197
|
-
padding={1}
|
|
198
|
-
flexShrink={0}
|
|
199
|
-
flexGrow={0}
|
|
200
|
-
>
|
|
201
|
-
<Text bold color={colors.textPrimary}>
|
|
202
|
-
Projects ({projects.length})
|
|
203
|
-
</Text>
|
|
204
|
-
<Box flexDirection="column" flexGrow={1}>
|
|
205
|
-
{projects.length === 0 ? (
|
|
206
|
-
<Text color={colors.textTertiary}>No projects found</Text>
|
|
207
|
-
) : (
|
|
208
|
-
<>
|
|
209
|
-
{hasMoreAbove && (
|
|
210
|
-
<Text color={colors.textTertiary}>↑ {startIndex} more above</Text>
|
|
211
|
-
)}
|
|
212
|
-
{visibleProjects.map((project, localIndex) => {
|
|
213
|
-
const index = startIndex + localIndex;
|
|
214
|
-
const isSelected = index === selectedIndex;
|
|
215
|
-
|
|
216
|
-
// Check if this project has running scripts
|
|
217
|
-
const projectRunning = runningProcesses.filter(
|
|
218
|
-
(p: any) => p.projectPath === project.path
|
|
219
|
-
);
|
|
220
|
-
const hasRunningScripts = projectRunning.length > 0;
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<Text key={project.id} color={isSelected ? colors.accentCyan : colors.textPrimary} bold={isSelected}>
|
|
224
|
-
{isSelected ? '▶ ' : ' '}
|
|
225
|
-
{hasRunningScripts && <Text color={colors.accentGreen}>● </Text>}
|
|
226
|
-
{truncateText(project.name, 30)}
|
|
227
|
-
{hasRunningScripts && <Text color={colors.accentGreen}> ({projectRunning.length})</Text>}
|
|
228
|
-
</Text>
|
|
229
|
-
);
|
|
230
|
-
})}
|
|
231
|
-
{hasMoreBelow && (
|
|
232
|
-
<Text color={colors.textTertiary}>↓ {projects.length - endIndex} more below</Text>
|
|
233
|
-
)}
|
|
234
|
-
</>
|
|
235
|
-
)}
|
|
236
|
-
</Box>
|
|
237
|
-
</Box>
|
|
238
|
-
);
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
interface ProjectDetailsProps {
|
|
242
|
-
project: Project | null;
|
|
243
|
-
runningProcesses: any[];
|
|
244
|
-
isFocused: boolean;
|
|
245
|
-
editingName: boolean;
|
|
246
|
-
editingDescription: boolean;
|
|
247
|
-
editingTags: boolean;
|
|
248
|
-
editInput: string;
|
|
249
|
-
allTags: string[];
|
|
250
|
-
onTagRemove?: (tag: string) => void;
|
|
251
|
-
height: number;
|
|
252
|
-
scrollOffset: number;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
256
|
-
project,
|
|
257
|
-
runningProcesses,
|
|
258
|
-
isFocused,
|
|
259
|
-
editingName,
|
|
260
|
-
editingDescription,
|
|
261
|
-
editingTags,
|
|
262
|
-
editInput,
|
|
263
|
-
allTags,
|
|
264
|
-
onTagRemove,
|
|
265
|
-
height,
|
|
266
|
-
scrollOffset,
|
|
267
|
-
}) => {
|
|
268
|
-
const { focus } = useFocus({ id: 'projectDetails' });
|
|
269
|
-
const [scripts, setScripts] = useState<any>(null);
|
|
270
|
-
const [ports, setPorts] = useState<any[]>([]);
|
|
271
|
-
const [tests, setTests] = useState<any[]>([]);
|
|
272
|
-
|
|
273
|
-
useEffect(() => {
|
|
274
|
-
if (!project) {
|
|
275
|
-
setScripts(null);
|
|
276
|
-
setPorts([]);
|
|
277
|
-
setTests([]);
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Load scripts
|
|
282
|
-
try {
|
|
283
|
-
const projectScripts = getProjectScripts(project.path);
|
|
284
|
-
setScripts(projectScripts);
|
|
285
|
-
} catch (error) {
|
|
286
|
-
setScripts(null);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Load ports
|
|
290
|
-
try {
|
|
291
|
-
const db = getDatabaseManager();
|
|
292
|
-
const projectPorts = db.getProjectPorts(project.id);
|
|
293
|
-
setPorts(projectPorts);
|
|
294
|
-
} catch (error) {
|
|
295
|
-
setPorts([]);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Load tests
|
|
299
|
-
try {
|
|
300
|
-
const db = getDatabaseManager();
|
|
301
|
-
const projectTests = db.getTestsByProject(project.id);
|
|
302
|
-
setTests(projectTests);
|
|
303
|
-
} catch (error) {
|
|
304
|
-
setTests([]);
|
|
305
|
-
}
|
|
306
|
-
}, [project]);
|
|
307
|
-
|
|
308
|
-
if (!project) {
|
|
309
|
-
return (
|
|
310
|
-
<Box
|
|
311
|
-
flexDirection="column"
|
|
312
|
-
flexGrow={1}
|
|
313
|
-
borderStyle="round"
|
|
314
|
-
borderColor={isFocused ? colors.accentCyan : colors.borderColor}
|
|
315
|
-
padding={1}
|
|
316
|
-
>
|
|
317
|
-
<Text color={colors.textSecondary}>Select a project to view details</Text>
|
|
318
|
-
</Box>
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const lastScanned = project.last_scanned
|
|
323
|
-
? new Date(project.last_scanned * 1000).toLocaleString()
|
|
324
|
-
: 'Never';
|
|
325
|
-
|
|
326
|
-
// Get running processes for this project
|
|
327
|
-
const projectProcesses = runningProcesses.filter((p: any) => p.projectPath === project.path);
|
|
328
|
-
|
|
329
|
-
// Count tests by framework
|
|
330
|
-
const testsByFramework = tests.reduce((acc: any, test: any) => {
|
|
331
|
-
const framework = test.framework || 'unknown';
|
|
332
|
-
acc[framework] = (acc[framework] || 0) + 1;
|
|
333
|
-
return acc;
|
|
334
|
-
}, {});
|
|
335
|
-
|
|
336
|
-
// Build content lines for virtual scrolling
|
|
337
|
-
const contentLines: React.ReactNode[] = [];
|
|
338
|
-
|
|
339
|
-
// Header section (always visible at top)
|
|
340
|
-
contentLines.push(
|
|
341
|
-
editingName ? (
|
|
342
|
-
<Box key="edit-name">
|
|
343
|
-
<Text color={colors.accentCyan}>Editing name: </Text>
|
|
344
|
-
<Text color={colors.textPrimary}>{editInput}</Text>
|
|
345
|
-
<Text color={colors.textSecondary}> (Press Enter to save, Esc to cancel)</Text>
|
|
346
|
-
</Box>
|
|
347
|
-
) : (
|
|
348
|
-
<Text key="name" bold color={colors.textPrimary}>
|
|
349
|
-
{project.name}
|
|
350
|
-
</Text>
|
|
351
|
-
)
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
if (editingDescription) {
|
|
355
|
-
contentLines.push(
|
|
356
|
-
<Box key="edit-desc">
|
|
357
|
-
<Text color={colors.accentCyan}>Editing description: </Text>
|
|
358
|
-
<Text color={colors.textPrimary}>{editInput}</Text>
|
|
359
|
-
<Text color={colors.textSecondary}> (Press Enter to save, Esc to cancel)</Text>
|
|
360
|
-
</Box>
|
|
361
|
-
);
|
|
362
|
-
} else if (project.description) {
|
|
363
|
-
contentLines.push(
|
|
364
|
-
<Text key="desc" color={colors.textSecondary}>{truncateText(project.description, 100)}</Text>
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
contentLines.push(
|
|
369
|
-
<Text key="path" color={colors.textTertiary}>{truncateText(project.path, 100)}</Text>
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
// Tags (simplified to single line)
|
|
373
|
-
if (project.tags && project.tags.length > 0) {
|
|
374
|
-
const tagsText = project.tags.join(', ');
|
|
375
|
-
contentLines.push(
|
|
376
|
-
<Text key="tags">
|
|
377
|
-
<Text color={colors.textSecondary}>Tags: </Text>
|
|
378
|
-
<Text color={colors.accentPurple}>{truncateText(tagsText, 80)}</Text>
|
|
379
|
-
</Text>
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (editingTags) {
|
|
384
|
-
contentLines.push(
|
|
385
|
-
<Text key="edit-tags">
|
|
386
|
-
<Text color={colors.accentCyan}>Add tag: </Text>
|
|
387
|
-
<Text color={colors.textPrimary}>{editInput}</Text>
|
|
388
|
-
</Text>
|
|
389
|
-
);
|
|
390
|
-
const suggestions = allTags.filter(t => t.toLowerCase().includes(editInput.toLowerCase()) && !project.tags?.includes(t)).slice(0, 3);
|
|
391
|
-
if (editInput && suggestions.length > 0) {
|
|
392
|
-
contentLines.push(
|
|
393
|
-
<Text key="suggestions">
|
|
394
|
-
<Text color={colors.textTertiary}>Suggestions: </Text>
|
|
395
|
-
<Text color={colors.accentPurple}>{suggestions.join(', ')}</Text>
|
|
396
|
-
</Text>
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
contentLines.push(<Text key="spacer1"> </Text>);
|
|
402
|
-
|
|
403
|
-
// Stats
|
|
404
|
-
contentLines.push(
|
|
405
|
-
<Box key="stats">
|
|
406
|
-
<Text>Tests: <Text color={colors.accentCyan}>{tests.length}</Text></Text>
|
|
407
|
-
<Text> | </Text>
|
|
408
|
-
<Text>Frameworks: <Text color={colors.accentCyan}>{Object.keys(testsByFramework).length}</Text></Text>
|
|
409
|
-
<Text> | </Text>
|
|
410
|
-
<Text>Ports: <Text color={colors.accentCyan}>{ports.length}</Text></Text>
|
|
411
|
-
<Text> | </Text>
|
|
412
|
-
<Text>Scripts: <Text color={colors.accentCyan}>{scripts?.scripts?.size || 0}</Text></Text>
|
|
413
|
-
</Box>
|
|
414
|
-
);
|
|
415
|
-
|
|
416
|
-
contentLines.push(<Text key="spacer2"> </Text>);
|
|
417
|
-
|
|
418
|
-
if (project.framework) {
|
|
419
|
-
contentLines.push(
|
|
420
|
-
<Text key="framework">
|
|
421
|
-
Framework: <Text color={colors.accentCyan}>{project.framework}</Text>
|
|
422
|
-
</Text>
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
contentLines.push(
|
|
427
|
-
<Text key="last-scanned">Last Scanned: {lastScanned}</Text>
|
|
428
|
-
);
|
|
429
|
-
contentLines.push(<Text key="spacer3"> </Text>);
|
|
430
|
-
|
|
431
|
-
// Running Processes
|
|
432
|
-
if (projectProcesses.length > 0) {
|
|
433
|
-
contentLines.push(
|
|
434
|
-
<Text key="proc-header" bold color={colors.accentGreen}>
|
|
435
|
-
Running Processes ({projectProcesses.length}):
|
|
436
|
-
</Text>
|
|
437
|
-
);
|
|
438
|
-
projectProcesses.forEach((process: any) => {
|
|
439
|
-
const uptime = Math.floor((Date.now() - process.startedAt) / 1000);
|
|
440
|
-
const minutes = Math.floor(uptime / 60);
|
|
441
|
-
const seconds = uptime % 60;
|
|
442
|
-
const uptimeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
443
|
-
|
|
444
|
-
contentLines.push(
|
|
445
|
-
<Text key={`proc-${process.pid}`}>
|
|
446
|
-
{' '}
|
|
447
|
-
<Text color={colors.accentGreen}>●</Text>
|
|
448
|
-
{' '}
|
|
449
|
-
<Text color={colors.textPrimary}>{process.scriptName}</Text>
|
|
450
|
-
<Text color={colors.textSecondary}> (PID: {process.pid}, {uptimeStr})</Text>
|
|
451
|
-
</Text>
|
|
452
|
-
);
|
|
453
|
-
});
|
|
454
|
-
contentLines.push(<Text key="spacer4"> </Text>);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Scripts
|
|
458
|
-
if (scripts && scripts.scripts && scripts.scripts.size > 0) {
|
|
459
|
-
contentLines.push(
|
|
460
|
-
<Text key="scripts-header" bold>
|
|
461
|
-
Available Scripts (<Text color={colors.accentCyan}>{scripts.scripts.size}</Text>):
|
|
462
|
-
</Text>
|
|
463
|
-
);
|
|
464
|
-
Array.from(scripts.scripts.entries() as IterableIterator<[string, any]>).slice(0, 5).forEach(([name, script]) => {
|
|
465
|
-
contentLines.push(
|
|
466
|
-
<Text key={`script-${name}`}>
|
|
467
|
-
{' '}
|
|
468
|
-
<Text color={colors.accentGreen}>{name}</Text>
|
|
469
|
-
{' - '}
|
|
470
|
-
<Text color={colors.textSecondary}>{truncateText(script.command, 60)}</Text>
|
|
471
|
-
</Text>
|
|
472
|
-
);
|
|
473
|
-
});
|
|
474
|
-
if (scripts.scripts.size > 5) {
|
|
475
|
-
contentLines.push(
|
|
476
|
-
<Text key="scripts-more" color={colors.textTertiary}> ... and {scripts.scripts.size - 5} more</Text>
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
contentLines.push(<Text key="spacer5"> </Text>);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Ports
|
|
483
|
-
if (ports.length > 0) {
|
|
484
|
-
contentLines.push(
|
|
485
|
-
<Text key="ports-header" bold>
|
|
486
|
-
Detected Ports (<Text color={colors.accentCyan}>{ports.length}</Text>):
|
|
487
|
-
</Text>
|
|
488
|
-
);
|
|
489
|
-
ports.slice(0, 5).forEach((port: any) => {
|
|
490
|
-
contentLines.push(
|
|
491
|
-
<Text key={`port-${port.id}`}>
|
|
492
|
-
{' '}Port <Text color={colors.accentCyan}>{port.port}</Text>
|
|
493
|
-
<Text color={colors.textSecondary}> - {truncateText(port.config_source, 50)}</Text>
|
|
494
|
-
</Text>
|
|
495
|
-
);
|
|
496
|
-
});
|
|
497
|
-
if (ports.length > 5) {
|
|
498
|
-
contentLines.push(
|
|
499
|
-
<Text key="ports-more" color={colors.textTertiary}> ... and {ports.length - 5} more</Text>
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
contentLines.push(<Text key="spacer6"> </Text>);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Test Files
|
|
506
|
-
if (tests.length > 0) {
|
|
507
|
-
contentLines.push(
|
|
508
|
-
<Text key="tests-header" bold>
|
|
509
|
-
Test Files (<Text color={colors.accentCyan}>{tests.length}</Text>):
|
|
510
|
-
</Text>
|
|
511
|
-
);
|
|
512
|
-
Object.entries(testsByFramework).forEach(([framework, count]) => {
|
|
513
|
-
contentLines.push(
|
|
514
|
-
<Text key={`test-${framework}`}>
|
|
515
|
-
{' '}
|
|
516
|
-
<Text color={colors.accentPurple}>{framework}</Text>
|
|
517
|
-
{': '}
|
|
518
|
-
<Text color={colors.textSecondary}>{count as number}</Text>
|
|
519
|
-
</Text>
|
|
520
|
-
);
|
|
521
|
-
});
|
|
522
|
-
contentLines.push(<Text key="spacer7"> </Text>);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Calculate visible range for virtual scrolling
|
|
526
|
-
// Render enough items to fill the available space
|
|
527
|
-
const startIndex = Math.max(0, scrollOffset);
|
|
528
|
-
const hasMoreAbove = startIndex > 0;
|
|
529
|
-
const paddingHeight = 2; // top + bottom padding
|
|
530
|
-
// Reserve space for scroll indicators
|
|
531
|
-
const reservedForIndicators = (hasMoreAbove ? 1 : 0) + 1;
|
|
532
|
-
// Available space for content - be less conservative now that we have truncation
|
|
533
|
-
const availableContentHeight = height - paddingHeight - reservedForIndicators;
|
|
534
|
-
const visibleHeight = Math.max(5, availableContentHeight); // Render enough to fill space
|
|
535
|
-
const endIndex = Math.min(contentLines.length, startIndex + visibleHeight);
|
|
536
|
-
const visibleContent = contentLines.slice(startIndex, endIndex);
|
|
537
|
-
const hasMoreBelow = endIndex < contentLines.length;
|
|
538
|
-
|
|
539
|
-
return (
|
|
540
|
-
<Box
|
|
541
|
-
flexDirection="column"
|
|
542
|
-
width="65%"
|
|
543
|
-
height={height}
|
|
544
|
-
borderStyle="round"
|
|
545
|
-
borderColor={isFocused ? colors.accentCyan : colors.borderColor}
|
|
546
|
-
padding={1}
|
|
547
|
-
flexShrink={0}
|
|
548
|
-
flexGrow={0}
|
|
549
|
-
>
|
|
550
|
-
<Box flexDirection="column" flexGrow={1}>
|
|
551
|
-
{hasMoreAbove && (
|
|
552
|
-
<Text color={colors.textTertiary}>↑ {startIndex} more above</Text>
|
|
553
|
-
)}
|
|
554
|
-
{visibleContent}
|
|
555
|
-
{hasMoreBelow && (
|
|
556
|
-
<Text color={colors.textTertiary}>↓ {contentLines.length - endIndex} more below</Text>
|
|
557
|
-
)}
|
|
558
|
-
</Box>
|
|
559
|
-
</Box>
|
|
560
|
-
);
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
interface StatusBarProps {
|
|
564
|
-
focusedPanel: 'list' | 'details';
|
|
565
|
-
selectedProject: Project | null;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const StatusBar: React.FC<StatusBarProps> = ({ focusedPanel, selectedProject }) => {
|
|
569
|
-
if (focusedPanel === 'list') {
|
|
570
|
-
return (
|
|
571
|
-
<Box flexDirection="column">
|
|
572
|
-
<Box>
|
|
573
|
-
<Text color={colors.accentGreen}>● API</Text>
|
|
574
|
-
<Text color={colors.textSecondary}> | </Text>
|
|
575
|
-
<Text color={colors.textSecondary}>Focus: </Text>
|
|
576
|
-
<Text color={colors.accentCyan}>Projects</Text>
|
|
577
|
-
</Box>
|
|
578
|
-
<Box>
|
|
579
|
-
<Text bold>/</Text>
|
|
580
|
-
<Text color={colors.textSecondary}> Search | </Text>
|
|
581
|
-
<Text bold>↑↓/kj</Text>
|
|
582
|
-
<Text color={colors.textSecondary}> Navigate | </Text>
|
|
583
|
-
<Text bold>Tab/←→</Text>
|
|
584
|
-
<Text color={colors.textSecondary}> Switch | </Text>
|
|
585
|
-
<Text bold>s</Text>
|
|
586
|
-
<Text color={colors.textSecondary}> Scan | </Text>
|
|
587
|
-
<Text bold>?</Text>
|
|
588
|
-
<Text color={colors.textSecondary}> Help | </Text>
|
|
589
|
-
<Text bold>q</Text>
|
|
590
|
-
<Text color={colors.textSecondary}> Quit</Text>
|
|
591
|
-
</Box>
|
|
592
|
-
</Box>
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Details panel - show project-specific actions
|
|
597
|
-
return (
|
|
598
|
-
<Box flexDirection="column">
|
|
599
|
-
<Box>
|
|
600
|
-
<Text color={colors.accentGreen}>● API</Text>
|
|
601
|
-
<Text color={colors.textSecondary}> | </Text>
|
|
602
|
-
<Text color={colors.textSecondary}>Focus: </Text>
|
|
603
|
-
<Text color={colors.accentCyan}>Details</Text>
|
|
604
|
-
{selectedProject && (
|
|
605
|
-
<>
|
|
606
|
-
<Text color={colors.textSecondary}> | </Text>
|
|
607
|
-
<Text color={colors.textPrimary}>{selectedProject.name}</Text>
|
|
608
|
-
</>
|
|
609
|
-
)}
|
|
610
|
-
</Box>
|
|
611
|
-
<Box>
|
|
612
|
-
<Text bold>↑↓/kj</Text>
|
|
613
|
-
<Text color={colors.textSecondary}> Scroll | </Text>
|
|
614
|
-
<Text bold>e</Text>
|
|
615
|
-
<Text color={colors.textSecondary}> Edit | </Text>
|
|
616
|
-
<Text bold>t</Text>
|
|
617
|
-
<Text color={colors.textSecondary}> Tags | </Text>
|
|
618
|
-
<Text bold>o</Text>
|
|
619
|
-
<Text color={colors.textSecondary}> Editor | </Text>
|
|
620
|
-
<Text bold>f</Text>
|
|
621
|
-
<Text color={colors.textSecondary}> Files | </Text>
|
|
622
|
-
<Text bold>u</Text>
|
|
623
|
-
<Text color={colors.textSecondary}> URLs | </Text>
|
|
624
|
-
<Text bold>s</Text>
|
|
625
|
-
<Text color={colors.textSecondary}> Scan | </Text>
|
|
626
|
-
<Text bold>p</Text>
|
|
627
|
-
<Text color={colors.textSecondary}> Ports | </Text>
|
|
628
|
-
<Text bold>r</Text>
|
|
629
|
-
<Text color={colors.textSecondary}> Scripts | </Text>
|
|
630
|
-
<Text bold>x</Text>
|
|
631
|
-
<Text color={colors.textSecondary}> Stop | </Text>
|
|
632
|
-
<Text bold>d</Text>
|
|
633
|
-
<Text color={colors.textSecondary}> Delete | </Text>
|
|
634
|
-
<Text bold>Tab</Text>
|
|
635
|
-
<Text color={colors.textSecondary}> Switch | </Text>
|
|
636
|
-
<Text bold>?</Text>
|
|
637
|
-
<Text color={colors.textSecondary}> Help | </Text>
|
|
638
|
-
<Text bold>q</Text>
|
|
639
|
-
<Text color={colors.textSecondary}> Quit</Text>
|
|
640
|
-
</Box>
|
|
641
|
-
</Box>
|
|
642
|
-
);
|
|
643
|
-
};
|
|
644
|
-
|
|
645
|
-
// Simple fuzzy search function
|
|
646
|
-
function fuzzyMatch(query: string, text: string): boolean {
|
|
647
|
-
const queryLower = query.toLowerCase();
|
|
648
|
-
const textLower = text.toLowerCase();
|
|
649
|
-
|
|
650
|
-
if (queryLower === '') return true;
|
|
651
|
-
|
|
652
|
-
let queryIndex = 0;
|
|
653
|
-
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
|
|
654
|
-
if (textLower[i] === queryLower[queryIndex]) {
|
|
655
|
-
queryIndex++;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
return queryIndex === queryLower.length;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const App: React.FC = () => {
|
|
663
|
-
const { exit } = useApp();
|
|
664
|
-
const { focusNext, focusPrevious } = useFocusManager();
|
|
665
|
-
const [allProjects, setAllProjects] = useState<Project[]>([]);
|
|
666
|
-
const [projects, setProjects] = useState<Project[]>([]);
|
|
667
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
668
|
-
const [showHelp, setShowHelp] = useState(false);
|
|
669
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
670
|
-
const [loadingMessage, setLoadingMessage] = useState('');
|
|
671
|
-
const [error, setError] = useState<string | null>(null);
|
|
672
|
-
const [runningProcesses, setRunningProcesses] = useState<any[]>([]);
|
|
673
|
-
const [focusedPanel, setFocusedPanel] = useState<'list' | 'details'>('list');
|
|
674
|
-
|
|
675
|
-
// Editing state
|
|
676
|
-
const [editingName, setEditingName] = useState(false);
|
|
677
|
-
const [editingDescription, setEditingDescription] = useState(false);
|
|
678
|
-
const [editingTags, setEditingTags] = useState(false);
|
|
679
|
-
const [editInput, setEditInput] = useState('');
|
|
680
|
-
const [showUrls, setShowUrls] = useState(false);
|
|
681
|
-
const [allTags, setAllTags] = useState<string[]>([]);
|
|
682
|
-
|
|
683
|
-
// Search state
|
|
684
|
-
const [showSearch, setShowSearch] = useState(false);
|
|
685
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
686
|
-
const [listScrollOffset, setListScrollOffset] = useState(0);
|
|
687
|
-
const [detailsScrollOffset, setDetailsScrollOffset] = useState(0);
|
|
688
|
-
|
|
689
|
-
// Get terminal dimensions
|
|
690
|
-
const terminalHeight = process.stdout.rows || 24;
|
|
691
|
-
const availableHeight = terminalHeight - 3; // Subtract status bar
|
|
692
|
-
|
|
693
|
-
useEffect(() => {
|
|
694
|
-
loadProjects();
|
|
695
|
-
loadRunningProcesses();
|
|
696
|
-
loadAllTags();
|
|
697
|
-
|
|
698
|
-
// Refresh running processes every 5 seconds
|
|
699
|
-
const interval = setInterval(() => {
|
|
700
|
-
loadRunningProcesses();
|
|
701
|
-
}, 5000);
|
|
702
|
-
|
|
703
|
-
return () => clearInterval(interval);
|
|
704
|
-
}, []);
|
|
705
|
-
|
|
706
|
-
// Reset editing state and scroll when project changes
|
|
707
|
-
useEffect(() => {
|
|
708
|
-
setEditingName(false);
|
|
709
|
-
setEditingDescription(false);
|
|
710
|
-
setEditingTags(false);
|
|
711
|
-
setEditInput('');
|
|
712
|
-
setDetailsScrollOffset(0); // Reset scroll when switching projects
|
|
713
|
-
}, [selectedIndex]);
|
|
714
|
-
|
|
715
|
-
// Update scroll offset when selected index changes
|
|
716
|
-
useEffect(() => {
|
|
717
|
-
const visibleHeight = Math.max(1, availableHeight - 3);
|
|
718
|
-
setListScrollOffset(prevOffset => {
|
|
719
|
-
if (selectedIndex < prevOffset) {
|
|
720
|
-
return Math.max(0, selectedIndex);
|
|
721
|
-
} else if (selectedIndex >= prevOffset + visibleHeight) {
|
|
722
|
-
return Math.max(0, selectedIndex - visibleHeight + 1);
|
|
723
|
-
}
|
|
724
|
-
return prevOffset;
|
|
725
|
-
});
|
|
726
|
-
}, [selectedIndex, availableHeight]);
|
|
727
|
-
|
|
728
|
-
const loadAllTags = () => {
|
|
729
|
-
try {
|
|
730
|
-
const db = getDatabaseManager();
|
|
731
|
-
const allProjects = getAllProjects();
|
|
732
|
-
const tagsSet = new Set<string>();
|
|
733
|
-
allProjects.forEach(project => {
|
|
734
|
-
if (project.tags && Array.isArray(project.tags)) {
|
|
735
|
-
project.tags.forEach(tag => tagsSet.add(tag));
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
setAllTags(Array.from(tagsSet));
|
|
739
|
-
} catch (error) {
|
|
740
|
-
setAllTags([]);
|
|
741
|
-
}
|
|
742
|
-
};
|
|
743
|
-
|
|
744
|
-
const loadProjects = () => {
|
|
745
|
-
const loadedProjects = getAllProjects();
|
|
746
|
-
setAllProjects(loadedProjects);
|
|
747
|
-
filterProjects(loadedProjects, searchQuery);
|
|
748
|
-
};
|
|
749
|
-
|
|
750
|
-
const filterProjects = (projectsToFilter: Project[], query: string) => {
|
|
751
|
-
if (!query.trim()) {
|
|
752
|
-
setProjects(projectsToFilter);
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const filtered = projectsToFilter.filter(project => {
|
|
757
|
-
const nameMatch = fuzzyMatch(query, project.name);
|
|
758
|
-
const descMatch = project.description ? fuzzyMatch(query, project.description) : false;
|
|
759
|
-
const pathMatch = fuzzyMatch(query, project.path);
|
|
760
|
-
const tagsMatch = project.tags?.some(tag => fuzzyMatch(query, tag)) || false;
|
|
761
|
-
|
|
762
|
-
return nameMatch || descMatch || pathMatch || tagsMatch;
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
setProjects(filtered);
|
|
766
|
-
|
|
767
|
-
// Adjust selected index if current selection is out of bounds
|
|
768
|
-
if (selectedIndex >= filtered.length) {
|
|
769
|
-
setSelectedIndex(Math.max(0, filtered.length - 1));
|
|
770
|
-
}
|
|
771
|
-
};
|
|
772
|
-
|
|
773
|
-
const loadRunningProcesses = async () => {
|
|
774
|
-
try {
|
|
775
|
-
const processes = await getRunningProcessesClean();
|
|
776
|
-
setRunningProcesses(processes);
|
|
777
|
-
} catch (error) {
|
|
778
|
-
setRunningProcesses([]);
|
|
779
|
-
}
|
|
780
|
-
};
|
|
781
|
-
|
|
782
|
-
const selectedProject = projects.length > 0 ? projects[selectedIndex] : null;
|
|
783
|
-
|
|
784
|
-
// Helper function to get editor command
|
|
785
|
-
const getEditorCommand = (): { command: string; args: string[] } => {
|
|
786
|
-
try {
|
|
787
|
-
// Try to load settings from core
|
|
788
|
-
const corePath = path.join(__dirname, '..', '..', 'core', 'dist', 'settings');
|
|
789
|
-
const settingsPath = path.join(__dirname, '..', '..', '..', 'core', 'dist', 'settings');
|
|
790
|
-
let settings: any;
|
|
791
|
-
|
|
792
|
-
try {
|
|
793
|
-
settings = require(corePath);
|
|
794
|
-
} catch {
|
|
795
|
-
try {
|
|
796
|
-
settings = require(settingsPath);
|
|
797
|
-
} catch {
|
|
798
|
-
// Fallback to default
|
|
799
|
-
return { command: 'code', args: [] };
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
const editorSettings = settings.getEditorSettings();
|
|
804
|
-
let command: string;
|
|
805
|
-
const args: string[] = [];
|
|
806
|
-
|
|
807
|
-
if (editorSettings.type === 'custom' && editorSettings.customPath) {
|
|
808
|
-
command = editorSettings.customPath;
|
|
809
|
-
} else {
|
|
810
|
-
switch (editorSettings.type) {
|
|
811
|
-
case 'vscode':
|
|
812
|
-
command = 'code';
|
|
813
|
-
break;
|
|
814
|
-
case 'cursor':
|
|
815
|
-
command = 'cursor';
|
|
816
|
-
break;
|
|
817
|
-
case 'windsurf':
|
|
818
|
-
command = 'windsurf';
|
|
819
|
-
break;
|
|
820
|
-
case 'zed':
|
|
821
|
-
command = 'zed';
|
|
822
|
-
break;
|
|
823
|
-
default:
|
|
824
|
-
command = 'code';
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
return { command, args };
|
|
829
|
-
} catch (error) {
|
|
830
|
-
return { command: 'code', args: [] };
|
|
831
|
-
}
|
|
832
|
-
};
|
|
833
|
-
|
|
834
|
-
// Helper function to open project in editor
|
|
835
|
-
const openInEditor = (projectPath: string) => {
|
|
836
|
-
try {
|
|
837
|
-
const { command, args } = getEditorCommand();
|
|
838
|
-
spawn(command, [...args, projectPath], {
|
|
839
|
-
detached: true,
|
|
840
|
-
stdio: 'ignore',
|
|
841
|
-
}).unref();
|
|
842
|
-
} catch (error) {
|
|
843
|
-
setError(`Failed to open editor: ${error instanceof Error ? error.message : String(error)}`);
|
|
844
|
-
}
|
|
845
|
-
};
|
|
846
|
-
|
|
847
|
-
// Helper function to open project directory
|
|
848
|
-
const openInFiles = (projectPath: string) => {
|
|
849
|
-
try {
|
|
850
|
-
let command: string;
|
|
851
|
-
const args: string[] = [projectPath];
|
|
852
|
-
|
|
853
|
-
if (os.platform() === 'darwin') {
|
|
854
|
-
command = 'open';
|
|
855
|
-
} else if (os.platform() === 'win32') {
|
|
856
|
-
command = 'explorer';
|
|
857
|
-
} else {
|
|
858
|
-
command = 'xdg-open';
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
spawn(command, args, {
|
|
862
|
-
detached: true,
|
|
863
|
-
stdio: 'ignore',
|
|
864
|
-
}).unref();
|
|
865
|
-
} catch (error) {
|
|
866
|
-
setError(`Failed to open file manager: ${error instanceof Error ? error.message : String(error)}`);
|
|
867
|
-
}
|
|
868
|
-
};
|
|
869
|
-
|
|
870
|
-
// Helper function to get URLs from project
|
|
871
|
-
const getProjectUrls = (project: Project): string[] => {
|
|
872
|
-
const urls = new Set<string>();
|
|
873
|
-
|
|
874
|
-
// Add URLs from running processes
|
|
875
|
-
const projectProcesses = runningProcesses.filter((p: any) => p.projectPath === project.path);
|
|
876
|
-
for (const process of projectProcesses) {
|
|
877
|
-
if (process.detectedUrls && Array.isArray(process.detectedUrls)) {
|
|
878
|
-
for (const url of process.detectedUrls) {
|
|
879
|
-
urls.add(url);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// Add URLs from detected ports
|
|
885
|
-
try {
|
|
886
|
-
const db = getDatabaseManager();
|
|
887
|
-
const projectPorts = db.getProjectPorts(project.id);
|
|
888
|
-
for (const portInfo of projectPorts) {
|
|
889
|
-
const url = `http://localhost:${portInfo.port}`;
|
|
890
|
-
urls.add(url);
|
|
891
|
-
}
|
|
892
|
-
} catch (error) {
|
|
893
|
-
// Ignore
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
return Array.from(urls).sort();
|
|
897
|
-
};
|
|
898
|
-
|
|
899
|
-
useInput((input: string, key: any) => {
|
|
900
|
-
// Handle search mode
|
|
901
|
-
if (showSearch) {
|
|
902
|
-
if (key.escape) {
|
|
903
|
-
setShowSearch(false);
|
|
904
|
-
setSearchQuery('');
|
|
905
|
-
filterProjects(allProjects, '');
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
if (key.return) {
|
|
910
|
-
setShowSearch(false);
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
if (key.backspace || key.delete) {
|
|
915
|
-
const newQuery = searchQuery.slice(0, -1);
|
|
916
|
-
setSearchQuery(newQuery);
|
|
917
|
-
filterProjects(allProjects, newQuery);
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
922
|
-
const newQuery = searchQuery + input;
|
|
923
|
-
setSearchQuery(newQuery);
|
|
924
|
-
filterProjects(allProjects, newQuery);
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
return;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// Don't process input if modal is showing
|
|
932
|
-
if (showHelp || isLoading || error || showUrls) {
|
|
933
|
-
// Handle URLs modal
|
|
934
|
-
if (showUrls && (key.escape || key.return || input === 'q' || input === 'u')) {
|
|
935
|
-
setShowUrls(false);
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
// Search shortcut
|
|
942
|
-
if (input === '/') {
|
|
943
|
-
setShowSearch(true);
|
|
944
|
-
setSearchQuery('');
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
// Handle editing modes
|
|
949
|
-
if (editingName || editingDescription || editingTags) {
|
|
950
|
-
if (key.escape) {
|
|
951
|
-
setEditingName(false);
|
|
952
|
-
setEditingDescription(false);
|
|
953
|
-
setEditingTags(false);
|
|
954
|
-
setEditInput('');
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
if (key.return) {
|
|
959
|
-
// Save changes
|
|
960
|
-
if (selectedProject) {
|
|
961
|
-
if (editingName && editInput.trim()) {
|
|
962
|
-
try {
|
|
963
|
-
const db = getDatabaseManager();
|
|
964
|
-
db.updateProjectName(selectedProject.id, editInput.trim());
|
|
965
|
-
loadProjects();
|
|
966
|
-
setEditingName(false);
|
|
967
|
-
setEditInput('');
|
|
968
|
-
} catch (err) {
|
|
969
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
970
|
-
}
|
|
971
|
-
} else if (editingDescription) {
|
|
972
|
-
try {
|
|
973
|
-
const db = getDatabaseManager();
|
|
974
|
-
db.updateProject(selectedProject.id, { description: editInput.trim() || null });
|
|
975
|
-
loadProjects();
|
|
976
|
-
setEditingDescription(false);
|
|
977
|
-
setEditInput('');
|
|
978
|
-
} catch (err) {
|
|
979
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
980
|
-
}
|
|
981
|
-
} else if (editingTags) {
|
|
982
|
-
// Handle tag input
|
|
983
|
-
const newTag = editInput.trim();
|
|
984
|
-
if (newTag) {
|
|
985
|
-
try {
|
|
986
|
-
const db = getDatabaseManager();
|
|
987
|
-
const currentTags = selectedProject.tags || [];
|
|
988
|
-
if (!currentTags.includes(newTag)) {
|
|
989
|
-
db.updateProject(selectedProject.id, { tags: [...currentTags, newTag] });
|
|
990
|
-
loadProjects();
|
|
991
|
-
loadAllTags();
|
|
992
|
-
}
|
|
993
|
-
setEditInput('');
|
|
994
|
-
} catch (err) {
|
|
995
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
return;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// Handle backspace
|
|
1004
|
-
if (key.backspace || key.delete) {
|
|
1005
|
-
setEditInput(prev => prev.slice(0, -1));
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
// Handle regular input
|
|
1010
|
-
if (input && input.length === 1) {
|
|
1011
|
-
setEditInput(prev => prev + input);
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// Quit
|
|
1019
|
-
if (input === 'q' || key.escape) {
|
|
1020
|
-
exit();
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Help
|
|
1025
|
-
if (input === '?') {
|
|
1026
|
-
setShowHelp(true);
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// Switch panels with Tab or Left/Right arrows
|
|
1031
|
-
if (key.tab || key.leftArrow || key.rightArrow) {
|
|
1032
|
-
setFocusedPanel((prev) => prev === 'list' ? 'details' : 'list');
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// Navigation (only when focused on list)
|
|
1037
|
-
if (focusedPanel === 'list') {
|
|
1038
|
-
if (key.upArrow || input === 'k') {
|
|
1039
|
-
setSelectedIndex((prev) => {
|
|
1040
|
-
const newIndex = Math.max(0, prev - 1);
|
|
1041
|
-
// Update scroll offset
|
|
1042
|
-
const visibleHeight = Math.max(1, availableHeight - 3);
|
|
1043
|
-
if (newIndex < listScrollOffset) {
|
|
1044
|
-
setListScrollOffset(Math.max(0, newIndex));
|
|
1045
|
-
}
|
|
1046
|
-
return newIndex;
|
|
1047
|
-
});
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
if (key.downArrow || input === 'j') {
|
|
1052
|
-
setSelectedIndex((prev) => {
|
|
1053
|
-
const newIndex = Math.min(projects.length - 1, prev + 1);
|
|
1054
|
-
// Update scroll offset
|
|
1055
|
-
const visibleHeight = Math.max(1, availableHeight - 3);
|
|
1056
|
-
if (newIndex >= listScrollOffset + visibleHeight) {
|
|
1057
|
-
setListScrollOffset(Math.max(0, newIndex - visibleHeight + 1));
|
|
1058
|
-
}
|
|
1059
|
-
return newIndex;
|
|
1060
|
-
});
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// Details panel actions
|
|
1066
|
-
if (focusedPanel === 'details' && selectedProject) {
|
|
1067
|
-
// Scroll details panel
|
|
1068
|
-
if (key.upArrow || input === 'k') {
|
|
1069
|
-
setDetailsScrollOffset(prev => Math.max(0, prev - 1));
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
if (key.downArrow || input === 'j') {
|
|
1074
|
-
const visibleHeight = Math.max(1, availableHeight - 3);
|
|
1075
|
-
// Estimate content height (rough calculation)
|
|
1076
|
-
setDetailsScrollOffset(prev => prev + 1);
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// Edit name
|
|
1081
|
-
if (input === 'e') {
|
|
1082
|
-
setEditingName(true);
|
|
1083
|
-
setEditingDescription(false);
|
|
1084
|
-
setEditingTags(false);
|
|
1085
|
-
setEditInput(selectedProject.name);
|
|
1086
|
-
return;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
// Edit tags
|
|
1090
|
-
if (input === 't') {
|
|
1091
|
-
setEditingTags(true);
|
|
1092
|
-
setEditingName(false);
|
|
1093
|
-
setEditingDescription(false);
|
|
1094
|
-
setEditInput('');
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// Open in editor
|
|
1099
|
-
if (input === 'o') {
|
|
1100
|
-
openInEditor(selectedProject.path);
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// Open in file manager
|
|
1105
|
-
if (input === 'f') {
|
|
1106
|
-
openInFiles(selectedProject.path);
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
// Show URLs
|
|
1111
|
-
if (input === 'u') {
|
|
1112
|
-
setShowUrls(true);
|
|
1113
|
-
return;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
// Delete project
|
|
1117
|
-
if (input === 'd') {
|
|
1118
|
-
setIsLoading(true);
|
|
1119
|
-
setLoadingMessage(`Deleting ${selectedProject.name}...`);
|
|
1120
|
-
setTimeout(async () => {
|
|
1121
|
-
try {
|
|
1122
|
-
const db = getDatabaseManager();
|
|
1123
|
-
db.removeProject(selectedProject.id);
|
|
1124
|
-
loadProjects();
|
|
1125
|
-
if (selectedIndex >= projects.length - 1) {
|
|
1126
|
-
setSelectedIndex(Math.max(0, projects.length - 2));
|
|
1127
|
-
}
|
|
1128
|
-
setIsLoading(false);
|
|
1129
|
-
} catch (err) {
|
|
1130
|
-
setIsLoading(false);
|
|
1131
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
1132
|
-
}
|
|
1133
|
-
}, 100);
|
|
1134
|
-
return;
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
// Scan project
|
|
1139
|
-
if (input === 's' && selectedProject) {
|
|
1140
|
-
setIsLoading(true);
|
|
1141
|
-
setLoadingMessage(`Scanning ${selectedProject.name}...`);
|
|
1142
|
-
|
|
1143
|
-
setTimeout(async () => {
|
|
1144
|
-
try {
|
|
1145
|
-
await scanProject(selectedProject.id);
|
|
1146
|
-
loadProjects();
|
|
1147
|
-
setIsLoading(false);
|
|
1148
|
-
} catch (err) {
|
|
1149
|
-
setIsLoading(false);
|
|
1150
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
1151
|
-
}
|
|
1152
|
-
}, 100);
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
// Scan ports
|
|
1157
|
-
if (input === 'p' && selectedProject) {
|
|
1158
|
-
setIsLoading(true);
|
|
1159
|
-
setLoadingMessage(`Scanning ports for ${selectedProject.name}...`);
|
|
1160
|
-
|
|
1161
|
-
setTimeout(async () => {
|
|
1162
|
-
try {
|
|
1163
|
-
// Import port scanner dynamically
|
|
1164
|
-
const { scanProjectPorts } = await import('../../cli/src/port-scanner');
|
|
1165
|
-
await scanProjectPorts(selectedProject.id);
|
|
1166
|
-
loadProjects();
|
|
1167
|
-
setIsLoading(false);
|
|
1168
|
-
} catch (err) {
|
|
1169
|
-
setIsLoading(false);
|
|
1170
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
1171
|
-
}
|
|
1172
|
-
}, 100);
|
|
1173
|
-
return;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// Run script
|
|
1177
|
-
if (input === 'r' && selectedProject) {
|
|
1178
|
-
setIsLoading(true);
|
|
1179
|
-
setLoadingMessage('Select script to run...');
|
|
1180
|
-
|
|
1181
|
-
setTimeout(async () => {
|
|
1182
|
-
try {
|
|
1183
|
-
const projectScripts = getProjectScripts(selectedProject.path);
|
|
1184
|
-
if (projectScripts.scripts.size === 0) {
|
|
1185
|
-
setIsLoading(false);
|
|
1186
|
-
setError(`No scripts found in ${selectedProject.name}`);
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
setIsLoading(false);
|
|
1191
|
-
|
|
1192
|
-
// For now, just show error to select a script
|
|
1193
|
-
const scriptList = Array.from(projectScripts.scripts.keys()).join(', ');
|
|
1194
|
-
setError(`Use CLI to run scripts: prx run ${selectedProject.name} <script>\nAvailable: ${scriptList}`);
|
|
1195
|
-
} catch (err) {
|
|
1196
|
-
setIsLoading(false);
|
|
1197
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
1198
|
-
}
|
|
1199
|
-
}, 100);
|
|
1200
|
-
return;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// Stop all scripts for project
|
|
1204
|
-
if (input === 'x' && selectedProject) {
|
|
1205
|
-
setIsLoading(true);
|
|
1206
|
-
setLoadingMessage(`Stopping scripts for ${selectedProject.name}...`);
|
|
1207
|
-
|
|
1208
|
-
setTimeout(async () => {
|
|
1209
|
-
try {
|
|
1210
|
-
const projectProcesses = runningProcesses.filter((p: any) => p.projectPath === selectedProject.path);
|
|
1211
|
-
if (projectProcesses.length === 0) {
|
|
1212
|
-
setIsLoading(false);
|
|
1213
|
-
setError(`No running scripts for ${selectedProject.name}`);
|
|
1214
|
-
return;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
for (const proc of projectProcesses) {
|
|
1218
|
-
await stopScript(proc.pid);
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
await loadRunningProcesses();
|
|
1222
|
-
setIsLoading(false);
|
|
1223
|
-
} catch (err) {
|
|
1224
|
-
setIsLoading(false);
|
|
1225
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
1226
|
-
}
|
|
1227
|
-
}, 100);
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
});
|
|
1232
|
-
|
|
1233
|
-
if (showHelp) {
|
|
1234
|
-
return (
|
|
1235
|
-
<Box flexDirection="column" padding={1}>
|
|
1236
|
-
<HelpModal onClose={() => setShowHelp(false)} />
|
|
1237
|
-
</Box>
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
if (isLoading) {
|
|
1242
|
-
return (
|
|
1243
|
-
<Box flexDirection="column" padding={1}>
|
|
1244
|
-
<LoadingModal message={loadingMessage} />
|
|
1245
|
-
</Box>
|
|
1246
|
-
);
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
if (error) {
|
|
1250
|
-
return (
|
|
1251
|
-
<Box flexDirection="column" padding={1}>
|
|
1252
|
-
<ErrorModal message={error} onClose={() => setError(null)} />
|
|
1253
|
-
</Box>
|
|
1254
|
-
);
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
if (showSearch) {
|
|
1258
|
-
return (
|
|
1259
|
-
<Box flexDirection="column" padding={1}>
|
|
1260
|
-
<Box
|
|
1261
|
-
flexDirection="column"
|
|
1262
|
-
borderStyle="round"
|
|
1263
|
-
borderColor={colors.accentCyan}
|
|
1264
|
-
padding={1}
|
|
1265
|
-
width={60}
|
|
1266
|
-
>
|
|
1267
|
-
<Text bold color={colors.accentCyan}>
|
|
1268
|
-
Search Projects
|
|
1269
|
-
</Text>
|
|
1270
|
-
<Text> </Text>
|
|
1271
|
-
<Text color={colors.textPrimary}>
|
|
1272
|
-
/{searchQuery}
|
|
1273
|
-
<Text color={colors.textTertiary}>_</Text>
|
|
1274
|
-
</Text>
|
|
1275
|
-
<Text> </Text>
|
|
1276
|
-
{projects.length === 0 ? (
|
|
1277
|
-
<Text color={colors.textTertiary}>No projects match "{searchQuery}"</Text>
|
|
1278
|
-
) : (
|
|
1279
|
-
<Text color={colors.textSecondary}>
|
|
1280
|
-
Found {projects.length} project{projects.length !== 1 ? 's' : ''}
|
|
1281
|
-
</Text>
|
|
1282
|
-
)}
|
|
1283
|
-
<Text> </Text>
|
|
1284
|
-
<Text color={colors.textSecondary}>Press Enter to confirm, Esc to cancel</Text>
|
|
1285
|
-
</Box>
|
|
1286
|
-
</Box>
|
|
1287
|
-
);
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
if (showUrls && selectedProject) {
|
|
1291
|
-
const urls = getProjectUrls(selectedProject);
|
|
1292
|
-
return (
|
|
1293
|
-
<Box flexDirection="column" padding={1}>
|
|
1294
|
-
<Box
|
|
1295
|
-
flexDirection="column"
|
|
1296
|
-
borderStyle="round"
|
|
1297
|
-
borderColor={colors.accentCyan}
|
|
1298
|
-
padding={1}
|
|
1299
|
-
width={70}
|
|
1300
|
-
>
|
|
1301
|
-
<Text bold color={colors.accentCyan}>
|
|
1302
|
-
URLs for {selectedProject.name}
|
|
1303
|
-
</Text>
|
|
1304
|
-
<Text> </Text>
|
|
1305
|
-
{urls.length === 0 ? (
|
|
1306
|
-
<Text color={colors.textTertiary}>No URLs detected</Text>
|
|
1307
|
-
) : (
|
|
1308
|
-
urls.map((url, idx) => (
|
|
1309
|
-
<Text key={idx} color={colors.textPrimary}>
|
|
1310
|
-
{idx + 1}. {url}
|
|
1311
|
-
</Text>
|
|
1312
|
-
))
|
|
1313
|
-
)}
|
|
1314
|
-
<Text> </Text>
|
|
1315
|
-
<Text color={colors.textSecondary}>Press Esc or u to close...</Text>
|
|
1316
|
-
</Box>
|
|
1317
|
-
</Box>
|
|
1318
|
-
);
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
const handleTagRemove = (tag: string) => {
|
|
1322
|
-
if (selectedProject) {
|
|
1323
|
-
try {
|
|
1324
|
-
const db = getDatabaseManager();
|
|
1325
|
-
const currentTags = selectedProject.tags || [];
|
|
1326
|
-
db.updateProject(selectedProject.id, { tags: currentTags.filter(t => t !== tag) });
|
|
1327
|
-
loadProjects();
|
|
1328
|
-
loadAllTags();
|
|
1329
|
-
} catch (err) {
|
|
1330
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
};
|
|
1334
|
-
|
|
1335
|
-
return (
|
|
1336
|
-
<Box flexDirection="column" height={terminalHeight}>
|
|
1337
|
-
<Box flexDirection="row" height={availableHeight} flexGrow={0} flexShrink={0}>
|
|
1338
|
-
<ProjectListComponent
|
|
1339
|
-
projects={projects}
|
|
1340
|
-
selectedIndex={selectedIndex}
|
|
1341
|
-
runningProcesses={runningProcesses}
|
|
1342
|
-
isFocused={focusedPanel === 'list'}
|
|
1343
|
-
height={availableHeight}
|
|
1344
|
-
scrollOffset={listScrollOffset}
|
|
1345
|
-
/>
|
|
1346
|
-
<Box width={1} />
|
|
1347
|
-
<ProjectDetailsComponent
|
|
1348
|
-
project={selectedProject}
|
|
1349
|
-
runningProcesses={runningProcesses}
|
|
1350
|
-
isFocused={focusedPanel === 'details'}
|
|
1351
|
-
editingName={editingName}
|
|
1352
|
-
editingDescription={editingDescription}
|
|
1353
|
-
editingTags={editingTags}
|
|
1354
|
-
editInput={editInput}
|
|
1355
|
-
allTags={allTags}
|
|
1356
|
-
onTagRemove={handleTagRemove}
|
|
1357
|
-
height={availableHeight}
|
|
1358
|
-
scrollOffset={detailsScrollOffset}
|
|
1359
|
-
/>
|
|
1360
|
-
</Box>
|
|
1361
|
-
|
|
1362
|
-
<Box paddingX={1} borderStyle="single" borderColor={colors.borderColor} flexShrink={0} height={3}>
|
|
1363
|
-
<StatusBar focusedPanel={focusedPanel} selectedProject={selectedProject} />
|
|
1364
|
-
</Box>
|
|
1365
|
-
</Box>
|
|
1366
|
-
);
|
|
1367
|
-
};
|
|
1368
|
-
|
|
1369
|
-
// Render the app
|
|
1370
|
-
render(<App />);
|