projax 3.3.58 → 3.3.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +10 -1
  2. package/dist/electron/preload.d.ts +1 -0
  3. package/dist/electron/renderer/assets/index-CmtZriN5.js +66 -0
  4. package/dist/electron/renderer/index.html +1 -1
  5. package/dist/electron/script-runner.js +52 -20
  6. package/dist/index.js +14 -10
  7. package/dist/prxi.js +877 -109
  8. package/dist/prxi.tsx +1249 -177
  9. package/dist/script-runner.js +52 -20
  10. package/package.json +1 -1
  11. package/coverage/base.css +0 -224
  12. package/coverage/block-navigation.js +0 -87
  13. package/coverage/core-bridge.ts.html +0 -292
  14. package/coverage/favicon.png +0 -0
  15. package/coverage/index.html +0 -191
  16. package/coverage/lcov-report/base.css +0 -224
  17. package/coverage/lcov-report/block-navigation.js +0 -87
  18. package/coverage/lcov-report/core-bridge.ts.html +0 -292
  19. package/coverage/lcov-report/favicon.png +0 -0
  20. package/coverage/lcov-report/index.html +0 -191
  21. package/coverage/lcov-report/port-extractor.ts.html +0 -1174
  22. package/coverage/lcov-report/port-scanner.ts.html +0 -301
  23. package/coverage/lcov-report/port-utils.ts.html +0 -670
  24. package/coverage/lcov-report/prettify.css +0 -1
  25. package/coverage/lcov-report/prettify.js +0 -2
  26. package/coverage/lcov-report/script-runner.ts.html +0 -3346
  27. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  28. package/coverage/lcov-report/sorter.js +0 -210
  29. package/coverage/lcov-report/test-parser.ts.html +0 -799
  30. package/coverage/lcov.info +0 -1338
  31. package/coverage/port-extractor.ts.html +0 -1174
  32. package/coverage/port-scanner.ts.html +0 -301
  33. package/coverage/port-utils.ts.html +0 -670
  34. package/coverage/prettify.css +0 -1
  35. package/coverage/prettify.js +0 -2
  36. package/coverage/script-runner.ts.html +0 -3346
  37. package/coverage/sort-arrow-sprite.png +0 -0
  38. package/coverage/sorter.js +0 -210
  39. package/coverage/test-parser.ts.html +0 -799
  40. package/dist/__tests__/core-bridge.test.d.ts +0 -1
  41. package/dist/__tests__/core-bridge.test.js +0 -135
  42. package/dist/__tests__/port-extractor.test.d.ts +0 -1
  43. package/dist/__tests__/port-extractor.test.js +0 -407
  44. package/dist/__tests__/port-scanner.test.d.ts +0 -1
  45. package/dist/__tests__/port-scanner.test.js +0 -170
  46. package/dist/__tests__/port-utils.test.d.ts +0 -1
  47. package/dist/__tests__/port-utils.test.js +0 -127
  48. package/dist/__tests__/script-runner.test.d.ts +0 -1
  49. package/dist/__tests__/script-runner.test.js +0 -491
  50. package/dist/__tests__/test-parser.test.d.ts +0 -1
  51. package/dist/__tests__/test-parser.test.js +0 -276
  52. package/dist/api/__tests__/database.test.d.ts +0 -2
  53. package/dist/api/__tests__/database.test.d.ts.map +0 -1
  54. package/dist/api/__tests__/database.test.js +0 -485
  55. package/dist/api/__tests__/database.test.js.map +0 -1
  56. package/dist/api/__tests__/routes.test.d.ts +0 -2
  57. package/dist/api/__tests__/routes.test.d.ts.map +0 -1
  58. package/dist/api/__tests__/routes.test.js +0 -484
  59. package/dist/api/__tests__/routes.test.js.map +0 -1
  60. package/dist/api/__tests__/scanner.test.d.ts +0 -2
  61. package/dist/api/__tests__/scanner.test.d.ts.map +0 -1
  62. package/dist/api/__tests__/scanner.test.js +0 -403
  63. package/dist/api/__tests__/scanner.test.js.map +0 -1
  64. package/dist/core/__tests__/database.test.d.ts +0 -1
  65. package/dist/core/__tests__/database.test.js +0 -557
  66. package/dist/core/__tests__/detector.test.d.ts +0 -1
  67. package/dist/core/__tests__/detector.test.js +0 -375
  68. package/dist/core/__tests__/index.test.d.ts +0 -1
  69. package/dist/core/__tests__/index.test.js +0 -469
  70. package/dist/core/__tests__/scanner.test.d.ts +0 -1
  71. package/dist/core/__tests__/scanner.test.js +0 -406
  72. package/dist/core/__tests__/settings.test.d.ts +0 -1
  73. package/dist/core/__tests__/settings.test.js +0 -280
  74. package/dist/electron/core/__tests__/database.test.d.ts +0 -1
  75. package/dist/electron/core/__tests__/database.test.js +0 -557
  76. package/dist/electron/core/__tests__/detector.test.d.ts +0 -1
  77. package/dist/electron/core/__tests__/detector.test.js +0 -375
  78. package/dist/electron/core/__tests__/index.test.d.ts +0 -1
  79. package/dist/electron/core/__tests__/index.test.js +0 -469
  80. package/dist/electron/core/__tests__/scanner.test.d.ts +0 -1
  81. package/dist/electron/core/__tests__/scanner.test.js +0 -406
  82. package/dist/electron/core/__tests__/settings.test.d.ts +0 -1
  83. package/dist/electron/core/__tests__/settings.test.js +0 -280
  84. 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,157 @@ 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
+ };
123
308
  const ErrorModal = ({ message, onClose }) => {
124
309
  (0, ink_1.useInput)((input, key) => {
125
310
  if (key.escape || key.return) {
@@ -176,7 +361,7 @@ const ScriptSelectionModal = ({ scripts, projectName, projectPath, onSelect, onC
176
361
  react_1.default.createElement(ink_1.Text, null, " "),
177
362
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "\u2191\u2193/kj: Navigate | Enter: Run | b: Background | Esc/q: Cancel")));
178
363
  };
179
- const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFocused, height, scrollOffset, }) => {
364
+ const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFocused, height, scrollOffset, gitBranches, filterType, sortType, }) => {
180
365
  const { focus } = (0, ink_1.useFocus)({ id: 'projectList' });
181
366
  // Calculate visible range
182
367
  const startIndex = Math.max(0, scrollOffset);
@@ -189,10 +374,15 @@ const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFoc
189
374
  const visibleProjects = projects.slice(startIndex, endIndex);
190
375
  const hasMoreBelow = endIndex < projects.length;
191
376
  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
- ")"),
377
+ react_1.default.createElement(ink_1.Box, { flexDirection: "column" },
378
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.textPrimary },
379
+ "Projects (",
380
+ projects.length,
381
+ ")"),
382
+ react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
383
+ react_1.default.createElement(ink_1.Text, { color: colors.accentPurple }, FILTER_LABELS[filterType]),
384
+ ' | ',
385
+ react_1.default.createElement(ink_1.Text, { color: colors.accentOrange }, SORT_LABELS[sortType]))),
196
386
  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
