projax 3.3.58 → 3.3.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/electron/script-runner.js +52 -20
  2. package/dist/index.js +1 -1
  3. package/dist/prxi.js +844 -109
  4. package/dist/prxi.tsx +1234 -179
  5. package/dist/script-runner.js +52 -20
  6. package/package.json +1 -1
  7. package/coverage/base.css +0 -224
  8. package/coverage/block-navigation.js +0 -87
  9. package/coverage/core-bridge.ts.html +0 -292
  10. package/coverage/favicon.png +0 -0
  11. package/coverage/index.html +0 -191
  12. package/coverage/lcov-report/base.css +0 -224
  13. package/coverage/lcov-report/block-navigation.js +0 -87
  14. package/coverage/lcov-report/core-bridge.ts.html +0 -292
  15. package/coverage/lcov-report/favicon.png +0 -0
  16. package/coverage/lcov-report/index.html +0 -191
  17. package/coverage/lcov-report/port-extractor.ts.html +0 -1174
  18. package/coverage/lcov-report/port-scanner.ts.html +0 -301
  19. package/coverage/lcov-report/port-utils.ts.html +0 -670
  20. package/coverage/lcov-report/prettify.css +0 -1
  21. package/coverage/lcov-report/prettify.js +0 -2
  22. package/coverage/lcov-report/script-runner.ts.html +0 -3346
  23. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  24. package/coverage/lcov-report/sorter.js +0 -210
  25. package/coverage/lcov-report/test-parser.ts.html +0 -799
  26. package/coverage/lcov.info +0 -1338
  27. package/coverage/port-extractor.ts.html +0 -1174
  28. package/coverage/port-scanner.ts.html +0 -301
  29. package/coverage/port-utils.ts.html +0 -670
  30. package/coverage/prettify.css +0 -1
  31. package/coverage/prettify.js +0 -2
  32. package/coverage/script-runner.ts.html +0 -3346
  33. package/coverage/sort-arrow-sprite.png +0 -0
  34. package/coverage/sorter.js +0 -210
  35. package/coverage/test-parser.ts.html +0 -799
  36. package/dist/__tests__/core-bridge.test.d.ts +0 -1
  37. package/dist/__tests__/core-bridge.test.js +0 -135
  38. package/dist/__tests__/port-extractor.test.d.ts +0 -1
  39. package/dist/__tests__/port-extractor.test.js +0 -407
  40. package/dist/__tests__/port-scanner.test.d.ts +0 -1
  41. package/dist/__tests__/port-scanner.test.js +0 -170
  42. package/dist/__tests__/port-utils.test.d.ts +0 -1
  43. package/dist/__tests__/port-utils.test.js +0 -127
  44. package/dist/__tests__/script-runner.test.d.ts +0 -1
  45. package/dist/__tests__/script-runner.test.js +0 -491
  46. package/dist/__tests__/test-parser.test.d.ts +0 -1
  47. package/dist/__tests__/test-parser.test.js +0 -276
  48. package/dist/api/__tests__/database.test.d.ts +0 -2
  49. package/dist/api/__tests__/database.test.d.ts.map +0 -1
  50. package/dist/api/__tests__/database.test.js +0 -485
  51. package/dist/api/__tests__/database.test.js.map +0 -1
  52. package/dist/api/__tests__/routes.test.d.ts +0 -2
  53. package/dist/api/__tests__/routes.test.d.ts.map +0 -1
  54. package/dist/api/__tests__/routes.test.js +0 -484
  55. package/dist/api/__tests__/routes.test.js.map +0 -1
  56. package/dist/api/__tests__/scanner.test.d.ts +0 -2
  57. package/dist/api/__tests__/scanner.test.d.ts.map +0 -1
  58. package/dist/api/__tests__/scanner.test.js +0 -403
  59. package/dist/api/__tests__/scanner.test.js.map +0 -1
  60. package/dist/core/__tests__/database.test.d.ts +0 -1
  61. package/dist/core/__tests__/database.test.js +0 -557
  62. package/dist/core/__tests__/detector.test.d.ts +0 -1
  63. package/dist/core/__tests__/detector.test.js +0 -375
  64. package/dist/core/__tests__/index.test.d.ts +0 -1
  65. package/dist/core/__tests__/index.test.js +0 -469
  66. package/dist/core/__tests__/scanner.test.d.ts +0 -1
  67. package/dist/core/__tests__/scanner.test.js +0 -406
  68. package/dist/core/__tests__/settings.test.d.ts +0 -1
  69. package/dist/core/__tests__/settings.test.js +0 -280
  70. package/dist/electron/core/__tests__/database.test.d.ts +0 -1
  71. package/dist/electron/core/__tests__/database.test.js +0 -557
  72. package/dist/electron/core/__tests__/detector.test.d.ts +0 -1
  73. package/dist/electron/core/__tests__/detector.test.js +0 -375
  74. package/dist/electron/core/__tests__/index.test.d.ts +0 -1
  75. package/dist/electron/core/__tests__/index.test.js +0 -469
  76. package/dist/electron/core/__tests__/scanner.test.d.ts +0 -1
  77. package/dist/electron/core/__tests__/scanner.test.js +0 -406
  78. package/dist/electron/core/__tests__/settings.test.d.ts +0 -1
  79. package/dist/electron/core/__tests__/settings.test.js +0 -280
  80. package/jest.config.js +0 -26
package/dist/prxi.js CHANGED
@@ -35,11 +35,24 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  const react_1 = __importStar(require("react"));
37
37
  const ink_1 = require("ink");
38
+ // Handle EPIPE errors gracefully to prevent crashes when output streams close
39
+ process.stdout.on('error', (err) => {
40
+ if (err.code === 'EPIPE') {
41
+ // Output stream closed, exit gracefully
42
+ process.exit(0);
43
+ }
44
+ });
45
+ process.stderr.on('error', (err) => {
46
+ if (err.code === 'EPIPE') {
47
+ process.exit(0);
48
+ }
49
+ });
38
50
  const core_bridge_1 = require("./core-bridge");
39
51
  const script_runner_1 = require("./script-runner");
40
52
  const child_process_1 = require("child_process");
41
53
  const path = __importStar(require("path"));
42
54
  const os = __importStar(require("os"));
55
+ const fs = __importStar(require("fs"));
43
56
  // Color scheme matching desktop app
44
57
  const colors = {
45
58
  bgPrimary: '#0d1117',
@@ -74,26 +87,51 @@ function truncateText(text, maxLength) {
74
87
  return text;
75
88
  return text.substring(0, maxLength - 3) + '...';
76
89
  }
90
+ const FILTER_TYPES = ['all', 'name', 'path', 'ports', 'tags', 'running'];
91
+ const SORT_TYPES = ['name-asc', 'name-desc', 'recent', 'oldest', 'running'];
92
+ const FILTER_LABELS = {
93
+ 'all': 'All',
94
+ 'name': 'Name',
95
+ 'path': 'Path',
96
+ 'ports': 'Ports',
97
+ 'tags': 'Tags',
98
+ 'running': 'Running',
99
+ };
100
+ const SORT_LABELS = {
101
+ 'name-asc': 'Name A-Z',
102
+ 'name-desc': 'Name Z-A',
103
+ 'recent': 'Recently Scanned',
104
+ 'oldest': 'Oldest First',
105
+ 'running': 'Running First',
106
+ };
77
107
  const HelpModal = ({ onClose }) => {
78
108
  (0, ink_1.useInput)((input, key) => {
79
109
  if (input === 'q' || key.escape || key.return) {
80
110
  onClose();
81
111
  }
82
112
  });
83
- return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentCyan, padding: 1, width: 70 },
113
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentCyan, padding: 1, width: 75 },
84
114
  react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, "PROJAX Terminal UI - Help"),
85
115
  react_1.default.createElement(ink_1.Text, null, " "),
86
- react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Navigation:"),
87
- react_1.default.createElement(ink_1.Text, null, " \u2191/k Move up in project list"),
88
- react_1.default.createElement(ink_1.Text, null, " \u2193/j Move down in project list"),
89
- react_1.default.createElement(ink_1.Text, null, " Tab/\u2190\u2192 Switch between list and details"),
116
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "View Navigation:"),
117
+ react_1.default.createElement(ink_1.Text, null, " 1 Projects view"),
118
+ react_1.default.createElement(ink_1.Text, null, " 2 Workspaces view"),
119
+ react_1.default.createElement(ink_1.Text, null, " 3 Global processes view"),
120
+ react_1.default.createElement(ink_1.Text, null, " 4 Settings"),
121
+ react_1.default.createElement(ink_1.Text, null, " T Toggle terminal output panel"),
90
122
  react_1.default.createElement(ink_1.Text, null, " "),
