projax 3.3.66 → 3.3.68

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 (3) hide show
  1. package/dist/prxi.js +111 -22
  2. package/dist/prxi.tsx +108 -19
  3. package/package.json +19 -20
package/dist/prxi.js CHANGED
@@ -139,6 +139,7 @@ const HelpModal = ({ onClose }) => {
139
139
  react_1.default.createElement(ink_1.Text, null, " u Show detected URLs"),
140
140
  react_1.default.createElement(ink_1.Text, null, " s Scan project for tests"),
141
141
  react_1.default.createElement(ink_1.Text, null, " p Scan ports for project"),
142
+ react_1.default.createElement(ink_1.Text, null, " Enter Run selected script (in details panel)"),
142
143
  react_1.default.createElement(ink_1.Text, null, " r Run scripts (select from list)"),
143
144
  react_1.default.createElement(ink_1.Text, null, " x Stop all scripts for project"),
144
145
  react_1.default.createElement(ink_1.Text, null, " d Delete project (with confirmation)"),
@@ -336,12 +337,12 @@ const ScriptSelectionModal = ({ scripts, projectName, projectPath, onSelect, onC
336
337
  }
337
338
  if (key.return) {
338
339
  const [scriptName] = scriptArray[selectedIndex];
339
- onSelect(scriptName, false);
340
+ onSelect(scriptName, true);
340
341
  return;
341
342
  }
342
- if (input === 'b') {
343
+ if (input === 'f') {
343
344
  const [scriptName] = scriptArray[selectedIndex];
344
- onSelect(scriptName, true);
345
+ onSelect(scriptName, false);
345
346
  return;
346
347
  }
347
348
  });
@@ -359,7 +360,7 @@ const ScriptSelectionModal = ({ scripts, projectName, projectPath, onSelect, onC
359
360
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, truncateText(script.command, 50))));
360
361
  }),
361
362
  react_1.default.createElement(ink_1.Text, null, " "),
362
- react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "\u2191\u2193/kj: Navigate | Enter: Run | b: Background | Esc/q: Cancel")));
363
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "\u2191\u2193/kj: Navigate | Enter: Run (bg) | f: Foreground | Esc/q: Cancel")));
363
364
  };
364
365
  const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFocused, height, scrollOffset, gitBranches, filterType, sortType, }) => {
365
366
  const { focus } = (0, ink_1.useFocus)({ id: 'projectList' });
@@ -415,7 +416,7 @@ const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFoc
415
416
  projects.length - endIndex,
416
417
  " more below")))))));
417
418
  };
418
- const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editingName, editingDescription, editingTags, editInput, allTags, onTagRemove, height, scrollOffset, gitBranch, }) => {
419
+ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editingName, editingDescription, editingTags, editInput, allTags, onTagRemove, height, scrollOffset, gitBranch, selectedScriptIndex, }) => {
419
420
  const { focus } = (0, ink_1.useFocus)({ id: 'projectDetails' });
420
421
  const [scripts, setScripts] = (0, react_1.useState)(null);
421
422
  const [ports, setPorts] = (0, react_1.useState)([]);
@@ -572,10 +573,11 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
572
573
  "Available Scripts (",
573
574
  react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts.scripts.size),
574
575
  "):"));