387
  hasMoreAbove && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
198
388
  "\u2191 ",
@@ -204,28 +394,37 @@ const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFoc
204
394
  // Check if this project has running scripts
205
395
  const projectRunning = runningProcesses.filter((p) => p.projectPath === project.path);
206
396
  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
- ")")));
397
+ // Get git branch
398
+ const branch = gitBranches.get(project.id);
399
+ const isMainBranch = branch === 'main' || branch === 'master';
400
+ return (react_1.default.createElement(ink_1.Box, { key: project.id, flexDirection: "column" },
401
+ react_1.default.createElement(ink_1.Text, { color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
402
+ isSelected ? '▶ ' : ' ',
403
+ hasRunningScripts && react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF "),
404
+ truncateText(project.name, 22),
405
+ hasRunningScripts && react_1.default.createElement(ink_1.Text, { color: colors.accentGreen },
406
+ " (",
407
+ projectRunning.length,
408
+ ")")),
409
+ branch && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
410
+ ' ',
411
+ react_1.default.createElement(ink_1.Text, { color: isMainBranch ? colors.accentGreen : colors.accentBlue }, truncateText(branch, 18))))));
215
412
  }),
216
413
  hasMoreBelow && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
217
414
  "\u2193 ",
218
415
  projects.length - endIndex,
219
416
  " more below")))))));
220
417
  };