91
- react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "List Panel Actions:"),
123
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Projects View - Navigation:"),
124
+ react_1.default.createElement(ink_1.Text, null, " \u2191/k Move up in list"),
125
+ react_1.default.createElement(ink_1.Text, null, " \u2193/j Move down in list"),
126
+ react_1.default.createElement(ink_1.Text, null, " Tab/\u2190\u2192 Switch between list and details"),
92
127
  react_1.default.createElement(ink_1.Text, null, " / Search projects (fuzzy search)"),
93
- react_1.default.createElement(ink_1.Text, null, " s Scan selected project for tests"),
128
+ react_1.default.createElement(ink_1.Text, null, " F Cycle filter type (all/name/path/ports/tags/running)"),
129
+ react_1.default.createElement(ink_1.Text, null, " S Cycle sort order (name/recent/oldest/running)"),
130
+ react_1.default.createElement(ink_1.Text, null, " g Refresh git branches"),
131
+ react_1.default.createElement(ink_1.Text, null, " R Full refresh (projects + branches + processes)"),
94
132
  react_1.default.createElement(ink_1.Text, null, " "),
95
- react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Details Panel Actions:"),
96
- react_1.default.createElement(ink_1.Text, null, " \u2191\u2193/kj Scroll details"),
133
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Projects View - Actions:"),
134
+ react_1.default.createElement(ink_1.Text, null, " a Add new project"),
97
135
  react_1.default.createElement(ink_1.Text, null, " e Edit project name"),
98
136
  react_1.default.createElement(ink_1.Text, null, " t Add/edit tags"),
99
137
  react_1.default.createElement(ink_1.Text, null, " o Open project in editor"),
@@ -101,16 +139,12 @@ const HelpModal = ({ onClose }) => {
101
139
  react_1.default.createElement(ink_1.Text, null, " u Show detected URLs"),
102
140
  react_1.default.createElement(ink_1.Text, null, " s Scan project for tests"),
103
141
  react_1.default.createElement(ink_1.Text, null, " p Scan ports for project"),
104
- react_1.default.createElement(ink_1.Text, null, " r Show scripts (use CLI to run)"),
142
+ react_1.default.createElement(ink_1.Text, null, " r Run scripts (select from list)"),
105
143
  react_1.default.createElement(ink_1.Text, null, " x Stop all scripts for project"),
106
- react_1.default.createElement(ink_1.Text, null, " d Delete project"),
107
- react_1.default.createElement(ink_1.Text, null, " "),
108
- react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Editing:"),
109
- react_1.default.createElement(ink_1.Text, null, " Enter Save changes"),
110
- react_1.default.createElement(ink_1.Text, null, " Esc Cancel editing"),
144
+ react_1.default.createElement(ink_1.Text, null, " d Delete project (with confirmation)"),
111
145
  react_1.default.createElement(ink_1.Text, null, " "),
112
146
  react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "General:"),
113
- react_1.default.createElement(ink_1.Text, null, " q/Esc Quit"),
147
+ react_1.default.createElement(ink_1.Text, null, " q/Esc Quit (or close modal)"),
114
148
  react_1.default.createElement(ink_1.Text, null, " ? Show this help"),
115
149
  react_1.default.createElement(ink_1.Text, null, " "),
116
150
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Press any key to close...")));
@@ -120,6 +154,264 @@ const LoadingModal = ({ message }) => {
120
154
  react_1.default.createElement(ink_1.Text, null, message),
121
155
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Please wait...")));
122
156
  };