575
- Array.from(scripts.scripts.entries()).forEach(([name, script]) => {
576
- contentLines.push(react_1.default.createElement(ink_1.Text, { key: `script-${name}` },
577
- ' ',
578
- react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, name),
576
+ Array.from(scripts.scripts.entries()).forEach(([name, script], idx) => {
577
+ const isScriptSelected = isFocused && idx === selectedScriptIndex;
578
+ contentLines.push(react_1.default.createElement(ink_1.Text, { key: `script-${name}`, bold: isScriptSelected },
579
+ isScriptSelected ? '▶ ' : ' ',
580
+ react_1.default.createElement(ink_1.Text, { color: isScriptSelected ? colors.accentCyan : colors.accentGreen }, name),
579
581
  ' - ',
580
582
  react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, truncateText(script.command, 60))));
581
583
  });
@@ -806,6 +808,7 @@ const App = () => {
806
808
  // Script selection state
807
809
  const [showScriptModal, setShowScriptModal] = (0, react_1.useState)(false);
808
810
  const [scriptModalData, setScriptModalData] = (0, react_1.useState)(null);
811
+ const [detailSelectedScript, setDetailSelectedScript] = (0, react_1.useState)(0);
809
812
  // Workspace state
810
813
  const [workspaces, setWorkspaces] = (0, react_1.useState)([]);
811
814
  const [selectedWorkspaceIndex, setSelectedWorkspaceIndex] = (0, react_1.useState)(0);
@@ -813,6 +816,7 @@ const App = () => {
813
816
  const [showTerminalPanel, setShowTerminalPanel] = (0, react_1.useState)(false);
814
817
  const [terminalLogs, setTerminalLogs] = (0, react_1.useState)([]);
815
818
  const [selectedProcessPid, setSelectedProcessPid] = (0, react_1.useState)(null);
819
+ const [selectedProcessIndex, setSelectedProcessIndex] = (0, react_1.useState)(0);
816
820
  // Settings state
817
821
  const [settings, setSettings] = (0, react_1.useState)({
818
822
  editor: { type: 'vscode' },
@@ -876,6 +880,7 @@ const App = () => {
876
880
  setEditingTags(false);
877
881
  setEditInput('');
878
882
  setDetailsScrollOffset(0); // Reset scroll when switching projects
883
+ setDetailSelectedScript(0); // Reset selected script
879
884
  }, [selectedIndex]);
880
885
  // Load workspaces when switching to workspaces view
881
886
  (0, react_1.useEffect)(() => {
@@ -980,6 +985,15 @@ const App = () => {
980
985
  (0, react_1.useEffect)(() => {
981
986
  applyFilterAndSort(allProjects, searchQuery, filterType, sortType);
982
987
  }, [filterType, sortType, runningProcesses]);
988
+ // Reset/clamp selectedProcessIndex when processes change
989
+ (0, react_1.useEffect)(() => {
990
+ if (runningProcesses.length === 0) {
991
+ setSelectedProcessIndex(0);
992
+ }
993
+ else {
994
+ setSelectedProcessIndex((prev) => Math.min(prev, runningProcesses.length - 1));
995
+ }
996
+ }, [runningProcesses.length]);
983
997
  const loadRunningProcesses = async () => {
984
998
  try {
985
999
  const processes = await (0, script_runner_1.getRunningProcessesClean)();
@@ -1306,10 +1320,38 @@ const App = () => {
1306
1320
  }
1307
1321
  // Handle navigation in processes view
1308
1322
  if (currentView === 'processes') {
1323
+ if (key.upArrow || input === 'k') {
1324
+ setSelectedProcessIndex((prev) => Math.max(0, prev - 1));
1325
+ return;
1326
+ }
1327
+ if (key.downArrow || input === 'j') {
1328
+ setSelectedProcessIndex((prev) => Math.min(runningProcesses.length - 1, prev + 1));
1329
+ return;
1330
+ }
1309
1331
  if (input === 'x' && runningProcesses.length > 0) {
1310
- // Stop all processes (or could select one)
1332
+ // Stop the selected process
1333
+ const selectedProc = runningProcesses[selectedProcessIndex];
1334
+ if (!selectedProc)
1335
+ return;
1311
1336
  setIsLoading(true);
1312
- setLoadingMessage('Stopping processes...');
1337
+ setLoadingMessage(`Stopping process ${selectedProc.pid}...`);
1338
+ setTimeout(async () => {
1339
+ try {
1340
+ await (0, script_runner_1.stopScript)(selectedProc.pid);
1341
+ await loadRunningProcesses();
1342
+ setIsLoading(false);
1343
+ }
1344
+ catch (err) {
1345
+ setIsLoading(false);
1346
+ setError(err instanceof Error ? err.message : String(err));
1347
+ }
1348
+ }, 100);
1349
+ return;
1350
+ }
1351
+ if (input === 'X' && runningProcesses.length > 0) {
1352
+ // Stop ALL processes
1353
+ setIsLoading(true);
1354
+ setLoadingMessage('Stopping all processes...');
1313
1355
  setTimeout(async () => {
1314
1356
  try {
1315
1357
  for (const proc of runningProcesses) {
@@ -1536,15 +1578,54 @@ const App = () => {
1536
1578
  }
1537
1579
  // Details panel actions
1538
1580
  if (focusedPanel === 'details' && selectedProject) {
1539
- // Scroll details panel
1581
+ // Navigate scripts in details panel
1540
1582
  if (key.upArrow || input === 'k') {
1541
- setDetailsScrollOffset(prev => Math.max(0, prev - 1));
1583
+ const projectScripts = (0, script_runner_1.getProjectScripts)(selectedProject.path);
1584
+ if (projectScripts.scripts.size > 0) {
1585
+ setDetailSelectedScript(prev => Math.max(0, prev - 1));
1586
+ }
1587
+ else {
1588
+ setDetailsScrollOffset(prev => Math.max(0, prev - 1));
1589
+ }
1542
1590
  return;
1543
1591
  }
1544
1592
  if (key.downArrow || input === 'j') {
1545
- const visibleHeight = Math.max(1, availableHeight - 3);
1546
- // Estimate content height (rough calculation)
1547
- setDetailsScrollOffset(prev => prev + 1);
1593
+ const projectScripts = (0, script_runner_1.getProjectScripts)(selectedProject.path);
1594
+ if (projectScripts.scripts.size > 0) {
1595
+ setDetailSelectedScript(prev => Math.min(projectScripts.scripts.size - 1, prev + 1));
1596
+ }
1597
+ else {
1598
+ setDetailsScrollOffset(prev => prev + 1);
1599
+ }
1600
+ return;
1601
+ }
1602
+ // Run selected script with Enter
1603
+ if (key.return) {
1604
+ try {
1605
+ const projectScripts = (0, script_runner_1.getProjectScripts)(selectedProject.path);
1606
+ if (projectScripts.scripts.size > 0) {
1607
+ const scriptArray = Array.from(projectScripts.scripts.keys());
1608
+ const scriptName = scriptArray[detailSelectedScript];
1609
+ if (scriptName) {
1610
+ setIsLoading(true);
1611
+ setLoadingMessage(`Running ${scriptName} in background...`);
1612
+ setTimeout(async () => {
1613
+ try {
1614
+ await (0, script_runner_1.runScriptInBackground)(selectedProject.path, selectedProject.name, scriptName, [], false);
1615
+ setIsLoading(false);
1616
+ await loadRunningProcesses();
1617
+ }
1618
+ catch (err) {
1619
+ setIsLoading(false);
1620
+ setError(err instanceof Error ? err.message : String(err));
1621
+ }
1622
+ }, 100);
1623
+ }
1624
+ }
1625
+ }
1626
+ catch (err) {
1627
+ setError(err instanceof Error ? err.message : String(err));
1628
+ }
1548
1629
  return;
1549
1630
  }
1550
1631
  // Edit name
@@ -1744,7 +1825,7 @@ const App = () => {
1744
1825
  const renderProjectsView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "row", height: availableHeight, flexGrow: 0, flexShrink: 0 },
1745
1826
  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
1827
  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 }),
1828
+ 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, selectedScriptIndex: detailSelectedScript }),
1748
1829
  showTerminalPanel && (react_1.default.createElement(react_1.default.Fragment, null,
1749
1830
  react_1.default.createElement(ink_1.Box, { width: 1 }),
1750
1831
  react_1.default.createElement(TerminalOutputPanel, { processes: runningProcesses, selectedPid: selectedProcessPid, height: availableHeight, onSelectProcess: (pid) => setSelectedProcessPid(pid) })))));
@@ -1803,19 +1884,24 @@ const App = () => {
1803
1884
  // Ignore workspace loading errors
1804
1885
  }
1805
1886
  };
1806
- // Render Processes view placeholder
1887
+ // Render Processes view
1807
1888
  const renderProcessesView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 2 },
1808
1889
  react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan },
1809
1890
  "Running Processes (",
1810
1891
  runningProcesses.length,
1811
1892
  ")"),
1812
1893
  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) => {
1894
+ runningProcesses.length === 0 ? (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "No running processes")) : (runningProcesses.map((proc, index) => {
1814
1895
  const uptime = Math.floor((Date.now() - proc.startedAt) / 1000);
1815
1896
  const minutes = Math.floor(uptime / 60);
1816
1897
  const seconds = uptime % 60;
1817
1898
  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 },
1899
+ const isSelected = index === selectedProcessIndex;
1900
+ const portsStr = proc.detectedPorts && proc.detectedPorts.length > 0
1901
+ ? ` [ports: ${proc.detectedPorts.join(', ')}]`
1902
+ : '';
1903
+ return (react_1.default.createElement(ink_1.Text, { key: proc.pid, color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
1904
+ isSelected ? '> ' : ' ',
1819
1905
  react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF"),
1820
1906
  " PID ",
1821
1907
  proc.pid,
@@ -1824,10 +1910,13 @@ const App = () => {
1824
1910
  " (",
1825
1911
  proc.scriptName,
1826
1912
  ") - ",
1827
- uptimeStr));
1913
+ uptimeStr,
1914
+ portsStr && react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, portsStr)));
1828
1915
  })),
1829
1916
  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")));
1917
+ react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, runningProcesses.length > 0
1918
+ ? 'up/down: navigate | x: stop selected | X: stop all | 1: back to projects'
1919
+ : 'Press 1 to return to Projects')));
1831
1920
  // Render Settings view
1832
1921
  const renderSettingsView = () => {
1833
1922
  const currentOptions = settingsSection === 'editor' ? settingsEditorOptions : settingsBrowserOptions;
package/dist/prxi.tsx CHANGED
@@ -162,6 +162,7 @@ const HelpModal: React.FC<HelpModalProps> = ({ onClose }) => {
162
162
  <Text> u Show detected URLs</Text>
163
163
  <Text> s Scan project for tests</Text>
164
164
  <Text> p Scan ports for project</Text>
165
+ <Text> Enter Run selected script (in details panel)</Text>
165
166
  <Text> r Run scripts (select from list)</Text>
166
167
  <Text> x Stop all scripts for project</Text>
167
168
  <Text> d Delete project (with confirmation)</Text>
@@ -470,13 +471,13 @@ const ScriptSelectionModal: React.FC<ScriptSelectionModalProps> = ({
470
471
 
471
472
  if (key.return) {
472
473
  const [scriptName] = scriptArray[selectedIndex];
473
- onSelect(scriptName, false);
474
+ onSelect(scriptName, true);
474
475
  return;
475
476
  }
476
477
 
477
- if (input === 'b') {
478
+ if (input === 'f') {
478
479
  const [scriptName] = scriptArray[selectedIndex];
479
- onSelect(scriptName, true);
480
+ onSelect(scriptName, false);
480
481
  return;
481
482
  }
482
483
  });
@@ -505,7 +506,7 @@ const ScriptSelectionModal: React.FC<ScriptSelectionModalProps> = ({
505
506
  );
506
507
  })}
507
508
  <Text> </Text>
508
- <Text color={colors.textSecondary}>↑↓/kj: Navigate | Enter: Run | b: Background | Esc/q: Cancel</Text>
509
+ <Text color={colors.textSecondary}>↑↓/kj: Navigate | Enter: Run (bg) | f: Foreground | Esc/q: Cancel</Text>
509
510
  </Box>
510
511
  );
511
512
  };