221
- const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editingName, editingDescription, editingTags, editInput, allTags, onTagRemove, height, scrollOffset, }) => {
418
+ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editingName, editingDescription, editingTags, editInput, allTags, onTagRemove, height, scrollOffset, gitBranch, }) => {
222
419
  const { focus } = (0, ink_1.useFocus)({ id: 'projectDetails' });
223
420
  const [scripts, setScripts] = (0, react_1.useState)(null);
224
421
  const [ports, setPorts] = (0, react_1.useState)([]);
422
+ const [npmPackage, setNpmPackage] = (0, react_1.useState)(null);
225
423
  (0, react_1.useEffect)(() => {
226
424
  if (!project) {
227
425
  setScripts(null);
228
426
  setPorts([]);
427
+ setNpmPackage(null);
229
428
  return;
230
429
  }
231
430
  // Load scripts
@@ -245,6 +444,26 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
245
444
  catch (error) {
246
445
  setPorts([]);
247
446
  }
447
+ // Check if npm package (live registry check)
448
+ setNpmPackage(null);
449
+ try {
450
+ const packageJsonPath = path.join(project.path, 'package.json');
451
+ if (fs.existsSync(packageJsonPath)) {
452
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
453
+ if (pkg.name && !pkg.private) {
454
+ fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`, {
455
+ method: 'HEAD',
456
+ signal: AbortSignal.timeout(2000),
457
+ })
458
+ .then(res => {
459
+ if (res.ok)
460
+ setNpmPackage(pkg.name);
461
+ })
462
+ .catch(() => { });
463
+ }
464
+ }
465
+ }
466
+ catch { }
248
467
  }, [project]);
249
468
  if (!project) {
250
469
  return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: isFocused ? colors.accentCyan : colors.borderColor, padding: 1 },
@@ -299,8 +518,20 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
299
518
  react_1.default.createElement(ink_1.Text, null, " | "),
300
519
  react_1.default.createElement(ink_1.Text, null,
301
520
  "Scripts: ",
302
- react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts?.scripts?.size || 0))));
521
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts?.scripts?.size || 0)),
522
+ npmPackage && (react_1.default.createElement(react_1.default.Fragment, null,
523
+ react_1.default.createElement(ink_1.Text, null, " | "),
524
+ react_1.default.createElement(ink_1.Text, null,
525
+ "NPM: ",
526
+ react_1.default.createElement(ink_1.Text, { color: "#f85149" }, npmPackage))))));
303
527
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer2" }, " "));
528
+ // Git branch
529
+ if (gitBranch) {
530
+ const isMainBranch = gitBranch === 'main' || gitBranch === 'master';
531
+ contentLines.push(react_1.default.createElement(ink_1.Text, { key: "git-branch" },
532
+ "Branch: ",
533
+ react_1.default.createElement(ink_1.Text, { color: isMainBranch ? colors.accentGreen : colors.accentBlue }, gitBranch)));
534
+ }
304
535
  if (project.framework) {
305
536
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "framework" },
306
537
  "Framework: ",
@@ -335,34 +566,28 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
335
566
  });
336
567
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer4" }, " "));
337
568
  }
338
- // Scripts
569
+ // Scripts - show all, let virtual scrolling handle visibility
339
570
  if (scripts && scripts.scripts && scripts.scripts.size > 0) {
340
571
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "scripts-header", bold: true },
341
572
  "Available Scripts (",
342
573
  react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts.scripts.size),
343
574
  "):"));
344
- Array.from(scripts.scripts.entries()).slice(0, 5).forEach(([name, script]) => {
575
+ Array.from(scripts.scripts.entries()).forEach(([name, script]) => {
345
576
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: `script-${name}` },
346
577
  ' ',
347
578
  react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, name),
348
579
  ' - ',
349
580
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, truncateText(script.command, 60))));
350
581
  });
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
582
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer5" }, " "));
358
583
  }
359
- // Ports
584
+ // Ports - show all, let virtual scrolling handle visibility
360
585
  if (ports.length > 0) {
361
586
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "ports-header", bold: true },
362
587
  "Detected Ports (",
363
588
  react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, ports.length),
364
589
  "):"));
365
- ports.slice(0, 5).forEach((port) => {
590
+ ports.forEach((port) => {
366
591
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: `port-${port.id}` },
367
592
  ' ',
368
593
  "Port ",
@@ -371,12 +596,6 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
371
596
  " - ",
372
597
  truncateText(port.config_source, 50))));
373
598
  });
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
599
  contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer6" }, " "));
381
600
  }
382
601
  // Calculate visible range for virtual scrolling
@@ -404,6 +623,80 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
404
623
  contentLines.length - endIndex,
405
624
  " more below")))));
406
625
  };
626
+ const TerminalOutputPanel = ({ processes, selectedPid, height, onSelectProcess, }) => {
627
+ const [logs, setLogs] = (0, react_1.useState)([]);
628
+ const [scrollOffset, setScrollOffset] = (0, react_1.useState)(0);
629
+ // Find the selected process or default to first
630
+ const activeProcess = selectedPid
631
+ ? processes.find((p) => p.pid === selectedPid)
632
+ : processes[0];
633
+ (0, react_1.useEffect)(() => {
634
+ if (!activeProcess?.logFile) {
635
+ setLogs(['No active process selected']);
636
+ return;
637
+ }
638
+ // Read initial logs
639
+ try {
640
+ if (fs.existsSync(activeProcess.logFile)) {
641
+ const content = fs.readFileSync(activeProcess.logFile, 'utf-8');
642
+ const lines = content.split('\n').slice(-50); // Last 50 lines
643
+ setLogs(lines);
644
+ setScrollOffset(Math.max(0, lines.length - 10));
645
+ }
646
+ else {
647
+ setLogs(['Log file not found']);
648
+ }
649
+ }
650
+ catch {
651
+ setLogs(['Error reading logs']);
652
+ }
653
+ // Watch for changes
654
+ let watcher = null;
655
+ try {
656
+ watcher = fs.watch(activeProcess.logFile, () => {
657
+ try {
658
+ const content = fs.readFileSync(activeProcess.logFile, 'utf-8');
659
+ const lines = content.split('\n').slice(-100);
660
+ setLogs(lines);
661
+ // Auto-scroll to bottom
662
+ setScrollOffset(Math.max(0, lines.length - 10));
663
+ }
664
+ catch {
665
+ // Ignore read errors during watch
666
+ }
667
+ });
668
+ }
669
+ catch {
670
+ // Ignore watch errors
671
+ }
672
+ return () => {
673
+ if (watcher) {
674
+ watcher.close();
675
+ }
676
+ };
677
+ }, [activeProcess?.logFile, activeProcess?.pid]);
678
+ const visibleLines = logs.slice(scrollOffset, scrollOffset + height - 4);
679
+ const hasMoreAbove = scrollOffset > 0;
680
+ const hasMoreBelow = scrollOffset + height - 4 < logs.length;
681
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "30%", height: height, borderStyle: "round", borderColor: colors.accentGreen, padding: 1, flexShrink: 0 },
682
+ react_1.default.createElement(ink_1.Box, { flexDirection: "row", justifyContent: "space-between" },
683
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentGreen }, "Terminal Output"),
684
+ processes.length > 1 && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
685
+ "[",
686
+ processes.findIndex((p) => p.pid === activeProcess?.pid) + 1,
687
+ "/",
688
+ processes.length,
689
+ "]"))),
690
+ activeProcess && (react_1.default.createElement(ink_1.Text, { color: colors.textSecondary },
691
+ truncateText(activeProcess.scriptName, 20),
692
+ " (PID: ",
693
+ activeProcess.pid,
694
+ ")")),
695
+ react_1.default.createElement(ink_1.Box, { flexDirection: "column", flexGrow: 1, marginTop: 1 },
696
+ hasMoreAbove && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "\u2191 more above")),
697
+ visibleLines.map((line, idx) => (react_1.default.createElement(ink_1.Text, { key: idx, color: colors.textPrimary }, truncateText(line, 40)))),
698
+ hasMoreBelow && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "\u2193 more below")))));
699
+ };
407
700
  const StatusBar = ({ focusedPanel, selectedProject }) => {
408
701
  if (focusedPanel === 'list') {
409
702
  return (react_1.default.createElement(ink_1.Box, { flexDirection: "column" },
@@ -411,16 +704,22 @@ const StatusBar = ({ focusedPanel, selectedProject }) => {
411
704
  react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF API"),
412
705
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " | "),
413
706
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Focus: "),
414
- react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Projects")),
707
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "List")),
415
708
  react_1.default.createElement(ink_1.Box, null,
709
+ react_1.default.createElement(ink_1.Text, { bold: true }, "a"),
710
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Add | "),
416
711
  react_1.default.createElement(ink_1.Text, { bold: true }, "/"),
417
712
  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"),
713
+ react_1.default.createElement(ink_1.Text, { bold: true }, "F"),
714
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Filter | "),
715
+ react_1.default.createElement(ink_1.Text, { bold: true }, "S"),
716
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Sort | "),
717
+ react_1.default.createElement(ink_1.Text, { bold: true }, "\u2191\u2193"),
718
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Nav | "),
719
+ react_1.default.createElement(ink_1.Text, { bold: true }, "Tab"),
421
720
  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 | "),
721
+ react_1.default.createElement(ink_1.Text, { bold: true }, "T"),
722
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Terminal | "),
424
723
  react_1.default.createElement(ink_1.Text, { bold: true }, "?"),
425
724
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Help | "),
426
725
  react_1.default.createElement(ink_1.Text, { bold: true }, "q"),
@@ -435,26 +734,18 @@ const StatusBar = ({ focusedPanel, selectedProject }) => {
435
734
  react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Details"),
436
735
  selectedProject && (react_1.default.createElement(react_1.default.Fragment, null,
437
736
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " | "),
438
- react_1.default.createElement(ink_1.Text, { color: colors.textPrimary }, selectedProject.name)))),
737
+ react_1.default.createElement(ink_1.Text, { color: colors.textPrimary }, truncateText(selectedProject.name, 20))))),
439
738
  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
739
  react_1.default.createElement(ink_1.Text, { bold: true }, "e"),
443
740
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Edit | "),
444
741
  react_1.default.createElement(ink_1.Text, { bold: true }, "t"),
445
742
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Tags | "),
446
743
  react_1.default.createElement(ink_1.Text, { bold: true }, "o"),
447
744
  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 | "),
745
+ react_1.default.createElement(ink_1.Text, { bold: true }, "r"),
746
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Run | "),
452
747
  react_1.default.createElement(ink_1.Text, { bold: true }, "s"),
453
748
  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
749
  react_1.default.createElement(ink_1.Text, { bold: true }, "x"),
459
750
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Stop | "),
460
751
  react_1.default.createElement(ink_1.Text, { bold: true }, "d"),
@@ -462,9 +753,7 @@ const StatusBar = ({ focusedPanel, selectedProject }) => {
462
753
  react_1.default.createElement(ink_1.Text, { bold: true }, "Tab"),
463
754
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Switch | "),
464
755
  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"))));
756
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Help"))));
468
757
  };
469
758
  // Simple fuzzy search function
470
759
  function fuzzyMatch(query, text) {
@@ -492,6 +781,13 @@ const App = () => {
492
781
  const [error, setError] = (0, react_1.useState)(null);
493
782
  const [runningProcesses, setRunningProcesses] = (0, react_1.useState)([]);
494
783
  const [focusedPanel, setFocusedPanel] = (0, react_1.useState)('list');
784
+ // View state
785
+ const [currentView, setCurrentView] = (0, react_1.useState)('projects');
786
+ // Git branches
787
+ const [gitBranches, setGitBranches] = (0, react_1.useState)(new Map());
788
+ // Filter and sort state
789
+ const [filterType, setFilterType] = (0, react_1.useState)('all');
790
+ const [sortType, setSortType] = (0, react_1.useState)('name-asc');
495
791
  // Editing state
496
792
  const [editingName, setEditingName] = (0, react_1.useState)(false);
497
793
  const [editingDescription, setEditingDescription] = (0, react_1.useState)(false);
@@ -499,6 +795,9 @@ const App = () => {
499
795
  const [editInput, setEditInput] = (0, react_1.useState)('');
500
796
  const [showUrls, setShowUrls] = (0, react_1.useState)(false);
501
797
  const [allTags, setAllTags] = (0, react_1.useState)([]);
798
+ // Modal state
799
+ const [showAddProjectModal, setShowAddProjectModal] = (0, react_1.useState)(false);
800
+ const [showConfirmDelete, setShowConfirmDelete] = (0, react_1.useState)(false);
502
801
  // Search state
503
802
  const [showSearch, setShowSearch] = (0, react_1.useState)(false);
504
803
  const [searchQuery, setSearchQuery] = (0, react_1.useState)('');
@@ -507,19 +806,69 @@ const App = () => {
507
806
  // Script selection state
508
807
  const [showScriptModal, setShowScriptModal] = (0, react_1.useState)(false);
509
808
  const [scriptModalData, setScriptModalData] = (0, react_1.useState)(null);
809
+ // Workspace state
810
+ const [workspaces, setWorkspaces] = (0, react_1.useState)([]);
811
+ const [selectedWorkspaceIndex, setSelectedWorkspaceIndex] = (0, react_1.useState)(0);
812
+ // Terminal panel state
813
+ const [showTerminalPanel, setShowTerminalPanel] = (0, react_1.useState)(false);
814
+ const [terminalLogs, setTerminalLogs] = (0, react_1.useState)([]);
815
+ const [selectedProcessPid, setSelectedProcessPid] = (0, react_1.useState)(null);
816
+ // Settings state
817
+ const [settings, setSettings] = (0, react_1.useState)({
818
+ editor: { type: 'vscode' },
819
+ browser: { type: 'chrome' },
820
+ });
821
+ const [settingsSection, setSettingsSection] = (0, react_1.useState)('editor');
822
+ const [settingsOptionIndex, setSettingsOptionIndex] = (0, react_1.useState)(0);
823
+ const settingsEditorOptions = [
824
+ 'vscode', 'cursor', 'windsurf', 'zed', 'custom',
825
+ ];
826
+ const settingsBrowserOptions = [
827
+ 'chrome', 'firefox', 'safari', 'edge', 'custom',
828
+ ];
510
829
  // Get terminal dimensions
511
830
  const terminalHeight = process.stdout.rows || 24;
512
- const availableHeight = terminalHeight - 3; // Subtract status bar
831
+ const availableHeight = terminalHeight - 4; // Subtract status bar (increased for view indicator)
513
832
  (0, react_1.useEffect)(() => {
514
833
  loadProjects();
515
834
  loadRunningProcesses();
516
835
  loadAllTags();
517
- // Refresh running processes every 5 seconds
836
+ // Load settings
837
+ try {
838
+ const settingsPath = path.join(os.homedir(), '.projax', 'settings.json');
839
+ if (fs.existsSync(settingsPath)) {
840
+ const data = fs.readFileSync(settingsPath, 'utf-8');
841
+ setSettings(JSON.parse(data));
842
+ }
843
+ }
844
+ catch {
845
+ // Use defaults
846
+ }
847
+ // Refresh running processes and git branches every 5 seconds
518
848
  const interval = setInterval(() => {
519
849
  loadRunningProcesses();
520
850
  }, 5000);
521
851
  return () => clearInterval(interval);
522
852
  }, []);
853
+ // Load git branches when projects change
854
+ (0, react_1.useEffect)(() => {
855
+ if (allProjects.length > 0) {
856
+ loadGitBranches();
857
+ }
858
+ }, [allProjects]);
859
+ const loadGitBranches = async () => {
860
+ const branches = new Map();
861
+ for (const project of allProjects) {
862
+ try {
863
+ const branch = (0, core_bridge_1.getCurrentBranch)(project.path);
864
+ branches.set(project.id, branch);
865
+ }
866
+ catch {
867
+ branches.set(project.id, null);
868
+ }
869
+ }
870
+ setGitBranches(branches);
871
+ };
523
872
  // Reset editing state and scroll when project changes
524
873
  (0, react_1.useEffect)(() => {
525
874
  setEditingName(false);
@@ -528,6 +877,12 @@ const App = () => {
528
877
  setEditInput('');
529
878
  setDetailsScrollOffset(0); // Reset scroll when switching projects
530
879
  }, [selectedIndex]);
880
+ // Load workspaces when switching to workspaces view
881
+ (0, react_1.useEffect)(() => {
882
+ if (currentView === 'workspaces' && workspaces.length === 0) {
883
+ loadWorkspacesFromApi();
884
+ }
885
+ }, [currentView]);
531
886
  // Update scroll offset when selected index changes
532
887
  (0, react_1.useEffect)(() => {
533
888
  const visibleHeight = Math.max(1, availableHeight - 3);
@@ -560,26 +915,71 @@ const App = () => {
560
915
  const loadProjects = () => {
561
916
  const loadedProjects = (0, core_bridge_1.getAllProjects)();
562
917
  setAllProjects(loadedProjects);
563
- filterProjects(loadedProjects, searchQuery);
918
+ applyFilterAndSort(loadedProjects, searchQuery, filterType, sortType);
564
919
  };
565
- const filterProjects = (projectsToFilter, query) => {
566
- if (!query.trim()) {
567
- setProjects(projectsToFilter);
568
- return;
920
+ const applyFilterAndSort = (projectsToFilter, query, filter, sort) => {
921
+ let filtered = projectsToFilter;
922
+ // Apply search query with filter type
923
+ if (query.trim()) {
924
+ const q = query.toLowerCase();
925
+ filtered = projectsToFilter.filter(project => {
926
+ switch (filter) {
927
+ case 'name':
928
+ return fuzzyMatch(q, project.name);
929
+ case 'path':
930
+ return fuzzyMatch(q, project.path);
931
+ case 'tags':
932
+ return project.tags?.some((tag) => fuzzyMatch(q, tag)) || false;
933
+ case 'ports': {
934
+ // Check if project has ports matching the query
935
+ const db = (0, core_bridge_1.getDatabaseManager)();
936
+ const ports = db.getProjectPorts(project.id);
937
+ return ports.some((p) => p.port.toString().includes(q));
938
+ }
939
+ case 'running': {
940
+ const isRunning = runningProcesses.some((p) => p.projectPath === project.path);
941
+ return (q === 'running' || q === 'yes' || q === 'true') ? isRunning : !isRunning;
942
+ }
943
+ case 'all':
944
+ default:
945
+ return (fuzzyMatch(q, project.name) ||
946
+ (project.description ? fuzzyMatch(q, project.description) : false) ||
947
+ fuzzyMatch(q, project.path) ||
948
+ project.tags?.some((tag) => fuzzyMatch(q, tag)) ||
949
+ false);
950
+ }
951
+ });
569
952
  }
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;
953
+ // Apply sorting
954
+ const sorted = [...filtered].sort((a, b) => {
955
+ switch (sort) {
956
+ case 'name-asc':
957
+ return a.name.localeCompare(b.name);
958
+ case 'name-desc':
959
+ return b.name.localeCompare(a.name);
960
+ case 'recent':
961
+ return (b.last_scanned || 0) - (a.last_scanned || 0);
962
+ case 'oldest':
963
+ return (a.created_at || 0) - (b.created_at || 0);
964
+ case 'running': {
965
+ const aRunning = runningProcesses.filter((p) => p.projectPath === a.path).length;
966
+ const bRunning = runningProcesses.filter((p) => p.projectPath === b.path).length;
967
+ return bRunning - aRunning;
968
+ }
969
+ default:
970
+ return 0;
971
+ }
576
972
  });
577
- setProjects(filtered);
973
+ setProjects(sorted);
578
974
  // Adjust selected index if current selection is out of bounds
579
- if (selectedIndex >= filtered.length) {
580
- setSelectedIndex(Math.max(0, filtered.length - 1));
975
+ if (selectedIndex >= sorted.length) {
976
+ setSelectedIndex(Math.max(0, sorted.length - 1));
581
977
  }
582
978
  };
979
+ // Re-apply filter/sort when dependencies change
980
+ (0, react_1.useEffect)(() => {
981
+ applyFilterAndSort(allProjects, searchQuery, filterType, sortType);
982
+ }, [filterType, sortType, runningProcesses]);
583
983
  const loadRunningProcesses = async () => {
584
984
  try {
585
985
  const processes = await (0, script_runner_1.getRunningProcessesClean)();
@@ -726,13 +1126,144 @@ const App = () => {
726
1126
  setError(err instanceof Error ? err.message : String(err));
727
1127
  }
728
1128
  };
1129
+ // Handler for adding a project
1130
+ const handleAddProject = async (projectPath, projectName) => {
1131
+ setShowAddProjectModal(false);
1132
+ setIsLoading(true);
1133
+ setLoadingMessage('Adding project...');
1134
+ try {
1135
+ const name = projectName || path.basename(projectPath);
1136
+ const db = (0, core_bridge_1.getDatabaseManager)();
1137
+ const project = db.addProject(name, projectPath);
1138
+ // Scan for tests
1139
+ setLoadingMessage('Scanning for tests...');
1140
+ await (0, core_bridge_1.scanProject)(project.id);
1141
+ // Scan for ports
1142
+ setLoadingMessage('Scanning for ports...');
1143
+ try {
1144
+ const { scanProjectPorts } = await Promise.resolve().then(() => __importStar(require('./port-scanner')));
1145
+ await scanProjectPorts(project.id);
1146
+ }
1147
+ catch {
1148
+ // Ignore port scanning errors
1149
+ }
1150
+ loadProjects();
1151
+ setIsLoading(false);
1152
+ // Select the newly added project
1153
+ const newProjects = (0, core_bridge_1.getAllProjects)();
1154
+ const newIndex = newProjects.findIndex((p) => p.id === project.id);
1155
+ if (newIndex >= 0) {
1156
+ setSelectedIndex(newIndex);
1157
+ }
1158
+ }
1159
+ catch (err) {
1160
+ setIsLoading(false);
1161
+ setError(err instanceof Error ? err.message : String(err));
1162
+ }
1163
+ };
1164
+ // Handler for deleting a project
1165
+ const handleDeleteProject = () => {
1166
+ if (!selectedProject)
1167
+ return;
1168
+ setShowConfirmDelete(false);
1169
+ setIsLoading(true);
1170
+ setLoadingMessage(`Deleting ${selectedProject.name}...`);
1171
+ setTimeout(async () => {
1172
+ try {
1173
+ const db = (0, core_bridge_1.getDatabaseManager)();
1174
+ db.removeProject(selectedProject.id);
1175
+ loadProjects();
1176
+ if (selectedIndex >= projects.length - 1) {
1177
+ setSelectedIndex(Math.max(0, projects.length - 2));
1178
+ }
1179
+ setIsLoading(false);
1180
+ }
1181
+ catch (err) {
1182
+ setIsLoading(false);
1183
+ setError(err instanceof Error ? err.message : String(err));
1184
+ }
1185
+ }, 100);
1186
+ };
1187
+ // Cycle filter type
1188
+ const cycleFilterType = () => {
1189
+ const currentIndex = FILTER_TYPES.indexOf(filterType);
1190
+ const nextIndex = (currentIndex + 1) % FILTER_TYPES.length;
1191
+ setFilterType(FILTER_TYPES[nextIndex]);
1192
+ };
1193
+ // Cycle sort type
1194
+ const cycleSortType = () => {
1195
+ const currentIndex = SORT_TYPES.indexOf(sortType);
1196
+ const nextIndex = (currentIndex + 1) % SORT_TYPES.length;
1197
+ setSortType(SORT_TYPES[nextIndex]);
1198
+ };
729
1199
  (0, ink_1.useInput)((input, key) => {
1200
+ if (key.mouse) {
1201
+ const { x, y, wheelDown, wheelUp, left } = key.mouse;
1202
+ const { columns: width } = process.stdout;
1203
+ // Assuming project list is on the left 35% and details on the right.
1204
+ const projectListWidth = Math.floor(width * 0.35);
1205
+ if (x < projectListWidth) {
1206
+ // Mouse is over the project list
1207
+ if (wheelUp) {
1208
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
1209
+ return;
1210
+ }
1211
+ if (wheelDown) {
1212
+ setSelectedIndex((prev) => Math.min(projects.length - 1, prev + 1));
1213
+ return;
1214
+ }
1215
+ if (left) {
1216
+ // It's a click, so we need to calculate which item was clicked.
1217
+ // This is an approximation based on the known layout of the ProjectListComponent.
1218
+ const listTopBorder = 1;
1219
+ const listPadding = 1;
1220
+ const listHeaderHeight = 2; // "Projects (...)" + "Filter | Sort"
1221
+ const listStartY = listTopBorder + listPadding + listHeaderHeight;
1222
+ const scrollIndicatorHeight = listScrollOffset > 0 ? 1 : 0;
1223
+ const firstItemY = listStartY + scrollIndicatorHeight;
1224
+ const clickYInList = y - firstItemY;
1225
+ if (clickYInList >= 0) {
1226
+ let cumulativeHeight = 0;
1227
+ const visibleProjects = projects.slice(listScrollOffset);
1228
+ for (let i = 0; i < visibleProjects.length; i++) {
1229
+ const project = visibleProjects[i];
1230
+ const branch = gitBranches.get(project.id);
1231
+ const itemHeight = branch ? 2 : 1;
1232
+ if (clickYInList >= cumulativeHeight && clickYInList < cumulativeHeight + itemHeight) {
1233
+ const newIndex = listScrollOffset + i;
1234
+ if (newIndex < projects.length) {
1235
+ setSelectedIndex(newIndex);
1236
+ setFocusedPanel('list');
1237
+ }
1238
+ break;
1239
+ }
1240
+ cumulativeHeight += itemHeight;
1241
+ if (cumulativeHeight > availableHeight) {
1242
+ break;
1243
+ }
1244
+ }
1245
+ }
1246
+ return;
1247
+ }
1248
+ }
1249
+ else {
1250
+ // Mouse is over the details panel
1251
+ if (wheelUp) {
1252
+ setDetailsScrollOffset(prev => Math.max(0, prev - 1));
1253
+ return;
1254
+ }
1255
+ if (wheelDown) {
1256
+ setDetailsScrollOffset(prev => prev + 1);
1257
+ return;
1258
+ }
1259
+ }
1260
+ }
730
1261
  // Handle search mode
731
1262
  if (showSearch) {
732
1263
  if (key.escape) {
733
1264
  setShowSearch(false);
734
1265
  setSearchQuery('');
735
- filterProjects(allProjects, '');
1266
+ applyFilterAndSort(allProjects, '', filterType, sortType);
736
1267
  return;
737
1268
  }
738
1269
  if (key.return) {
@@ -742,19 +1273,19 @@ const App = () => {
742
1273
  if (key.backspace || key.delete) {
743
1274
  const newQuery = searchQuery.slice(0, -1);
744
1275
  setSearchQuery(newQuery);
745
- filterProjects(allProjects, newQuery);
1276
+ applyFilterAndSort(allProjects, newQuery, filterType, sortType);
746
1277
  return;
747
1278
  }
748
1279
  if (input && input.length === 1 && !key.ctrl && !key.meta) {
749
1280
  const newQuery = searchQuery + input;
750
1281
  setSearchQuery(newQuery);
751
- filterProjects(allProjects, newQuery);
1282
+ applyFilterAndSort(allProjects, newQuery, filterType, sortType);
752
1283
  return;
753
1284
  }
754
1285
  return;
755
1286
  }
756
1287
  // Don't process input if modal is showing
757
- if (showHelp || isLoading || error || showUrls || showScriptModal) {
1288
+ if (showHelp || isLoading || error || showUrls || showScriptModal || showAddProjectModal || showConfirmDelete) {
758
1289
  // Handle URLs modal
759
1290
  if (showUrls && (key.escape || key.return || input === 'q' || input === 'u')) {
760
1291
  setShowUrls(false);
@@ -762,12 +1293,135 @@ const App = () => {
762
1293
  }
763
1294
  return;
764
1295
  }
1296
+ // Handle navigation in workspaces view
1297
+ if (currentView === 'workspaces') {
1298
+ if (key.upArrow || input === 'k') {
1299
+ setSelectedWorkspaceIndex((prev) => Math.max(0, prev - 1));
1300
+ return;
1301
+ }
1302
+ if (key.downArrow || input === 'j') {
1303
+ setSelectedWorkspaceIndex((prev) => Math.min(workspaces.length - 1, prev + 1));
1304
+ return;
1305
+ }
1306
+ }
1307
+ // Handle navigation in processes view
1308
+ if (currentView === 'processes') {
1309
+ if (input === 'x' && runningProcesses.length > 0) {
1310
+ // Stop all processes (or could select one)
1311
+ setIsLoading(true);
1312
+ setLoadingMessage('Stopping processes...');
1313
+ setTimeout(async () => {
1314
+ try {
1315
+ for (const proc of runningProcesses) {
1316
+ await (0, script_runner_1.stopScript)(proc.pid);
1317
+ }
1318
+ await loadRunningProcesses();
1319
+ setIsLoading(false);
1320
+ }
1321
+ catch (err) {
1322
+ setIsLoading(false);
1323
+ setError(err instanceof Error ? err.message : String(err));
1324
+ }
1325
+ }, 100);
1326
+ return;
1327
+ }
1328
+ }
1329
+ // Handle navigation in settings view
1330
+ if (currentView === 'settings') {
1331
+ if (key.tab) {
1332
+ setSettingsSection((prev) => (prev === 'editor' ? 'browser' : 'editor'));
1333
+ setSettingsOptionIndex(0);
1334
+ return;
1335
+ }
1336
+ if (key.upArrow || input === 'k') {
1337
+ setSettingsOptionIndex((prev) => Math.max(0, prev - 1));
1338
+ return;
1339
+ }
1340
+ if (key.downArrow || input === 'j') {
1341
+ const maxIndex = settingsSection === 'editor' ? settingsEditorOptions.length - 1 : settingsBrowserOptions.length - 1;
1342
+ setSettingsOptionIndex((prev) => Math.min(maxIndex, prev + 1));
1343
+ return;
1344
+ }
1345
+ if (input === ' ' || key.return) {
1346
+ // Select option and save
1347
+ const newSettings = { ...settings };
1348
+ if (settingsSection === 'editor') {
1349
+ newSettings.editor = { type: settingsEditorOptions[settingsOptionIndex] };
1350
+ }
1351
+ else {
1352
+ newSettings.browser = { type: settingsBrowserOptions[settingsOptionIndex] };
1353
+ }
1354
+ setSettings(newSettings);
1355
+ // Save to file
1356
+ try {
1357
+ const settingsDir = path.join(os.homedir(), '.projax');
1358
+ if (!fs.existsSync(settingsDir)) {
1359
+ fs.mkdirSync(settingsDir, { recursive: true });
1360
+ }
1361
+ const settingsPath = path.join(settingsDir, 'settings.json');
1362
+ fs.writeFileSync(settingsPath, JSON.stringify(newSettings, null, 2));
1363
+ }
1364
+ catch {
1365
+ // Ignore save errors
1366
+ }
1367
+ return;
1368
+ }
1369
+ }
1370
+ // Global navigation - number keys for view switching
1371
+ if (input === '1') {
1372
+ setCurrentView('projects');
1373
+ return;
1374
+ }
1375
+ if (input === '2') {
1376
+ setCurrentView('workspaces');
1377
+ return;
1378
+ }
1379
+ if (input === '3') {
1380
+ setCurrentView('processes');
1381
+ return;
1382
+ }
1383
+ if (input === '4') {
1384
+ setCurrentView('settings');
1385
+ return;
1386
+ }
1387
+ // Terminal panel toggle
1388
+ if (input === 'T') {
1389
+ setShowTerminalPanel(prev => !prev);
1390
+ return;
1391
+ }
765
1392
  // Search shortcut
766
1393
  if (input === '/') {
767
1394
  setShowSearch(true);
768
1395
  setSearchQuery('');
769
1396
  return;
770
1397
  }
1398
+ // Add project shortcut (in projects view)
1399
+ if (input === 'a' && currentView === 'projects') {
1400
+ setShowAddProjectModal(true);
1401
+ return;
1402
+ }
1403
+ // Filter cycle shortcut
1404
+ if (input === 'F' && currentView === 'projects') {
1405
+ cycleFilterType();
1406
+ return;
1407
+ }
1408
+ // Sort cycle shortcut
1409
+ if (input === 'S' && currentView === 'projects') {
1410
+ cycleSortType();
1411
+ return;
1412
+ }
1413
+ // Refresh git branches
1414
+ if (input === 'g' && currentView === 'projects') {
1415
+ loadGitBranches();
1416
+ return;
1417
+ }
1418
+ // Full refresh
1419
+ if (input === 'R') {
1420
+ loadProjects();
1421
+ loadRunningProcesses();
1422
+ loadGitBranches();
1423
+ return;
1424
+ }
771
1425
  // Handle editing modes
772
1426
  if (editingName || editingDescription || editingTags) {
773
1427
  if (key.escape) {
@@ -924,25 +1578,9 @@ const App = () => {
924
1578
  setShowUrls(true);
925
1579
  return;
926
1580
  }
927
- // Delete project
1581
+ // Delete project (with confirmation)
928
1582
  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);
1583
+ setShowConfirmDelete(true);
946
1584
  return;
947
1585
  }
948
1586
  }
@@ -1076,6 +1714,14 @@ const App = () => {
1076
1714
  react_1.default.createElement(ink_1.Text, null, " "),
1077
1715
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Press Esc or u to close..."))));
1078
1716
  }
1717
+ if (showAddProjectModal) {
1718
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
1719
+ react_1.default.createElement(AddProjectModal, { onAdd: handleAddProject, onCancel: () => setShowAddProjectModal(false) })));
1720
+ }
1721
+ if (showConfirmDelete && selectedProject) {
1722
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
1723
+ 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) })));
1724
+ }
1079
1725
  if (showScriptModal && scriptModalData) {
1080
1726
  return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
1081
1727
  react_1.default.createElement(ScriptSelectionModal, { scripts: scriptModalData.scripts, projectName: scriptModalData.projectName, projectPath: scriptModalData.projectPath, onSelect: handleScriptSelect, onClose: () => setShowScriptModal(false) })));
@@ -1094,11 +1740,133 @@ const App = () => {
1094
1740
  }
1095
1741
  }
1096
1742
  };
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 }),
1743
+ // Render Projects view
1744
+ const renderProjectsView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "row", height: availableHeight, flexGrow: 0, flexShrink: 0 },
1745
+ react_1.default.createElement(ProjectListComponent, { projects: projects, selectedIndex: selectedIndex, runningProcesses: runningProcesses, isFocused: focusedPanel === 'list', height: availableHeight, scrollOffset: listScrollOffset, gitBranches: gitBranches, filterType: filterType, sortType: sortType }),
1746
+ react_1.default.createElement(ink_1.Box, { width: 1 }),
1747
+ 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 }),
1748
+ showTerminalPanel && (react_1.default.createElement(react_1.default.Fragment, null,
1100
1749
  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 })),
1750
+ react_1.default.createElement(TerminalOutputPanel, { processes: runningProcesses, selectedPid: selectedProcessPid, height: availableHeight, onSelectProcess: (pid) => setSelectedProcessPid(pid) })))));
1751
+ // Render Workspaces view
1752
+ const renderWorkspacesView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "row", height: availableHeight, flexGrow: 0, flexShrink: 0 },
1753
+ react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "35%", height: availableHeight, borderStyle: "round", borderColor: colors.accentCyan, padding: 1 },
1754
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.textPrimary },
1755
+ "Workspaces (",
1756
+ workspaces.length,
1757
+ ")"),
1758
+ 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) => {
1759
+ const isSelected = index === selectedWorkspaceIndex;
1760
+ return (react_1.default.createElement(ink_1.Text, { key: ws.id, color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
1761
+ isSelected ? '▶ ' : ' ',
1762
+ truncateText(ws.name, 25)));
1763
+ })))),
1764
+ react_1.default.createElement(ink_1.Box, { width: 1 }),
1765
+ 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,
1766
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, workspaces[selectedWorkspaceIndex].name),
1767
+ react_1.default.createElement(ink_1.Text, null, " "),
1768
+ workspaces[selectedWorkspaceIndex].description && (react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, workspaces[selectedWorkspaceIndex].description)),
1769
+ react_1.default.createElement(ink_1.Text, null, " "),
1770
+ react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
1771
+ "Path: ",
1772
+ getDisplayPath(workspaces[selectedWorkspaceIndex].workspace_file_path)))) : (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "Select a workspace")))));
1773
+ // Load workspaces from API
1774
+ const loadWorkspacesFromApi = async () => {
1775
+ try {
1776
+ // Try common API ports
1777
+ const ports = [38124, 38125, 38126, 38127, 38128, 3001];
1778
+ let apiBaseUrl = '';
1779
+ for (const port of ports) {
1780
+ try {
1781
+ const response = await fetch(`http://localhost:${port}/health`, {
1782
+ signal: AbortSignal.timeout(500),
1783
+ });
1784
+ if (response.ok) {
1785
+ apiBaseUrl = `http://localhost:${port}/api`;
1786
+ break;
1787
+ }
1788
+ }
1789
+ catch {
1790
+ continue;
1791
+ }
1792
+ }
1793
+ if (!apiBaseUrl) {
1794
+ return;
1795
+ }
1796
+ const response = await fetch(`${apiBaseUrl}/workspaces`);
1797
+ if (response.ok) {
1798
+ const ws = (await response.json());
1799
+ setWorkspaces(ws);
1800
+ }
1801
+ }
1802
+ catch {
1803
+ // Ignore workspace loading errors
1804
+ }
1805
+ };
1806
+ // Render Processes view placeholder
1807
+ const renderProcessesView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 2 },
1808
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan },
1809
+ "Running Processes (",
1810
+ runningProcesses.length,
1811
+ ")"),
1812
+ react_1.default.createElement(ink_1.Text, null, " "),
1813
+ runningProcesses.length === 0 ? (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "No running processes")) : (runningProcesses.map((proc) => {
1814
+ const uptime = Math.floor((Date.now() - proc.startedAt) / 1000);
1815
+ const minutes = Math.floor(uptime / 60);
1816
+ const seconds = uptime % 60;
1817
+ const uptimeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
1818
+ return (react_1.default.createElement(ink_1.Text, { key: proc.pid, color: colors.textPrimary },
1819
+ react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF"),
1820
+ " PID ",
1821
+ proc.pid,
1822
+ ": ",
1823
+ proc.projectName,
1824
+ " (",
1825
+ proc.scriptName,
1826
+ ") - ",
1827
+ uptimeStr));
1828
+ })),
1829
+ react_1.default.createElement(ink_1.Text, null, " "),
1830
+ react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "Press 1 to return to Projects")));
1831
+ // Render Settings view
1832
+ const renderSettingsView = () => {
1833
+ const currentOptions = settingsSection === 'editor' ? settingsEditorOptions : settingsBrowserOptions;
1834
+ const currentValue = settingsSection === 'editor' ? settings.editor.type : settings.browser.type;
1835
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 2 },
1836
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, "Settings"),
1837
+ react_1.default.createElement(ink_1.Text, null, " "),
1838
+ react_1.default.createElement(ink_1.Box, null,
1839
+ react_1.default.createElement(ink_1.Text, { color: settingsSection === 'editor' ? colors.accentCyan : colors.textTertiary, bold: settingsSection === 'editor' }, "[Editor]"),
1840
+ react_1.default.createElement(ink_1.Text, null, " "),
1841
+ react_1.default.createElement(ink_1.Text, { color: settingsSection === 'browser' ? colors.accentCyan : colors.textTertiary, bold: settingsSection === 'browser' }, "[Browser]")),
1842
+ react_1.default.createElement(ink_1.Text, null, " "),
1843
+ currentOptions.map((option, index) => {
1844
+ const isSelected = index === settingsOptionIndex;
1845
+ const isActive = option === currentValue;
1846
+ return (react_1.default.createElement(ink_1.Text, { key: option, color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
1847
+ isSelected ? '▶ ' : ' ',
1848
+ isActive ? '● ' : '○ ',
1849
+ option.charAt(0).toUpperCase() + option.slice(1)));
1850
+ }),
1851
+ react_1.default.createElement(ink_1.Text, null, " "),
1852
+ react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "Tab: switch section | \u2191\u2193/jk: select | Space/Enter: choose")));
1853
+ };
1854
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", height: terminalHeight },
1855
+ react_1.default.createElement(ink_1.Box, { paddingX: 1, height: 1 },
1856
+ react_1.default.createElement(ink_1.Text, { color: currentView === 'projects' ? colors.accentCyan : colors.textTertiary }, "[1] Projects"),
1857
+ react_1.default.createElement(ink_1.Text, null, " "),
1858
+ react_1.default.createElement(ink_1.Text, { color: currentView === 'workspaces' ? colors.accentCyan : colors.textTertiary }, "[2] Workspaces"),
1859
+ react_1.default.createElement(ink_1.Text, null, " "),
1860
+ react_1.default.createElement(ink_1.Text, { color: currentView === 'processes' ? colors.accentCyan : colors.textTertiary }, "[3] Processes"),
1861
+ react_1.default.createElement(ink_1.Text, null, " "),
1862
+ react_1.default.createElement(ink_1.Text, { color: currentView === 'settings' ? colors.accentCyan : colors.textTertiary }, "[4] Settings"),
1863
+ showTerminalPanel && (react_1.default.createElement(react_1.default.Fragment, null,
1864
+ react_1.default.createElement(ink_1.Text, null, " | "),
1865
+ react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "Terminal [T]")))),
1866
+ currentView === 'projects' && renderProjectsView(),
1867
+ currentView === 'workspaces' && renderWorkspacesView(),
1868
+ currentView === 'processes' && renderProcessesView(),
1869
+ currentView === 'settings' && renderSettingsView(),
1102
1870
  react_1.default.createElement(ink_1.Box, { paddingX: 1, borderStyle: "single", borderColor: colors.borderColor, flexShrink: 0, height: 3 },
1103
1871
  react_1.default.createElement(StatusBar, { focusedPanel: focusedPanel, selectedProject: selectedProject }))));
1104
1872
  };