157
+ const ConfirmModal = ({ title, message, onConfirm, onCancel }) => {
158
+ const [selected, setSelected] = (0, react_1.useState)('no');
159
+ (0, ink_1.useInput)((input, key) => {
160
+ if (key.escape || input === 'n') {
161
+ onCancel();
162
+ return;
163
+ }
164
+ if (key.return) {
165
+ if (selected === 'yes') {
166
+ onConfirm();
167
+ }
168
+ else {
169
+ onCancel();
170
+ }
171
+ return;
172
+ }
173
+ if (input === 'y') {
174
+ onConfirm();
175
+ return;
176
+ }
177
+ if (key.leftArrow || key.rightArrow || input === 'h' || input === 'l') {
178
+ setSelected(prev => prev === 'yes' ? 'no' : 'yes');
179
+ }
180
+ });
181
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentOrange, padding: 1, width: 60 },
182
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentOrange }, title),
183
+ react_1.default.createElement(ink_1.Text, null, " "),
184
+ react_1.default.createElement(ink_1.Text, null, message),
185
+ react_1.default.createElement(ink_1.Text, null, " "),
186
+ react_1.default.createElement(ink_1.Box, null,
187
+ react_1.default.createElement(ink_1.Text, { color: selected === 'yes' ? colors.accentCyan : colors.textSecondary },
188
+ selected === 'yes' ? '▶ ' : ' ',
189
+ "Yes"),
190
+ react_1.default.createElement(ink_1.Text, null, " "),
191
+ react_1.default.createElement(ink_1.Text, { color: selected === 'no' ? colors.accentCyan : colors.textSecondary },
192
+ selected === 'no' ? '▶ ' : ' ',
193
+ "No")),
194
+ react_1.default.createElement(ink_1.Text, null, " "),
195
+ react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "y/n or \u2190\u2192 to select, Enter to confirm")));
196
+ };
197
+ const AddProjectModal = ({ onAdd, onCancel }) => {
198
+ const [step, setStep] = (0, react_1.useState)('path');
199
+ const [pathInput, setPathInput] = (0, react_1.useState)('');
200
+ const [nameInput, setNameInput] = (0, react_1.useState)('');
201
+ const [error, setError] = (0, react_1.useState)(null);
202
+ (0, ink_1.useInput)((input, key) => {
203
+ if (key.escape) {
204
+ onCancel();
205
+ return;
206
+ }
207
+ if (step === 'path') {
208
+ if (key.return) {
209
+ // Validate path
210
+ const resolvedPath = pathInput.startsWith('~')
211
+ ? path.join(os.homedir(), pathInput.slice(1))
212
+ : path.resolve(pathInput);
213
+ if (!fs.existsSync(resolvedPath)) {
214
+ setError('Path does not exist');
215
+ return;
216
+ }
217
+ if (!fs.statSync(resolvedPath).isDirectory()) {
218
+ setError('Path is not a directory');
219
+ return;
220
+ }
221
+ // Check if project already exists
222
+ const db = (0, core_bridge_1.getDatabaseManager)();
223
+ const existing = db.getProjectByPath(resolvedPath);
224
+ if (existing) {
225
+ setError(`Project already exists: ${existing.name}`);
226
+ return;
227
+ }
228
+ setError(null);
229
+ setNameInput(path.basename(resolvedPath));
230
+ setStep('name');
231
+ return;
232
+ }
233
+ if (key.backspace || key.delete) {
234
+ setPathInput(prev => prev.slice(0, -1));
235
+ setError(null);
236
+ return;
237
+ }
238
+ if (key.tab) {
239
+ // Tab completion - get directories in current path
240
+ try {
241
+ const currentPath = pathInput.startsWith('~')
242
+ ? path.join(os.homedir(), pathInput.slice(1))
243
+ : pathInput || '.';
244
+ const dir = path.dirname(currentPath);
245
+ const base = path.basename(currentPath);
246
+ if (fs.existsSync(dir)) {
247
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
248
+ .filter(e => e.isDirectory() && e.name.startsWith(base) && !e.name.startsWith('.'))
249
+ .map(e => e.name);
250
+ if (entries.length === 1) {
251
+ const completed = path.join(dir, entries[0]) + '/';
252
+ setPathInput(completed.replace(os.homedir(), '~'));
253
+ }
254
+ }
255
+ }
256
+ catch {
257
+ // Ignore tab completion errors
258
+ }
259
+ return;
260
+ }
261
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
262
+ setPathInput(prev => prev + input);
263
+ setError(null);
264
+ }
265
+ }
266
+ else if (step === 'name') {
267
+ if (key.return) {
268
+ const resolvedPath = pathInput.startsWith('~')
269
+ ? path.join(os.homedir(), pathInput.slice(1))
270
+ : path.resolve(pathInput);
271
+ onAdd(resolvedPath, nameInput.trim() || undefined);
272
+ return;
273
+ }
274
+ if (key.backspace || key.delete) {
275
+ setNameInput(prev => prev.slice(0, -1));
276
+ return;
277
+ }
278
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
279
+ setNameInput(prev => prev + input);
280
+ }
281
+ }
282
+ });
283
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentCyan, padding: 1, width: 70 },
284
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, "Add Project"),
285
+ react_1.default.createElement(ink_1.Text, null, " "),
286
+ step === 'path' && (react_1.default.createElement(react_1.default.Fragment, null,
287
+ react_1.default.createElement(ink_1.Text, null, "Enter project path:"),
288
+ react_1.default.createElement(ink_1.Box, null,
289
+ react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "> "),
290
+ react_1.default.createElement(ink_1.Text, null, pathInput),
291
+ react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "_")),
292
+ error && (react_1.default.createElement(ink_1.Text, { color: "#f85149" }, error)),
293
+ react_1.default.createElement(ink_1.Text, null, " "),
294
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Tab: autocomplete | Enter: next | Esc: cancel"))),
295
+ step === 'name' && (react_1.default.createElement(react_1.default.Fragment, null,
296
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary },
297
+ "Path: ",
298
+ pathInput),
299
+ react_1.default.createElement(ink_1.Text, null, " "),
300
+ react_1.default.createElement(ink_1.Text, null, "Enter project name:"),
301
+ react_1.default.createElement(ink_1.Box, null,
302
+ react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "> "),
303
+ react_1.default.createElement(ink_1.Text, null, nameInput),
304
+ react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "_")),
305
+ react_1.default.createElement(ink_1.Text, null, " "),
306
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Enter: add project | Esc: cancel")))));
307
+ };
308
+ const SettingsModal = ({ onClose }) => {
309
+ const [settings, setSettings] = (0, react_1.useState)({
310
+ editor: { type: 'vscode' },
311
+ browser: { type: 'chrome' },
312
+ });
313
+ const [selectedSection, setSelectedSection] = (0, react_1.useState)('editor');
314
+ const [selectedOptionIndex, setSelectedOptionIndex] = (0, react_1.useState)(0);
315
+ const editorOptions = [
316
+ 'vscode',
317
+ 'cursor',
318
+ 'windsurf',
319
+ 'zed',
320
+ 'custom',
321
+ ];
322
+ const browserOptions = [
323
+ 'chrome',
324
+ 'firefox',
325
+ 'safari',
326
+ 'edge',
327
+ 'custom',
328
+ ];
329
+ (0, react_1.useEffect)(() => {
330
+ // Load settings
331
+ try {
332
+ const settingsPath = path.join(os.homedir(), '.projax', 'settings.json');
333
+ if (fs.existsSync(settingsPath)) {
334
+ const data = fs.readFileSync(settingsPath, 'utf-8');
335
+ setSettings(JSON.parse(data));
336
+ }
337
+ }
338
+ catch {
339
+ // Use defaults
340
+ }
341
+ }, []);
342
+ const saveSettings = () => {
343
+ try {
344
+ const settingsDir = path.join(os.homedir(), '.projax');
345
+ if (!fs.existsSync(settingsDir)) {
346
+ fs.mkdirSync(settingsDir, { recursive: true });
347
+ }
348
+ const settingsPath = path.join(settingsDir, 'settings.json');
349
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
350
+ onClose();
351
+ }
352
+ catch {
353
+ // Ignore save errors
354
+ }
355
+ };
356
+ (0, ink_1.useInput)((input, key) => {
357
+ if (key.escape || input === 'q') {
358
+ onClose();
359
+ return;
360
+ }
361
+ if (key.return) {
362
+ saveSettings();
363
+ return;
364
+ }
365
+ if (key.tab) {
366
+ setSelectedSection((prev) => (prev === 'editor' ? 'browser' : 'editor'));
367
+ setSelectedOptionIndex(0);
368
+ return;
369
+ }
370
+ if (key.upArrow || input === 'k') {
371
+ setSelectedOptionIndex((prev) => Math.max(0, prev - 1));
372
+ return;
373
+ }
374
+ if (key.downArrow || input === 'j') {
375
+ const maxIndex = selectedSection === 'editor' ? editorOptions.length - 1 : browserOptions.length - 1;
376
+ setSelectedOptionIndex((prev) => Math.min(maxIndex, prev + 1));
377
+ return;
378
+ }
379
+ if (input === ' ' || key.return) {
380
+ if (selectedSection === 'editor') {
381
+ setSettings({
382
+ ...settings,
383
+ editor: { type: editorOptions[selectedOptionIndex] },
384
+ });
385
+ }
386
+ else {
387
+ setSettings({
388
+ ...settings,
389
+ browser: { type: browserOptions[selectedOptionIndex] },
390
+ });
391
+ }
392
+ }
393
+ });
394
+ const currentOptions = selectedSection === 'editor' ? editorOptions : browserOptions;
395
+ const currentValue = selectedSection === 'editor' ? settings.editor.type : settings.browser.type;
396
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentCyan, padding: 1, width: 60 },
397
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, "Settings"),
398
+ react_1.default.createElement(ink_1.Text, null, " "),
399
+ react_1.default.createElement(ink_1.Box, null,
400
+ react_1.default.createElement(ink_1.Text, { color: selectedSection === 'editor' ? colors.accentCyan : colors.textTertiary, bold: selectedSection === 'editor' }, "[Editor]"),
401
+ react_1.default.createElement(ink_1.Text, null, " "),
402
+ react_1.default.createElement(ink_1.Text, { color: selectedSection === 'browser' ? colors.accentCyan : colors.textTertiary, bold: selectedSection === 'browser' }, "[Browser]")),
403
+ react_1.default.createElement(ink_1.Text, null, " "),
404
+ currentOptions.map((option, index) => {
405
+ const isSelected = index === selectedOptionIndex;
406
+ const isActive = option === currentValue;
407
+ return (react_1.default.createElement(ink_1.Text, { key: option, color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
408
+ isSelected ? '▶ ' : ' ',
409
+ isActive ? '● ' : '○ ',
410
+ option.charAt(0).toUpperCase() + option.slice(1)));
411
+ }),
412
+ react_1.default.createElement(ink_1.Text, null, " "),
413
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Tab: switch section | \u2191\u2193: select | Space: choose | Enter: save | Esc: close")));
414
+ };
123
415
  const ErrorModal = ({ message, onClose }) => {
124
416
  (0, ink_1.useInput)((input, key) => {
125
417
  if (key.escape || key.return) {
@@ -176,7 +468,7 @@ const ScriptSelectionModal = ({ scripts, projectName, projectPath, onSelect, onC
176
468
  react_1.default.createElement(ink_1.Text, null, " "),
177
469
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "\u2191\u2193/kj: Navigate | Enter: Run | b: Background | Esc/q: Cancel")));
178
470
  };
179
- const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFocused, height, scrollOffset, }) => {
471
+ const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFocused, height, scrollOffset, gitBranches, filterType, sortType, }) => {
180
472
  const { focus } = (0, ink_1.useFocus)({ id: 'projectList' });
181
473
  // Calculate visible range
182
474
  const startIndex = Math.max(0, scrollOffset);
@@ -189,10 +481,15 @@ const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFoc
189
481
  const visibleProjects = projects.slice(startIndex, endIndex);
190
482
  const hasMoreBelow = endIndex < projects.length;
191
483
  return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "35%", height: height, borderStyle: "round", borderColor: isFocused ? colors.accentCyan : colors.borderColor, padding: 1, flexShrink: 0, flexGrow: 0 },