@@ -631,6 +632,7 @@ interface ProjectDetailsProps {
631
632
  height: number;
632
633
  scrollOffset: number;
633
634
  gitBranch: string | null;
635
+ selectedScriptIndex: number;
634
636
  }
635
637
 
636
638
  const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
@@ -646,6 +648,7 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
646
648
  height,
647
649
  scrollOffset,
648
650
  gitBranch,
651
+ selectedScriptIndex,
649
652
  }) => {
650
653
  const { focus } = useFocus({ id: 'projectDetails' });
651
654
  const [scripts, setScripts] = useState<any>(null);
@@ -858,11 +861,12 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
858
861
  Available Scripts (<Text color={colors.accentCyan}>{scripts.scripts.size}</Text>):
859
862
  </Text>
860
863
  );
861
- Array.from(scripts.scripts.entries() as IterableIterator<[string, any]>).forEach(([name, script]) => {
864
+ Array.from(scripts.scripts.entries() as IterableIterator<[string, any]>).forEach(([name, script], idx) => {
865
+ const isScriptSelected = isFocused && idx === selectedScriptIndex;
862
866
  contentLines.push(
863
- <Text key={`script-${name}`}>
864
- {' '}
865
- <Text color={colors.accentGreen}>{name}</Text>
867
+ <Text key={`script-${name}`} bold={isScriptSelected}>
868
+ {isScriptSelected ? '▶ ' : ' '}
869
+ <Text color={isScriptSelected ? colors.accentCyan : colors.accentGreen}>{name}</Text>
866
870
  {' - '}
867
871
  <Text color={colors.textSecondary}>{truncateText(script.command, 60)}</Text>
868
872
  </Text>
@@ -1177,6 +1181,7 @@ const App: React.FC = () => {
1177
1181
  // Script selection state
1178
1182
  const [showScriptModal, setShowScriptModal] = useState(false);
1179
1183
  const [scriptModalData, setScriptModalData] = useState<{ scripts: Map<string, any>; projectName: string; projectPath: string } | null>(null);
1184
+ const [detailSelectedScript, setDetailSelectedScript] = useState(0);
1180
1185
 
1181
1186
  // Workspace state
1182
1187
  const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
@@ -1186,6 +1191,7 @@ const App: React.FC = () => {
1186
1191
  const [showTerminalPanel, setShowTerminalPanel] = useState(false);
1187
1192
  const [terminalLogs, setTerminalLogs] = useState<string[]>([]);
1188
1193
  const [selectedProcessPid, setSelectedProcessPid] = useState<number | null>(null);
1194
+ const [selectedProcessIndex, setSelectedProcessIndex] = useState(0);
1189
1195
 
1190
1196
  // Settings state
1191
1197
  const [settings, setSettings] = useState<AppSettings>({
@@ -1256,6 +1262,7 @@ const App: React.FC = () => {
1256
1262
  setEditingTags(false);
1257
1263
  setEditInput('');
1258
1264
  setDetailsScrollOffset(0); // Reset scroll when switching projects
1265
+ setDetailSelectedScript(0); // Reset selected script
1259
1266
  }, [selectedIndex]);
1260
1267
 
1261
1268
  // Load workspaces when switching to workspaces view
@@ -1376,6 +1383,15 @@ const App: React.FC = () => {
1376
1383
  applyFilterAndSort(allProjects, searchQuery, filterType, sortType);
1377
1384
  }, [filterType, sortType, runningProcesses]);
1378
1385
 
1386
+ // Reset/clamp selectedProcessIndex when processes change
1387
+ useEffect(() => {
1388
+ if (runningProcesses.length === 0) {
1389
+ setSelectedProcessIndex(0);
1390
+ } else {
1391
+ setSelectedProcessIndex((prev) => Math.min(prev, runningProcesses.length - 1));
1392
+ }
1393
+ }, [runningProcesses.length]);
1394
+
1379
1395
  const loadRunningProcesses = async () => {
1380
1396
  try {
1381
1397
  const processes = await getRunningProcessesClean();
@@ -1727,10 +1743,36 @@ const App: React.FC = () => {
1727
1743
 
1728
1744
  // Handle navigation in processes view
1729
1745
  if (currentView === 'processes') {
1746
+ if (key.upArrow || input === 'k') {
1747
+ setSelectedProcessIndex((prev) => Math.max(0, prev - 1));
1748
+ return;
1749
+ }
1750
+ if (key.downArrow || input === 'j') {
1751
+ setSelectedProcessIndex((prev) => Math.min(runningProcesses.length - 1, prev + 1));
1752
+ return;
1753
+ }
1730
1754
  if (input === 'x' && runningProcesses.length > 0) {
1731
- // Stop all processes (or could select one)
1755
+ // Stop the selected process
1756
+ const selectedProc = runningProcesses[selectedProcessIndex];
1757
+ if (!selectedProc) return;
1732
1758
  setIsLoading(true);
1733
- setLoadingMessage('Stopping processes...');
1759
+ setLoadingMessage(`Stopping process ${selectedProc.pid}...`);
1760
+ setTimeout(async () => {
1761
+ try {
1762
+ await stopScript(selectedProc.pid);
1763
+ await loadRunningProcesses();
1764
+ setIsLoading(false);
1765
+ } catch (err) {
1766
+ setIsLoading(false);
1767
+ setError(err instanceof Error ? err.message : String(err));
1768
+ }
1769
+ }, 100);
1770
+ return;
1771
+ }
1772
+ if (input === 'X' && runningProcesses.length > 0) {
1773
+ // Stop ALL processes
1774
+ setIsLoading(true);
1775
+ setLoadingMessage('Stopping all processes...');
1734
1776
  setTimeout(async () => {
1735
1777
  try {
1736
1778
  for (const proc of runningProcesses) {
@@ -1969,16 +2011,52 @@ const App: React.FC = () => {
1969
2011
 
1970
2012
  // Details panel actions
1971
2013
  if (focusedPanel === 'details' && selectedProject) {
1972
- // Scroll details panel
2014
+ // Navigate scripts in details panel
1973
2015
  if (key.upArrow || input === 'k') {
1974
- setDetailsScrollOffset(prev => Math.max(0, prev - 1));
2016
+ const projectScripts = getProjectScripts(selectedProject.path);
2017
+ if (projectScripts.scripts.size > 0) {
2018
+ setDetailSelectedScript(prev => Math.max(0, prev - 1));
2019
+ } else {
2020
+ setDetailsScrollOffset(prev => Math.max(0, prev - 1));
2021
+ }
1975
2022
  return;
1976
2023
  }
1977
2024
 
1978
2025
  if (key.downArrow || input === 'j') {
1979
- const visibleHeight = Math.max(1, availableHeight - 3);
1980
- // Estimate content height (rough calculation)
1981
- setDetailsScrollOffset(prev => prev + 1);
2026
+ const projectScripts = getProjectScripts(selectedProject.path);
2027
+ if (projectScripts.scripts.size > 0) {
2028
+ setDetailSelectedScript(prev => Math.min(projectScripts.scripts.size - 1, prev + 1));
2029
+ } else {
2030
+ setDetailsScrollOffset(prev => prev + 1);
2031
+ }
2032
+ return;
2033
+ }
2034
+
2035
+ // Run selected script with Enter
2036
+ if (key.return) {
2037
+ try {
2038
+ const projectScripts = getProjectScripts(selectedProject.path);
2039
+ if (projectScripts.scripts.size > 0) {
2040
+ const scriptArray = Array.from(projectScripts.scripts.keys());
2041
+ const scriptName = scriptArray[detailSelectedScript];
2042
+ if (scriptName) {
2043
+ setIsLoading(true);
2044
+ setLoadingMessage(`Running ${scriptName} in background...`);
2045
+ setTimeout(async () => {
2046
+ try {
2047
+ await runScriptInBackground(selectedProject.path, selectedProject.name, scriptName, [], false);
2048
+ setIsLoading(false);
2049
+ await loadRunningProcesses();
2050
+ } catch (err) {
2051
+ setIsLoading(false);
2052
+ setError(err instanceof Error ? err.message : String(err));
2053
+ }
2054
+ }, 100);
2055
+ }
2056
+ }
2057
+ } catch (err) {
2058
+ setError(err instanceof Error ? err.message : String(err));
2059
+ }
1982
2060
  return;
1983
2061
  }
1984
2062
 
@@ -2283,6 +2361,7 @@ const App: React.FC = () => {
2283
2361
  height={availableHeight}
2284
2362
  scrollOffset={detailsScrollOffset}
2285
2363
  gitBranch={selectedProject ? gitBranches.get(selectedProject.id) || null : null}
2364
+ selectedScriptIndex={detailSelectedScript}
2286
2365
  />
2287
2366
  {showTerminalPanel && (
2288
2367
  <>
@@ -2396,7 +2475,7 @@ const App: React.FC = () => {
2396
2475
  }
2397
2476
  };
2398
2477
 
2399
- // Render Processes view placeholder
2478
+ // Render Processes view
2400
2479
  const renderProcessesView = () => (
2401
2480
  <Box flexDirection="column" padding={2}>
2402
2481
  <Text bold color={colors.accentCyan}>Running Processes ({runningProcesses.length})</Text>
@@ -2404,20 +2483,30 @@ const App: React.FC = () => {
2404
2483
  {runningProcesses.length === 0 ? (
2405
2484
  <Text color={colors.textTertiary}>No running processes</Text>
2406
2485
  ) : (
2407
- runningProcesses.map((proc: any) => {
2486
+ runningProcesses.map((proc: any, index: number) => {
2408
2487
  const uptime = Math.floor((Date.now() - proc.startedAt) / 1000);
2409
2488
  const minutes = Math.floor(uptime / 60);
2410
2489
  const seconds = uptime % 60;
2411
2490
  const uptimeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
2491
+ const isSelected = index === selectedProcessIndex;
2492
+ const portsStr = proc.detectedPorts && proc.detectedPorts.length > 0
2493
+ ? ` [ports: ${proc.detectedPorts.join(', ')}]`
2494
+ : '';
2412
2495
  return (
2413
- <Text key={proc.pid} color={colors.textPrimary}>
2496
+ <Text key={proc.pid} color={isSelected ? colors.accentCyan : colors.textPrimary} bold={isSelected}>
2497
+ {isSelected ? '> ' : ' '}
2414
2498
  <Text color={colors.accentGreen}>●</Text> PID {proc.pid}: {proc.projectName} ({proc.scriptName}) - {uptimeStr}
2499
+ {portsStr && <Text color={colors.accentCyan}>{portsStr}</Text>}
2415
2500
  </Text>
2416
2501
  );
2417
2502
  })
2418
2503
  )}
2419
2504
  <Text> </Text>
2420
- <Text color={colors.textTertiary}>Press 1 to return to Projects</Text>
2505
+ <Text color={colors.textTertiary}>
2506
+ {runningProcesses.length > 0
2507
+ ? 'up/down: navigate | x: stop selected | X: stop all | 1: back to projects'
2508
+ : 'Press 1 to return to Projects'}
2509
+ </Text>
2421
2510
  </Box>
2422
2511
  );
2423
2512
 
package/package.json CHANGED
@@ -1,28 +1,11 @@
1
1
  {
2
2
  "name": "projax",
3
- "version": "3.3.66",
3
+ "version": "3.3.68",
4
4
  "description": "Cross-platform project management dashboard for tracking local development projects. Features CLI, Terminal UI, Desktop app, REST API, and built-in tools for test detection, port management, and script execution.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "prx": "./dist/index.js"
8
8
  },
9
- "scripts": {
10
- "build": "tsc",
11
- "typecheck": "echo \"Skipping cli typecheck (legacy)\"",
12
- "lint": "echo \"Skipping cli lint (not configured)\"",
13
- "lint:fix": "echo \"Skipping cli lint (not configured)\"",
14
- "build:electron": "cd ../desktop && pnpm run build",
15
- "copy:electron": "mkdir -p dist/electron && cp -r ../desktop/dist/* dist/electron/ && cp dist/script-runner.* dist/electron/ && cp dist/port-*.* dist/electron/ && cp -R dist/core dist/electron/",
16
- "copy:api": "mkdir -p dist/api && cp -r ../api/dist/* dist/api/ && cp ../api/package.json dist/api/",
17
- "copy:core": "mkdir -p dist/core && cp -r ../core/dist/* dist/core/",
18
- "copy:prxi": "cp src/prxi.tsx dist/prxi.tsx",
19
- "build:all": "pnpm run build && pnpm run copy:core && pnpm run build:electron && pnpm run copy:electron && pnpm run copy:api && pnpm run copy:prxi",
20
- "clean": "rm -rf dist",
21
- "test": "jest",
22
- "test:watch": "jest --watch",
23
- "test:coverage": "jest --coverage",
24
- "prepublishOnly": "pnpm run build:all"
25
- },
26
9
  "dependencies": {
27
10
  "chokidar": "^3.6.0",
28
11
  "commander": "^11.1.0",
@@ -74,5 +57,21 @@
74
57
  },
75
58
  "homepage": "https://projax.dev",
76
59
  "author": "",
77
- "license": "MIT"
78
- }
60
+ "license": "MIT",
61
+ "scripts": {
62
+ "build": "tsc",
63
+ "typecheck": "echo \"Skipping cli typecheck (legacy)\"",
64
+ "lint": "echo \"Skipping cli lint (not configured)\"",
65
+ "lint:fix": "echo \"Skipping cli lint (not configured)\"",
66
+ "build:electron": "cd ../desktop && pnpm run build",
67
+ "copy:electron": "mkdir -p dist/electron && cp -r ../desktop/dist/* dist/electron/ && cp dist/script-runner.* dist/electron/ && cp dist/port-*.* dist/electron/ && cp -R dist/core dist/electron/",
68
+ "copy:api": "mkdir -p dist/api && cp -r ../api/dist/* dist/api/ && cp ../api/package.json dist/api/",
69
+ "copy:core": "mkdir -p dist/core && cp -r ../core/dist/* dist/core/",
70
+ "copy:prxi": "cp src/prxi.tsx dist/prxi.tsx",
71
+ "build:all": "pnpm run build && pnpm run copy:core && pnpm run build:electron && pnpm run copy:electron && pnpm run copy:api && pnpm run copy:prxi",
72
+ "clean": "rm -rf dist",
73
+ "test": "jest",
74
+ "test:watch": "jest --watch",
75
+ "test:coverage": "jest --coverage"
76
+ }
77
+ }