192
- react_1.default.createElement(ink_1.Text, { bold: true, color: colors.textPrimary },
193
- "Projects (",
194
- projects.length,
195
- ")"),
484
+ react_1.default.createElement(ink_1.Box, { flexDirection: "column" },
485
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.textPrimary },
486
+ "Projects (",
487
+ projects.length,
488
+ ")"),
489
+ react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
490
+ react_1.default.createElement(ink_1.Text, { color: colors.accentPurple }, FILTER_LABELS[filterType]),
491
+ ' | ',
492
+ react_1.default.createElement(ink_1.Text, { color: colors.accentOrange }, SORT_LABELS[sortType]))),
196
493
  react_1.default.createElement(ink_1.Box, { flexDirection: "column", flexGrow: 1 }, projects.length === 0 ? (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "No projects found")) : (react_1.default.createElement(react_1.default.Fragment, null,
197
494
  hasMoreAbove && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
198
495
  "\u2191 ",
@@ -204,28 +501,37 @@ const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFoc
204
501
  // Check if this project has running scripts
205
502
  const projectRunning = runningProcesses.filter((p) => p.projectPath === project.path);
206
503
  const hasRunningScripts = projectRunning.length > 0;
207
- return (react_1.default.createElement(ink_1.Text, { key: project.id, color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
208
- isSelected ? '▶ ' : ' ',
209
- hasRunningScripts && react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF "),
210
- truncateText(project.name, 30),
211
- hasRunningScripts && react_1.default.createElement(ink_1.Text, { color: colors.accentGreen },
212
- " (",
213
- projectRunning.length,
214
- ")")));
504
+ // Get git branch
505
+ const branch = gitBranches.get(project.id);
506
+ const isMainBranch = branch === 'main' || branch === 'master';
507
+ return (react_1.default.createElement(ink_1.Box, { key: project.id, flexDirection: "column" },
508
+ react_1.default.createElement(ink_1.Text, { color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
509
+ isSelected ? '▶ ' : ' ',
510
+ hasRunningScripts && react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF "),
511
+ truncateText(project.name, 22),
512
+ hasRunningScripts && react_1.default.createElement(ink_1.Text, { color: colors.accentGreen },
513
+ " (",
514
+ projectRunning.length,
515
+ ")")),
516
+ branch && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
517
+ ' ',
518
+ react_1.default.createElement(ink_1.Text, { color: isMainBranch ? colors.accentGreen : colors.accentBlue }, truncateText(branch, 18))))));
215
519
  }),
216
520
  hasMoreBelow && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
217
521
  "\u2193 ",
218
522
  projects.length - endIndex,
219
523
  " more below")))))));
220
524
  };
221
- const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editingName, editingDescription, editingTags, editInput, allTags, onTagRemove, height, scrollOffset, }) => {
525
+ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editingName, editingDescription, editingTags, editInput, allTags, onTagRemove, height, scrollOffset, gitBranch, }) => {
222
526
  const { focus } = (0, ink_1.useFocus)({ id: 'projectDetails' });
223
527
  const [scripts, setScripts] = (0, react_1.useState)(null);
224
528
  const [ports, setPorts] = (0, react_1.useState)([]);
529
+ const [npmPackage, setNpmPackage] = (0, react_1.useState)(null);
225
530
  (0, react_1.useEffect)(() => {
226
531
  if (!project) {
227
532
  setScripts(null);
228
533
  setPorts([]);
534
+ setNpmPackage(null);
229
535
  return;
230
536
  }
231
537
  // Load scripts
@@ -245,6 +551,26 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
245
551
  catch (error) {
246
552
  setPorts([]);
247
553
  }
554
+ // Check if npm package (live registry check)
555
+ setNpmPackage(null);
556
+ try {
557
+ const packageJsonPath = path.join(project.path, 'package.json');
558
+ if (fs.existsSync(packageJsonPath)) {
559
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
560
+ if (pkg.name && !pkg.private) {
561
+ fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`, {
562
+ method: 'HEAD',
563
+ signal: AbortSignal.timeout(2000),
564
+ })
565
+ .then(res => {
566
+ if (res.ok)
567
+ setNpmPackage(pkg.name);
568
+ })
569
+ .catch(() => { });
570
+ }
571
+ }
572
+ }
573
+ catch { }
248
574
  }, [project]);
249
575
  if (!project) {
250
576
  return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: isFocused ? colors.accentCyan : colors.borderColor, padding: 1 },
@@ -299,8 +625,20 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
299
625
  react_1.default.createElement(ink_1.Text, null, " | "),
300
626
  react_1.default.createElement(ink_1.Text, null,
301
627
  "Scripts: ",
302
- react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts?.scripts?.size || 0))));
628
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts?.scripts?.size || 0)),
629
+ npmPackage && (react_1.default.createElement(react_1.default.Fragment, null,
630
+ react_1.default.createElement(ink_1.Text, null, " | "),
631
+ react_1.default.createElement(ink_1.Text, null,
632
+ "NPM: ",
633
+ react_1.default.createElement(ink_1.Text, { color: "#f85149" }, npmPackage))))));
303
634
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer2" }, " "));
635
+ // Git branch
636
+ if (gitBranch) {
637
+ const isMainBranch = gitBranch === 'main' || gitBranch === 'master';
638
+ contentLines.push(react_1.default.createElement(ink_1.Text, { key: "git-branch" },
639
+ "Branch: ",
640
+ react_1.default.createElement(ink_1.Text, { color: isMainBranch ? colors.accentGreen : colors.accentBlue }, gitBranch)));
641
+ }
304
642
  if (project.framework) {
305
643
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "framework" },
306
644
  "Framework: ",
@@ -335,34 +673,28 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
335
673
  });
336
674
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer4" }, " "));
337
675
  }
338
- // Scripts
676
+ // Scripts - show all, let virtual scrolling handle visibility
339
677
  if (scripts && scripts.scripts && scripts.scripts.size > 0) {
340
678
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "scripts-header", bold: true },
341
679
  "Available Scripts (",
342
680
  react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts.scripts.size),
343
681
  "):"));
344
- Array.from(scripts.scripts.entries()).slice(0, 5).forEach(([name, script]) => {
682
+ Array.from(scripts.scripts.entries()).forEach(([name, script]) => {
345
683
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: `script-${name}` },
346
684
  ' ',
347
685
  react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, name),
348
686
  ' - ',
349
687
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, truncateText(script.command, 60))));
350
688
  });
351
- if (scripts.scripts.size > 5) {
352
- contentLines.push(react_1.default.createElement(ink_1.Text, { key: "scripts-more", color: colors.textTertiary },
353
- " ... and ",
354
- scripts.scripts.size - 5,
355
- " more"));
356
- }
357
689
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer5" }, " "));
358
690
  }
359
- // Ports
691
+ // Ports - show all, let virtual scrolling handle visibility
360
692
  if (ports.length > 0) {
361
693
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "ports-header", bold: true },
362
694
  "Detected Ports (",
363
695
  react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, ports.length),
364
696
  "):"));
365
- ports.slice(0, 5).forEach((port) => {
697
+ ports.forEach((port) => {
366
698
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: `port-${port.id}` },
367
699
  ' ',
368
700
  "Port ",
@@ -371,12 +703,6 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
371
703
  " - ",
372
704
  truncateText(port.config_source, 50))));
373
705
  });
374
- if (ports.length > 5) {
375
- contentLines.push(react_1.default.createElement(ink_1.Text, { key: "ports-more", color: colors.textTertiary },
376
- " ... and ",
377
- ports.length - 5,
378
- " more"));
379
- }
380
706
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer6" }, " "));
381
707
  }
382
708
  // Calculate visible range for virtual scrolling
@@ -404,6 +730,80 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
404
730
  contentLines.length - endIndex,
405
731
  " more below")))));
406
732
  };
733
+ const TerminalOutputPanel = ({ processes, selectedPid, height, onSelectProcess, }) => {
734
+ const [logs, setLogs] = (0, react_1.useState)([]);
735
+ const [scrollOffset, setScrollOffset] = (0, react_1.useState)(0);
736
+ // Find the selected process or default to first
737
+ const activeProcess = selectedPid
738
+ ? processes.find((p) => p.pid === selectedPid)
739
+ : processes[0];
740
+ (0, react_1.useEffect)(() => {
741
+ if (!activeProcess?.logFile) {
742
+ setLogs(['No active process selected']);
743
+ return;
744
+ }
745
+ // Read initial logs
746
+ try {
747
+ if (fs.existsSync(activeProcess.logFile)) {
748
+ const content = fs.readFileSync(activeProcess.logFile, 'utf-8');
749
+ const lines = content.split('\n').slice(-50); // Last 50 lines
750
+ setLogs(lines);
751
+ setScrollOffset(Math.max(0, lines.length - 10));
752
+ }
753
+ else {
754
+ setLogs(['Log file not found']);
755
+ }
756
+ }
757
+ catch {
758
+ setLogs(['Error reading logs']);
759
+ }
760
+ // Watch for changes
761
+ let watcher = null;
762
+ try {
763
+ watcher = fs.watch(activeProcess.logFile, () => {
764
+ try {
765
+ const content = fs.readFileSync(activeProcess.logFile, 'utf-8');
766
+ const lines = content.split('\n').slice(-100);
767
+ setLogs(lines);
768
+ // Auto-scroll to bottom
769
+ setScrollOffset(Math.max(0, lines.length - 10));
770
+ }
771
+ catch {
772
+ // Ignore read errors during watch
773
+ }
774
+ });
775
+ }
776
+ catch {
777
+ // Ignore watch errors
778
+ }
779
+ return () => {
780
+ if (watcher) {
781
+ watcher.close();
782
+ }
783
+ };
784
+ }, [activeProcess?.logFile, activeProcess?.pid]);
785
+ const visibleLines = logs.slice(scrollOffset, scrollOffset + height - 4);
786
+ const hasMoreAbove = scrollOffset > 0;
787
+ const hasMoreBelow = scrollOffset + height - 4 < logs.length;
788
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "30%", height: height, borderStyle: "round", borderColor: colors.accentGreen, padding: 1, flexShrink: 0 },
789
+ react_1.default.createElement(ink_1.Box, { flexDirection: "row", justifyContent: "space-between" },
790
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentGreen }, "Terminal Output"),
791
+ processes.length > 1 && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
792
+ "[",
793
+ processes.findIndex((p) => p.pid === activeProcess?.pid) + 1,
794
+ "/",
795
+ processes.length,
796
+ "]"))),
797
+ activeProcess && (react_1.default.createElement(ink_1.Text, { color: colors.textSecondary },
798
+ truncateText(activeProcess.scriptName, 20),
799
+ " (PID: ",
800
+ activeProcess.pid,
801
+ ")")),
802
+ react_1.default.createElement(ink_1.Box, { flexDirection: "column", flexGrow: 1, marginTop: 1 },
803
+ hasMoreAbove && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "\u2191 more above")),
804
+ visibleLines.map((line, idx) => (react_1.default.createElement(ink_1.Text, { key: idx, color: colors.textPrimary }, truncateText(line, 40)))),
805
+ hasMoreBelow && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "\u2193 more below")))));
806
+ };
407
807
  const StatusBar = ({ focusedPanel, selectedProject }) => {
408
808
  if (focusedPanel === 'list') {
409
809
  return (react_1.default.createElement(ink_1.Box, { flexDirection: "column" },
@@ -411,16 +811,22 @@ const StatusBar = ({ focusedPanel, selectedProject }) => {
411
811
  react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF API"),
412
812
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " | "),
413
813
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Focus: "),
414
- react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Projects")),
814
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "List")),
415
815
  react_1.default.createElement(ink_1.Box, null,
816
+ react_1.default.createElement(ink_1.Text, { bold: true }, "a"),
817
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Add | "),
416
818
  react_1.default.createElement(ink_1.Text, { bold: true }, "/"),
417
819
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Search | "),
418
- react_1.default.createElement(ink_1.Text, { bold: true }, "\u2191\u2193/kj"),
419
- react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Navigate | "),
420
- react_1.default.createElement(ink_1.Text, { bold: true }, "Tab/\u2190\u2192"),
820
+ react_1.default.createElement(ink_1.Text, { bold: true }, "F"),
821
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Filter | "),
822
+ react_1.default.createElement(ink_1.Text, { bold: true }, "S"),
823
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Sort | "),
824
+ react_1.default.createElement(ink_1.Text, { bold: true }, "\u2191\u2193"),
825
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Nav | "),
826
+ react_1.default.createElement(ink_1.Text, { bold: true }, "Tab"),
421
827
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Switch | "),
422
- react_1.default.createElement(ink_1.Text, { bold: true }, "s"),
423
- react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Scan | "),
828
+ react_1.default.createElement(ink_1.Text, { bold: true }, "T"),
829
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Terminal | "),
424
830
  react_1.default.createElement(ink_1.Text, { bold: true }, "?"),
425
831
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Help | "),
426
832
  react_1.default.createElement(ink_1.Text, { bold: true }, "q"),
@@ -435,26 +841,18 @@ const StatusBar = ({ focusedPanel, selectedProject }) => {
435
841
  react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Details"),
436
842
  selectedProject && (react_1.default.createElement(react_1.default.Fragment, null,
437
843
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " | "),
438
- react_1.default.createElement(ink_1.Text, { color: colors.textPrimary }, selectedProject.name)))),
844
+ react_1.default.createElement(ink_1.Text, { color: colors.textPrimary }, truncateText(selectedProject.name, 20))))),
439
845
  react_1.default.createElement(ink_1.Box, null,
440
- react_1.default.createElement(ink_1.Text, { bold: true }, "\u2191\u2193/kj"),
441
- react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Scroll | "),
442
846
  react_1.default.createElement(ink_1.Text, { bold: true }, "e"),
443
847
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Edit | "),
444
848
  react_1.default.createElement(ink_1.Text, { bold: true }, "t"),
445
849
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Tags | "),
446
850
  react_1.default.createElement(ink_1.Text, { bold: true }, "o"),
447
851
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Editor | "),
448
- react_1.default.createElement(ink_1.Text, { bold: true }, "f"),
449
- react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Files | "),
450
- react_1.default.createElement(ink_1.Text, { bold: true }, "u"),
451
- react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " URLs | "),
852
+ react_1.default.createElement(ink_1.Text, { bold: true }, "r"),
853
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Run | "),
452
854
  react_1.default.createElement(ink_1.Text, { bold: true }, "s"),
453
855
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Scan | "),
454
- react_1.default.createElement(ink_1.Text, { bold: true }, "p"),
455
- react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Ports | "),
456
- react_1.default.createElement(ink_1.Text, { bold: true }, "r"),
457
- react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Scripts | "),
458
856
  react_1.default.createElement(ink_1.Text, { bold: true }, "x"),
459
857
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Stop | "),
460
858
  react_1.default.createElement(ink_1.Text, { bold: true }, "d"),
@@ -462,9 +860,7 @@ const StatusBar = ({ focusedPanel, selectedProject }) => {
462
860
  react_1.default.createElement(ink_1.Text, { bold: true }, "Tab"),
463
861
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Switch | "),
464
862
  react_1.default.createElement(ink_1.Text, { bold: true }, "?"),
465
- react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Help | "),
466
- react_1.default.createElement(ink_1.Text, { bold: true }, "q"),
467
- react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Quit"))));
863
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Help"))));
468
864
  };
469
865
  // Simple fuzzy search function
470
866
  function fuzzyMatch(query, text) {
@@ -492,6 +888,13 @@ const App = () => {
492
888
  const [error, setError] = (0, react_1.useState)(null);
493
889
  const [runningProcesses, setRunningProcesses] = (0, react_1.useState)([]);
494
890
  const [focusedPanel, setFocusedPanel] = (0, react_1.useState)('list');
891
+ // View state
892
+ const [currentView, setCurrentView] = (0, react_1.useState)('projects');
893
+ // Git branches
894
+ const [gitBranches, setGitBranches] = (0, react_1.useState)(new Map());
895
+ // Filter and sort state
896
+ const [filterType, setFilterType] = (0, react_1.useState)('all');
897
+ const [sortType, setSortType] = (0, react_1.useState)('name-asc');
495
898
  // Editing state
496
899
  const [editingName, setEditingName] = (0, react_1.useState)(false);
497
900
  const [editingDescription, setEditingDescription] = (0, react_1.useState)(false);
@@ -499,6 +902,9 @@ const App = () => {
499
902
  const [editInput, setEditInput] = (0, react_1.useState)('');
500
903
  const [showUrls, setShowUrls] = (0, react_1.useState)(false);
501
904
  const [allTags, setAllTags] = (0, react_1.useState)([]);
905
+ // Modal state
906
+ const [showAddProjectModal, setShowAddProjectModal] = (0, react_1.useState)(false);
907
+ const [showConfirmDelete, setShowConfirmDelete] = (0, react_1.useState)(false);
502
908
  // Search state
503
909
  const [showSearch, setShowSearch] = (0, react_1.useState)(false);
504
910
  const [searchQuery, setSearchQuery] = (0, react_1.useState)('');
@@ -507,19 +913,47 @@ const App = () => {
507
913
  // Script selection state
508
914
  const [showScriptModal, setShowScriptModal] = (0, react_1.useState)(false);
509
915
  const [scriptModalData, setScriptModalData] = (0, react_1.useState)(null);
916
+ // Workspace state
917
+ const [workspaces, setWorkspaces] = (0, react_1.useState)([]);
918
+ const [selectedWorkspaceIndex, setSelectedWorkspaceIndex] = (0, react_1.useState)(0);
919
+ // Terminal panel state
920
+ const [showTerminalPanel, setShowTerminalPanel] = (0, react_1.useState)(false);
921
+ const [terminalLogs, setTerminalLogs] = (0, react_1.useState)([]);
922
+ const [selectedProcessPid, setSelectedProcessPid] = (0, react_1.useState)(null);
923
+ // Settings state
924
+ const [showSettings, setShowSettings] = (0, react_1.useState)(false);
510
925
  // Get terminal dimensions
511
926
  const terminalHeight = process.stdout.rows || 24;
512
- const availableHeight = terminalHeight - 3; // Subtract status bar
927
+ const availableHeight = terminalHeight - 4; // Subtract status bar (increased for view indicator)
513
928
  (0, react_1.useEffect)(() => {
514
929
  loadProjects();
515
930
  loadRunningProcesses();
516
931
  loadAllTags();
517
- // Refresh running processes every 5 seconds
932
+ // Refresh running processes and git branches every 5 seconds
518
933
  const interval = setInterval(() => {
519
934
  loadRunningProcesses();
520
935
  }, 5000);
521
936
  return () => clearInterval(interval);
522
937
  }, []);
938
+ // Load git branches when projects change
939
+ (0, react_1.useEffect)(() => {
940
+ if (allProjects.length > 0) {
941
+ loadGitBranches();
942
+ }
943
+ }, [allProjects]);
944
+ const loadGitBranches = async () => {
945
+ const branches = new Map();
946
+ for (const project of allProjects) {
947
+ try {
948
+ const branch = (0, core_bridge_1.getCurrentBranch)(project.path);
949
+ branches.set(project.id, branch);
950
+ }
951
+ catch {
952
+ branches.set(project.id, null);
953
+ }
954
+ }
955
+ setGitBranches(branches);
956
+ };
523
957
  // Reset editing state and scroll when project changes
524
958
  (0, react_1.useEffect)(() => {
525
959
  setEditingName(false);
@@ -528,6 +962,12 @@ const App = () => {
528
962
  setEditInput('');
529
963
  setDetailsScrollOffset(0); // Reset scroll when switching projects
530
964
  }, [selectedIndex]);
965
+ // Load workspaces when switching to workspaces view
966
+ (0, react_1.useEffect)(() => {
967
+ if (currentView === 'workspaces' && workspaces.length === 0) {
968
+ loadWorkspacesFromApi();
969
+ }
970
+ }, [currentView]);
531
971
  // Update scroll offset when selected index changes
532
972
  (0, react_1.useEffect)(() => {
533
973
  const visibleHeight = Math.max(1, availableHeight - 3);
@@ -560,26 +1000,71 @@ const App = () => {
560
1000
  const loadProjects = () => {
561
1001
  const loadedProjects = (0, core_bridge_1.getAllProjects)();
562
1002
  setAllProjects(loadedProjects);
563
- filterProjects(loadedProjects, searchQuery);
1003
+ applyFilterAndSort(loadedProjects, searchQuery, filterType, sortType);
564
1004
  };
565
- const filterProjects = (projectsToFilter, query) => {
566
- if (!query.trim()) {
567
- setProjects(projectsToFilter);
568
- return;
1005
+ const applyFilterAndSort = (projectsToFilter, query, filter, sort) => {
1006
+ let filtered = projectsToFilter;
1007
+ // Apply search query with filter type
1008
+ if (query.trim()) {
1009
+ const q = query.toLowerCase();
1010
+ filtered = projectsToFilter.filter(project => {
1011
+ switch (filter) {
1012
+ case 'name':
1013
+ return fuzzyMatch(q, project.name);
1014
+ case 'path':
1015
+ return fuzzyMatch(q, project.path);
1016
+ case 'tags':
1017
+ return project.tags?.some((tag) => fuzzyMatch(q, tag)) || false;
1018
+ case 'ports': {
1019
+ // Check if project has ports matching the query
1020
+ const db = (0, core_bridge_1.getDatabaseManager)();
1021
+ const ports = db.getProjectPorts(project.id);
1022
+ return ports.some((p) => p.port.toString().includes(q));
1023
+ }
1024
+ case 'running': {
1025
+ const isRunning = runningProcesses.some((p) => p.projectPath === project.path);
1026
+ return (q === 'running' || q === 'yes' || q === 'true') ? isRunning : !isRunning;
1027
+ }
1028
+ case 'all':
1029
+ default:
1030
+ return (fuzzyMatch(q, project.name) ||
1031
+ (project.description ? fuzzyMatch(q, project.description) : false) ||
1032
+ fuzzyMatch(q, project.path) ||
1033
+ project.tags?.some((tag) => fuzzyMatch(q, tag)) ||
1034
+ false);
1035
+ }
1036
+ });
569
1037
  }
570
- const filtered = projectsToFilter.filter(project => {
571
- const nameMatch = fuzzyMatch(query, project.name);
572
- const descMatch = project.description ? fuzzyMatch(query, project.description) : false;
573
- const pathMatch = fuzzyMatch(query, project.path);
574
- const tagsMatch = project.tags?.some((tag) => fuzzyMatch(query, tag)) || false;
575
- return nameMatch || descMatch || pathMatch || tagsMatch;
1038
+ // Apply sorting
1039
+ const sorted = [...filtered].sort((a, b) => {
1040
+ switch (sort) {
1041
+ case 'name-asc':
1042
+ return a.name.localeCompare(b.name);
1043
+ case 'name-desc':
1044
+ return b.name.localeCompare(a.name);
1045
+ case 'recent':
1046
+ return (b.last_scanned || 0) - (a.last_scanned || 0);
1047
+ case 'oldest':
1048
+ return (a.created_at || 0) - (b.created_at || 0);
1049
+ case 'running': {
1050
+ const aRunning = runningProcesses.filter((p) => p.projectPath === a.path).length;
1051
+ const bRunning = runningProcesses.filter((p) => p.projectPath === b.path).length;
1052
+ return bRunning - aRunning;
1053
+ }
1054
+ default:
1055
+ return 0;
1056
+ }
576
1057
  });
577
- setProjects(filtered);
1058
+ setProjects(sorted);
578
1059
  // Adjust selected index if current selection is out of bounds
579
- if (selectedIndex >= filtered.length) {
580
- setSelectedIndex(Math.max(0, filtered.length - 1));
1060
+ if (selectedIndex >= sorted.length) {
1061
+ setSelectedIndex(Math.max(0, sorted.length - 1));
581
1062
  }
582
1063
  };
1064
+ // Re-apply filter/sort when dependencies change
1065
+ (0, react_1.useEffect)(() => {
1066
+ applyFilterAndSort(allProjects, searchQuery, filterType, sortType);
1067
+ }, [filterType, sortType, runningProcesses]);
583
1068
  const loadRunningProcesses = async () => {
584
1069
  try {
585
1070
  const processes = await (0, script_runner_1.getRunningProcessesClean)();
@@ -726,13 +1211,83 @@ const App = () => {
726
1211
  setError(err instanceof Error ? err.message : String(err));
727
1212
  }
728
1213
  };
1214
+ // Handler for adding a project
1215
+ const handleAddProject = async (projectPath, projectName) => {
1216
+ setShowAddProjectModal(false);
1217
+ setIsLoading(true);
1218
+ setLoadingMessage('Adding project...');
1219
+ try {
1220
+ const name = projectName || path.basename(projectPath);
1221
+ const db = (0, core_bridge_1.getDatabaseManager)();
1222
+ const project = db.addProject(name, projectPath);
1223
+ // Scan for tests
1224
+ setLoadingMessage('Scanning for tests...');
1225
+ await (0, core_bridge_1.scanProject)(project.id);
1226
+ // Scan for ports
1227
+ setLoadingMessage('Scanning for ports...');
1228
+ try {
1229
+ const { scanProjectPorts } = await Promise.resolve().then(() => __importStar(require('./port-scanner')));
1230
+ await scanProjectPorts(project.id);
1231
+ }
1232
+ catch {
1233
+ // Ignore port scanning errors
1234
+ }
1235
+ loadProjects();
1236
+ setIsLoading(false);
1237
+ // Select the newly added project
1238
+ const newProjects = (0, core_bridge_1.getAllProjects)();
1239
+ const newIndex = newProjects.findIndex((p) => p.id === project.id);
1240
+ if (newIndex >= 0) {
1241
+ setSelectedIndex(newIndex);
1242
+ }
1243
+ }
1244
+ catch (err) {
1245
+ setIsLoading(false);
1246
+ setError(err instanceof Error ? err.message : String(err));
1247
+ }
1248
+ };
1249
+ // Handler for deleting a project
1250
+ const handleDeleteProject = () => {
1251
+ if (!selectedProject)
1252
+ return;
1253
+ setShowConfirmDelete(false);
1254
+ setIsLoading(true);
1255
+ setLoadingMessage(`Deleting ${selectedProject.name}...`);
1256
+ setTimeout(async () => {
1257
+ try {
1258
+ const db = (0, core_bridge_1.getDatabaseManager)();
1259
+ db.removeProject(selectedProject.id);
1260
+ loadProjects();
1261
+ if (selectedIndex >= projects.length - 1) {
1262
+ setSelectedIndex(Math.max(0, projects.length - 2));
1263
+ }
1264
+ setIsLoading(false);
1265
+ }
1266
+ catch (err) {
1267
+ setIsLoading(false);
1268
+ setError(err instanceof Error ? err.message : String(err));
1269
+ }
1270
+ }, 100);
1271
+ };
1272
+ // Cycle filter type
1273
+ const cycleFilterType = () => {
1274
+ const currentIndex = FILTER_TYPES.indexOf(filterType);
1275
+ const nextIndex = (currentIndex + 1) % FILTER_TYPES.length;
1276
+ setFilterType(FILTER_TYPES[nextIndex]);
1277
+ };
1278
+ // Cycle sort type
1279
+ const cycleSortType = () => {
1280
+ const currentIndex = SORT_TYPES.indexOf(sortType);
1281
+ const nextIndex = (currentIndex + 1) % SORT_TYPES.length;
1282
+ setSortType(SORT_TYPES[nextIndex]);
1283
+ };
729
1284
  (0, ink_1.useInput)((input, key) => {
730
1285
  // Handle search mode
731
1286
  if (showSearch) {
732
1287
  if (key.escape) {
733
1288
  setShowSearch(false);
734
1289
  setSearchQuery('');
735
- filterProjects(allProjects, '');
1290
+ applyFilterAndSort(allProjects, '', filterType, sortType);
736
1291
  return;
737
1292
  }
738
1293
  if (key.return) {
@@ -742,19 +1297,19 @@ const App = () => {
742
1297
  if (key.backspace || key.delete) {
743
1298
  const newQuery = searchQuery.slice(0, -1);
744
1299
  setSearchQuery(newQuery);
745
- filterProjects(allProjects, newQuery);
1300
+ applyFilterAndSort(allProjects, newQuery, filterType, sortType);
746
1301
  return;
747
1302
  }
748
1303
  if (input && input.length === 1 && !key.ctrl && !key.meta) {
749
1304
  const newQuery = searchQuery + input;
750
1305
  setSearchQuery(newQuery);
751
- filterProjects(allProjects, newQuery);
1306
+ applyFilterAndSort(allProjects, newQuery, filterType, sortType);
752
1307
  return;
753
1308
  }
754
1309
  return;
755
1310
  }
756
1311
  // Don't process input if modal is showing
757
- if (showHelp || isLoading || error || showUrls || showScriptModal) {
1312
+ if (showHelp || isLoading || error || showUrls || showScriptModal || showAddProjectModal || showConfirmDelete || showSettings) {
758
1313
  // Handle URLs modal
759
1314
  if (showUrls && (key.escape || key.return || input === 'q' || input === 'u')) {
760
1315
  setShowUrls(false);
@@ -762,12 +1317,95 @@ const App = () => {
762
1317
  }
763
1318
  return;
764
1319
  }
1320
+ // Handle navigation in workspaces view
1321
+ if (currentView === 'workspaces') {
1322
+ if (key.upArrow || input === 'k') {
1323
+ setSelectedWorkspaceIndex((prev) => Math.max(0, prev - 1));
1324
+ return;
1325
+ }
1326
+ if (key.downArrow || input === 'j') {
1327
+ setSelectedWorkspaceIndex((prev) => Math.min(workspaces.length - 1, prev + 1));
1328
+ return;
1329
+ }
1330
+ }
1331
+ // Handle navigation in processes view
1332
+ if (currentView === 'processes') {
1333
+ if (input === 'x' && runningProcesses.length > 0) {
1334
+ // Stop all processes (or could select one)
1335
+ setIsLoading(true);
1336
+ setLoadingMessage('Stopping processes...');
1337
+ setTimeout(async () => {
1338
+ try {
1339
+ for (const proc of runningProcesses) {
1340
+ await (0, script_runner_1.stopScript)(proc.pid);
1341
+ }
1342
+ await loadRunningProcesses();
1343
+ setIsLoading(false);
1344
+ }
1345
+ catch (err) {
1346
+ setIsLoading(false);
1347
+ setError(err instanceof Error ? err.message : String(err));
1348
+ }
1349
+ }, 100);
1350
+ return;
1351
+ }
1352
+ }
1353
+ // Global navigation - number keys for view switching
1354
+ if (input === '1') {
1355
+ setCurrentView('projects');
1356
+ return;
1357
+ }
1358
+ if (input === '2') {
1359
+ setCurrentView('workspaces');
1360
+ return;
1361
+ }
1362
+ if (input === '3') {
1363
+ setCurrentView('processes');
1364
+ return;
1365
+ }
1366
+ if (input === '4') {
1367
+ setCurrentView('settings');
1368
+ setShowSettings(true);
1369
+ return;
1370
+ }
1371
+ // Terminal panel toggle
1372
+ if (input === 'T') {
1373
+ setShowTerminalPanel(prev => !prev);
1374
+ return;
1375
+ }
765
1376
  // Search shortcut
766
1377
  if (input === '/') {
767
1378
  setShowSearch(true);
768
1379
  setSearchQuery('');
769
1380
  return;
770
1381
  }
1382
+ // Add project shortcut (in projects view)
1383
+ if (input === 'a' && currentView === 'projects') {
1384
+ setShowAddProjectModal(true);
1385
+ return;
1386
+ }
1387
+ // Filter cycle shortcut
1388
+ if (input === 'F' && currentView === 'projects') {
1389
+ cycleFilterType();
1390
+ return;
1391
+ }
1392
+ // Sort cycle shortcut
1393
+ if (input === 'S' && currentView === 'projects') {
1394
+ cycleSortType();
1395
+ return;
1396
+ }
1397
+ // Refresh git branches
1398
+ if (input === 'g' && currentView === 'projects') {
1399
+ loadGitBranches();
1400
+ return;
1401
+ }
1402
+ // Full refresh
1403
+ if (input === 'R') {
1404
+ loadProjects();
1405
+ loadRunningProcesses();
1406
+ loadGitBranches();
1407
+ return;
1408
+ }
771
1409
  // Handle editing modes
772
1410
  if (editingName || editingDescription || editingTags) {
773
1411
  if (key.escape) {
@@ -924,25 +1562,9 @@ const App = () => {
924
1562
  setShowUrls(true);
925
1563
  return;
926
1564
  }
927
- // Delete project
1565
+ // Delete project (with confirmation)
928
1566
  if (input === 'd') {
929
- setIsLoading(true);
930
- setLoadingMessage(`Deleting ${selectedProject.name}...`);
931
- setTimeout(async () => {
932
- try {
933
- const db = (0, core_bridge_1.getDatabaseManager)();
934
- db.removeProject(selectedProject.id);
935
- loadProjects();
936
- if (selectedIndex >= projects.length - 1) {
937
- setSelectedIndex(Math.max(0, projects.length - 2));
938
- }
939
- setIsLoading(false);
940
- }
941
- catch (err) {
942
- setIsLoading(false);
943
- setError(err instanceof Error ? err.message : String(err));
944
- }
945
- }, 100);
1567
+ setShowConfirmDelete(true);
946
1568
  return;
947
1569
  }
948
1570
  }
@@ -1032,6 +1654,13 @@ const App = () => {
1032
1654
  return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
1033
1655
  react_1.default.createElement(HelpModal, { onClose: () => setShowHelp(false) })));
1034
1656
  }
1657
+ if (showSettings) {
1658
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
1659
+ react_1.default.createElement(SettingsModal, { onClose: () => {
1660
+ setShowSettings(false);
1661
+ setCurrentView('projects');
1662
+ } })));
1663
+ }
1035
1664
  if (isLoading) {
1036
1665
  return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
1037
1666
  react_1.default.createElement(LoadingModal, { message: loadingMessage })));
@@ -1076,6 +1705,14 @@ const App = () => {
1076
1705
  react_1.default.createElement(ink_1.Text, null, " "),
1077
1706
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Press Esc or u to close..."))));
1078
1707
  }
1708
+ if (showAddProjectModal) {
1709
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
1710
+ react_1.default.createElement(AddProjectModal, { onAdd: handleAddProject, onCancel: () => setShowAddProjectModal(false) })));
1711
+ }
1712
+ if (showConfirmDelete && selectedProject) {
1713
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
1714
+ react_1.default.createElement(ConfirmModal, { title: "Delete Project", message: `Are you sure you want to remove "${selectedProject.name}" from the dashboard?`, onConfirm: handleDeleteProject, onCancel: () => setShowConfirmDelete(false) })));
1715
+ }
1079
1716
  if (showScriptModal && scriptModalData) {
1080
1717
  return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
1081
1718
  react_1.default.createElement(ScriptSelectionModal, { scripts: scriptModalData.scripts, projectName: scriptModalData.projectName, projectPath: scriptModalData.projectPath, onSelect: handleScriptSelect, onClose: () => setShowScriptModal(false) })));
@@ -1094,11 +1731,109 @@ const App = () => {
1094
1731
  }
1095
1732
  }
1096
1733
  };
1097
- return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", height: terminalHeight },
1098
- react_1.default.createElement(ink_1.Box, { flexDirection: "row", height: availableHeight, flexGrow: 0, flexShrink: 0 },
1099
- react_1.default.createElement(ProjectListComponent, { projects: projects, selectedIndex: selectedIndex, runningProcesses: runningProcesses, isFocused: focusedPanel === 'list', height: availableHeight, scrollOffset: listScrollOffset }),
1734
+ // Render Projects view
1735
+ const renderProjectsView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "row", height: availableHeight, flexGrow: 0, flexShrink: 0 },
1736
+ react_1.default.createElement(ProjectListComponent, { projects: projects, selectedIndex: selectedIndex, runningProcesses: runningProcesses, isFocused: focusedPanel === 'list', height: availableHeight, scrollOffset: listScrollOffset, gitBranches: gitBranches, filterType: filterType, sortType: sortType }),
1737
+ react_1.default.createElement(ink_1.Box, { width: 1 }),
1738
+ react_1.default.createElement(ProjectDetailsComponent, { project: selectedProject, runningProcesses: runningProcesses, isFocused: focusedPanel === 'details', editingName: editingName, editingDescription: editingDescription, editingTags: editingTags, editInput: editInput, allTags: allTags, onTagRemove: handleTagRemove, height: availableHeight, scrollOffset: detailsScrollOffset, gitBranch: selectedProject ? gitBranches.get(selectedProject.id) || null : null }),
1739
+ showTerminalPanel && (react_1.default.createElement(react_1.default.Fragment, null,
1100
1740
  react_1.default.createElement(ink_1.Box, { width: 1 }),
1101
- react_1.default.createElement(ProjectDetailsComponent, { project: selectedProject, runningProcesses: runningProcesses, isFocused: focusedPanel === 'details', editingName: editingName, editingDescription: editingDescription, editingTags: editingTags, editInput: editInput, allTags: allTags, onTagRemove: handleTagRemove, height: availableHeight, scrollOffset: detailsScrollOffset })),
1741
+ react_1.default.createElement(TerminalOutputPanel, { processes: runningProcesses, selectedPid: selectedProcessPid, height: availableHeight, onSelectProcess: (pid) => setSelectedProcessPid(pid) })))));
1742
+ // Render Workspaces view
1743
+ const renderWorkspacesView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "row", height: availableHeight, flexGrow: 0, flexShrink: 0 },
1744
+ react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "35%", height: availableHeight, borderStyle: "round", borderColor: colors.accentCyan, padding: 1 },
1745
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.textPrimary },
1746
+ "Workspaces (",
1747
+ workspaces.length,
1748
+ ")"),
1749
+ react_1.default.createElement(ink_1.Box, { flexDirection: "column", flexGrow: 1 }, workspaces.length === 0 ? (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "No workspaces found")) : (workspaces.map((ws, index) => {
1750
+ const isSelected = index === selectedWorkspaceIndex;
1751
+ return (react_1.default.createElement(ink_1.Text, { key: ws.id, color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
1752
+ isSelected ? '▶ ' : ' ',
1753
+ truncateText(ws.name, 25)));
1754
+ })))),
1755
+ react_1.default.createElement(ink_1.Box, { width: 1 }),
1756
+ react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "65%", height: availableHeight, borderStyle: "round", borderColor: colors.borderColor, padding: 1 }, workspaces[selectedWorkspaceIndex] ? (react_1.default.createElement(react_1.default.Fragment, null,
1757
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, workspaces[selectedWorkspaceIndex].name),
1758
+ react_1.default.createElement(ink_1.Text, null, " "),
1759
+ workspaces[selectedWorkspaceIndex].description && (react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, workspaces[selectedWorkspaceIndex].description)),
1760
+ react_1.default.createElement(ink_1.Text, null, " "),
1761
+ react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
1762
+ "Path: ",
1763
+ getDisplayPath(workspaces[selectedWorkspaceIndex].workspace_file_path)))) : (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "Select a workspace")))));
1764
+ // Load workspaces from API
1765
+ const loadWorkspacesFromApi = async () => {
1766
+ try {
1767
+ // Try common API ports
1768
+ const ports = [38124, 38125, 38126, 38127, 38128, 3001];
1769
+ let apiBaseUrl = '';
1770
+ for (const port of ports) {
1771
+ try {
1772
+ const response = await fetch(`http://localhost:${port}/health`, {
1773
+ signal: AbortSignal.timeout(500),
1774
+ });
1775
+ if (response.ok) {
1776
+ apiBaseUrl = `http://localhost:${port}/api`;
1777
+ break;
1778
+ }
1779
+ }
1780
+ catch {
1781
+ continue;
1782
+ }
1783
+ }
1784
+ if (!apiBaseUrl) {
1785
+ return;
1786
+ }
1787
+ const response = await fetch(`${apiBaseUrl}/workspaces`);
1788
+ if (response.ok) {
1789
+ const ws = (await response.json());
1790
+ setWorkspaces(ws);
1791
+ }
1792
+ }
1793
+ catch {
1794
+ // Ignore workspace loading errors
1795
+ }
1796
+ };
1797
+ // Render Processes view placeholder
1798
+ const renderProcessesView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 2 },
1799
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan },
1800
+ "Running Processes (",
1801
+ runningProcesses.length,
1802
+ ")"),
1803
+ react_1.default.createElement(ink_1.Text, null, " "),
1804
+ runningProcesses.length === 0 ? (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "No running processes")) : (runningProcesses.map((proc) => {
1805
+ const uptime = Math.floor((Date.now() - proc.startedAt) / 1000);
1806
+ const minutes = Math.floor(uptime / 60);
1807
+ const seconds = uptime % 60;
1808
+ const uptimeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
1809
+ return (react_1.default.createElement(ink_1.Text, { key: proc.pid, color: colors.textPrimary },
1810
+ react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF"),
1811
+ " PID ",
1812
+ proc.pid,
1813
+ ": ",
1814
+ proc.projectName,
1815
+ " (",
1816
+ proc.scriptName,
1817
+ ") - ",
1818
+ uptimeStr));
1819
+ })),
1820
+ react_1.default.createElement(ink_1.Text, null, " "),
1821
+ react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "Press 1 to return to Projects")));
1822
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", height: terminalHeight },
1823
+ react_1.default.createElement(ink_1.Box, { paddingX: 1, height: 1 },
1824
+ react_1.default.createElement(ink_1.Text, { color: currentView === 'projects' ? colors.accentCyan : colors.textTertiary }, "[1] Projects"),
1825
+ react_1.default.createElement(ink_1.Text, null, " "),
1826
+ react_1.default.createElement(ink_1.Text, { color: currentView === 'workspaces' ? colors.accentCyan : colors.textTertiary }, "[2] Workspaces"),
1827
+ react_1.default.createElement(ink_1.Text, null, " "),
1828
+ react_1.default.createElement(ink_1.Text, { color: currentView === 'processes' ? colors.accentCyan : colors.textTertiary }, "[3] Processes"),
1829
+ react_1.default.createElement(ink_1.Text, null, " "),
1830
+ react_1.default.createElement(ink_1.Text, { color: currentView === 'settings' ? colors.accentCyan : colors.textTertiary }, "[4] Settings"),
1831
+ showTerminalPanel && (react_1.default.createElement(react_1.default.Fragment, null,
1832
+ react_1.default.createElement(ink_1.Text, null, " | "),
1833
+ react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "Terminal [T]")))),
1834
+ currentView === 'projects' && renderProjectsView(),
1835
+ currentView === 'workspaces' && renderWorkspacesView(),
1836
+ currentView === 'processes' && renderProcessesView(),
1102
1837
  react_1.default.createElement(ink_1.Box, { paddingX: 1, borderStyle: "single", borderColor: colors.borderColor, flexShrink: 0, height: 3 },
1103
1838
  react_1.default.createElement(StatusBar, { focusedPanel: focusedPanel, selectedProject: selectedProject }))));
1104
1839
  };