project-compass 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,8 +5,9 @@ Project Compass is a futuristic CLI navigator built with [Ink](https://github.co
5
5
  ## Highlights
6
6
 
7
7
  - 🔍 Scans directories for Node.js, Python, Rust, Go, Java, and Scala projects by looking at their manifest files.
8
- - Presents a modern Ink dashboard with an interactive project list, icons, and live stdout/stderr logs.
8
+ - 🎨 Combines the glyph-based art board with a Projects/Details row that keeps everything inside the viewport, while live stdout/stderr logs stay in their own band below.
9
9
  - 🚀 Press **Enter** on any project to open the detail view, where you can inspect the type, manifest, frameworks, commands, and save custom actions.
10
+ - 💡 A dedicated output row + help tiles keep logs in one pane, support smooth refresh (Shift+↑/↓ or mouse wheel to scroll, typing feeds stdin, Ctrl+C aborts, L reruns the last command) and include a `?` overlay for quick navigation tips.
10
11
  - 🎯 Built-in shortcuts (B/T/R) run the canonical build/test/run workflow, while numeric hotkeys (1, 2, 3...) execute whichever command is listed in the detail view.
11
12
  - 🧠 Add bespoke commands via **C** in detail view and store them globally (`~/.project-compass/config.json`) so every workspace remembers your favorite invocations.
12
13
  - 🔌 Extend detection via plugins (JSON specs under `~/.project-compass/plugins.json`) to teach Project Compass about extra frameworks or command sets.
@@ -33,6 +34,10 @@ project-compass [--dir /path/to/workspace]
33
34
  | B / T / R | Quick build / test / run actions (when available) |
34
35
  | 1‑9 | Execute the numbered command inside the detail view |
35
36
  | C | Add a custom command (`label|cmd`) that saves to `~/.project-compass/config.json` |
37
+ | Shift ↑ / ↓ | Scroll the output buffer (Shift+arrows or mouse wheel) |
38
+ | L | Rerun the last executed command |
39
+ | ? | Toggle the help overlay with navigation tips |
40
+ | Ctrl+C | Interrupt a running command (works while streaming output) |
36
41
  | Q | Quit |
37
42
 
38
43
  ## Framework & plugin support
@@ -60,6 +65,15 @@ You can teach it new frameworks by adding a `plugins.json` file in your config d
60
65
 
61
66
  Each command value can be a string or an array of tokens. When a plugin matches a project, its commands appear in the detail view with a `framework` badge, and the shortcut keys (B/T/R or numeric) can execute them.
62
67
 
68
+
69
+ ## Art board & detail view
70
+
71
+ Project Compass now opens with a rounded art board that shuffles your glyph row (▁▃▄▅▇ with neon accents) and three branded tiles showing workspace pulse, the selected project focus, and the rhythm of commands. The detail view sits beside the project list as a gallery; border colors, badges, and the ambient header hint keep it feeling like a living installation rather than a vanilla CLI.
72
+
73
+ ## Layout, output & help
74
+
75
+ Projects and details now occupy the same row, while the output panel takes its own full-width band beneath so long logs no longer stretch the rest of the UI. The output pane scrolls independently (Shift+↑/↓ or mouse wheel), accepts keystrokes to feed stdin while a command runs, and honors Ctrl+C to abort. A trio of help tiles underneath highlights navigation cues, command flow, and recent runs, and pressing `?` opens an overlay with extra tips (for example, left-click cycles the project focus, L reruns the last command, and the help overlay reminds you about the new shortcuts).
76
+
63
77
  ## Developer notes
64
78
 
65
79
  - `npm start` launches the Ink UI in the current directory.
@@ -69,4 +83,11 @@ Each command value can be a string or an array of tokens. When a plugin matches
69
83
 
70
84
  ## License
71
85
 
72
- MIT © 2026 Satyaa & Clawdy
86
+ MIT © 2026 Satyaa & Clawdy
87
+
88
+ ## Release & packaging
89
+
90
+ - Bump `package.json`/`package-lock.json` versions (e.g., `npm version 1.0.1 --no-git-tag-version`).
91
+ - Run `npm run lint` and `npm run test` to validate the workspace before publishing.
92
+ - Create the release artifact with `npm pack` (produces `project-compass-<version>.tgz` for uploading to GitHub Releases or npm).
93
+ - Tag the repo `git tag v<version>` and push both commits and tags to publish the release.
@@ -0,0 +1,20 @@
1
+ const {configs} = require('@eslint/js');
2
+
3
+ module.exports = [
4
+ configs.recommended,
5
+ {
6
+ files: ['src/**/*.js'],
7
+ languageOptions: {
8
+ ecmaVersion: 'latest',
9
+ sourceType: 'module',
10
+ globals: {
11
+ process: 'readonly',
12
+ console: 'readonly'
13
+ }
14
+ },
15
+ rules: {
16
+ 'no-console': 'off',
17
+ 'no-undef': 'error'
18
+ }
19
+ }
20
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Ink-based project explorer that detects local repos and lets you build/test/run them without memorizing commands.",
5
5
  "main": "src/cli.js",
6
6
  "type": "module",
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node src/cli.js",
12
- "test": "node src/cli.js --mode test"
12
+ "test": "node src/cli.js --mode test",
13
+ "lint": "eslint src"
13
14
  },
14
15
  "keywords": [
15
16
  "cli",
@@ -25,5 +26,9 @@
25
26
  "fast-glob": "^3.3.3",
26
27
  "ink": "^6.6.0",
27
28
  "kleur": "^4.1.5"
29
+ },
30
+ "devDependencies": {
31
+ "@eslint/js": "^10.0.1",
32
+ "eslint": "^10.0.0"
28
33
  }
29
34
  }
Binary file
package/src/cli.js CHANGED
@@ -1,23 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import React, {useCallback, useEffect, useMemo, useState} from 'react';
2
+ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
3
3
  import {render, Box, Text, useApp, useInput} from 'ink';
4
4
  import fastGlob from 'fast-glob';
5
5
  import path from 'path';
6
6
  import fs from 'fs';
7
7
  import os from 'os';
8
- import {fileURLToPath} from 'url';
9
8
  import kleur from 'kleur';
10
9
  import {execa} from 'execa';
11
10
 
12
11
  const create = React.createElement;
13
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
-
15
12
  const CONFIG_DIR = path.join(os.homedir(), '.project-compass');
16
13
  const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
17
14
  const PLUGIN_FILE = path.join(CONFIG_DIR, 'plugins.json');
18
15
  const DEFAULT_CONFIG = {customCommands: {}};
19
- const DEFAULT_PLUGIN_CONFIG = {plugins: []};
20
-
21
16
  function ensureConfigDir() {
22
17
  if (!fs.existsSync(CONFIG_DIR)) {
23
18
  fs.mkdirSync(CONFIG_DIR, {recursive: true});
@@ -774,6 +769,10 @@ const ACTION_MAP = {
774
769
  t: 'test',
775
770
  r: 'run'
776
771
  };
772
+ const ART_CHARS = ['▁', '▃', '▄', '▅', '▇'];
773
+ const ART_COLORS = ['magenta', 'blue', 'cyan', 'yellow', 'red'];
774
+ const OUTPUT_WINDOW_SIZE = 10;
775
+ const RECENT_RUN_LIMIT = 5;
777
776
 
778
777
  function useScanner(rootPath) {
779
778
  const [state, setState] = useState({projects: [], loading: true, error: null});
@@ -823,16 +822,25 @@ function Compass({rootPath}) {
823
822
  const [selectedIndex, setSelectedIndex] = useState(0);
824
823
  const [viewMode, setViewMode] = useState('list');
825
824
  const [logLines, setLogLines] = useState([]);
825
+ const [logOffset, setLogOffset] = useState(0);
826
826
  const [running, setRunning] = useState(false);
827
827
  const [lastAction, setLastAction] = useState(null);
828
828
  const [customMode, setCustomMode] = useState(false);
829
829
  const [customInput, setCustomInput] = useState('');
830
830
  const [config, setConfig] = useState(() => loadConfig());
831
-
831
+ const [showHelp, setShowHelp] = useState(false);
832
+ const [recentRuns, setRecentRuns] = useState([]);
832
833
  const selectedProject = projects[selectedIndex] || null;
834
+ const runningProcessRef = useRef(null);
835
+ const lastCommandRef = useRef(null);
833
836
 
834
837
  const addLog = useCallback((line) => {
835
- setLogLines((prev) => [...prev.slice(-200), typeof line === 'string' ? line : JSON.stringify(line)]);
838
+ setLogLines((prev) => {
839
+ const normalized = typeof line === 'string' ? line : JSON.stringify(line);
840
+ const next = [...prev, normalized];
841
+ return next.length > 250 ? next.slice(next.length - 250) : next;
842
+ });
843
+ setLogOffset(0);
836
844
  }, []);
837
845
 
838
846
  const detailCommands = useMemo(() => buildDetailCommands(selectedProject, config), [selectedProject, config]);
@@ -846,8 +854,9 @@ function Compass({rootPath}) {
846
854
  return map;
847
855
  }, [detailedIndexed]);
848
856
 
849
- const runProjectCommand = useCallback(async (commandMeta) => {
850
- if (!selectedProject) {
857
+ const runProjectCommand = useCallback(async (commandMeta, targetProject = selectedProject) => {
858
+ const project = targetProject || selectedProject;
859
+ if (!project) {
851
860
  return;
852
861
  }
853
862
  if (!commandMeta || !Array.isArray(commandMeta.command) || commandMeta.command.length === 0) {
@@ -859,16 +868,24 @@ function Compass({rootPath}) {
859
868
  return;
860
869
  }
861
870
 
871
+ const commandLabel = commandMeta.label || commandMeta.command.join(' ');
872
+ lastCommandRef.current = {project, commandMeta};
862
873
  setRunning(true);
863
- setLastAction(`${selectedProject.name} · ${commandMeta.label}`);
874
+ setLastAction(`${project.name} · ${commandLabel}`);
864
875
  const fullCmd = commandMeta.command;
865
876
  addLog(kleur.cyan(`> ${fullCmd.join(' ')}`));
877
+ setRecentRuns((prev) => {
878
+ const entry = {project: project.name, command: commandLabel, time: new Date().toLocaleTimeString()};
879
+ return [entry, ...prev].slice(0, RECENT_RUN_LIMIT);
880
+ });
866
881
 
867
882
  try {
868
883
  const subprocess = execa(fullCmd[0], fullCmd.slice(1), {
869
- cwd: selectedProject.path,
870
- env: process.env
884
+ cwd: project.path,
885
+ env: process.env,
886
+ stdin: 'pipe'
871
887
  });
888
+ runningProcessRef.current = subprocess;
872
889
 
873
890
  subprocess.stdout?.on('data', (chunk) => {
874
891
  addLog(chunk.toString().trimEnd());
@@ -878,13 +895,14 @@ function Compass({rootPath}) {
878
895
  });
879
896
 
880
897
  await subprocess;
881
- addLog(kleur.green(`✓ ${commandMeta.label} finished`));
898
+ addLog(kleur.green(`✓ ${commandLabel} finished`));
882
899
  } catch (error) {
883
- addLog(kleur.red(`✗ ${commandMeta.label} failed: ${error.shortMessage || error.message}`));
900
+ addLog(kleur.red(`✗ ${commandLabel} failed: ${error.shortMessage || error.message}`));
884
901
  } finally {
885
902
  setRunning(false);
903
+ runningProcessRef.current = null;
886
904
  }
887
- }, [selectedProject, addLog, running]);
905
+ }, [addLog, running, selectedProject]);
888
906
 
889
907
  const handleAddCustomCommand = useCallback((label, commandTokens) => {
890
908
  if (!selectedProject) {
@@ -954,11 +972,57 @@ function Compass({rootPath}) {
954
972
  return;
955
973
  }
956
974
 
957
- if (key.upArrow && projects.length > 0) {
975
+ if (key.mouse) {
976
+ if (key.mouse === 'left' && projects.length > 0) {
977
+ setSelectedIndex((prev) => (prev + 1) % projects.length);
978
+ } else if (key.mouse === 'scrollUp') {
979
+ const maxScroll = Math.max(0, logLines.length - OUTPUT_WINDOW_SIZE);
980
+ setLogOffset((prev) => Math.min(maxScroll, prev + 1));
981
+ } else if (key.mouse === 'scrollDown') {
982
+ setLogOffset((prev) => Math.max(0, prev - 1));
983
+ }
984
+ return;
985
+ }
986
+
987
+ if (running && runningProcessRef.current) {
988
+ if (key.ctrl && input === 'c') {
989
+ runningProcessRef.current.kill('SIGINT');
990
+ return;
991
+ }
992
+ if (key.return) {
993
+ runningProcessRef.current.stdin?.write('\n');
994
+ return;
995
+ }
996
+ if (input) {
997
+ runningProcessRef.current.stdin?.write(input);
998
+ }
999
+ return;
1000
+ }
1001
+
1002
+ if (key.shift && key.upArrow) {
1003
+ const maxScroll = Math.max(0, logLines.length - OUTPUT_WINDOW_SIZE);
1004
+ setLogOffset((prev) => Math.min(maxScroll, prev + 1));
1005
+ return;
1006
+ }
1007
+ if (key.shift && key.downArrow) {
1008
+ setLogOffset((prev) => Math.max(0, prev - 1));
1009
+ return;
1010
+ }
1011
+
1012
+ if (input === '?') {
1013
+ setShowHelp((prev) => !prev);
1014
+ return;
1015
+ }
1016
+ if (input === 'l' && lastCommandRef.current) {
1017
+ runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project);
1018
+ return;
1019
+ }
1020
+
1021
+ if (key.upArrow && !key.shift && projects.length > 0) {
958
1022
  setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length);
959
1023
  return;
960
1024
  }
961
- if (key.downArrow && projects.length > 0) {
1025
+ if (key.downArrow && !key.shift && projects.length > 0) {
962
1026
  setSelectedIndex((prev) => (prev + 1) % projects.length);
963
1027
  return;
964
1028
  }
@@ -980,17 +1044,17 @@ function Compass({rootPath}) {
980
1044
  }
981
1045
  if (ACTION_MAP[input]) {
982
1046
  const commandMeta = selectedProject?.commands?.[ACTION_MAP[input]];
983
- runProjectCommand(commandMeta);
1047
+ runProjectCommand(commandMeta, selectedProject);
984
1048
  return;
985
1049
  }
986
1050
  if (viewMode === 'detail' && detailShortcutMap.has(input)) {
987
- runProjectCommand(detailShortcutMap.get(input));
1051
+ runProjectCommand(detailShortcutMap.get(input), selectedProject);
988
1052
  }
989
1053
  });
990
1054
 
991
1055
  const projectRows = [];
992
1056
  if (loading) {
993
- projectRows.push(create(Text, {dimColor: true}, 'Scanning for projects…'));
1057
+ projectRows.push(create(Text, {dimColor: true}, 'Scanning projects…'));
994
1058
  }
995
1059
  if (error) {
996
1060
  projectRows.push(create(Text, {color: 'red'}, `Unable to scan: ${error}`));
@@ -1000,24 +1064,21 @@ function Compass({rootPath}) {
1000
1064
  }
1001
1065
  if (!loading) {
1002
1066
  projects.forEach((project, index) => {
1067
+ const isSelected = index === selectedIndex;
1003
1068
  const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
1004
1069
  projectRows.push(
1005
1070
  create(
1006
1071
  Box,
1007
- {key: project.id, flexDirection: 'column', marginBottom: 1},
1072
+ {key: project.id, flexDirection: 'column', marginBottom: 1, padding: 1},
1008
1073
  create(
1009
- Box,
1010
- {flexDirection: 'row'},
1011
- create(
1012
- Text,
1013
- {
1014
- color: index === selectedIndex ? 'green' : undefined,
1015
- bold: index === selectedIndex
1016
- },
1017
- `${project.icon} ${project.name}`
1018
- ),
1019
- create(Text, {dimColor: true}, ` ${project.type} · ${path.relative(rootPath, project.path) || '.'}`)
1074
+ Text,
1075
+ {
1076
+ color: isSelected ? 'cyan' : 'white',
1077
+ bold: isSelected
1078
+ },
1079
+ `${project.icon} ${project.name}`
1020
1080
  ),
1081
+ create(Text, {dimColor: true}, ` ${project.type} · ${path.relative(rootPath, project.path) || '.'}`),
1021
1082
  frameworkBadges && create(Text, {dimColor: true}, ` ${frameworkBadges}`)
1022
1083
  )
1023
1084
  );
@@ -1027,7 +1088,7 @@ function Compass({rootPath}) {
1027
1088
  const detailContent = [];
1028
1089
  if (viewMode === 'detail' && selectedProject) {
1029
1090
  detailContent.push(
1030
- create(Text, {color: 'yellow', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
1091
+ create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
1031
1092
  create(Text, {dimColor: true}, `${selectedProject.type} · ${selectedProject.manifest || 'detected manifest'}`),
1032
1093
  create(Text, {dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
1033
1094
  );
@@ -1046,7 +1107,7 @@ function Compass({rootPath}) {
1046
1107
  detailContent.push(create(Text, {bold: true, marginTop: 1}, 'Commands'));
1047
1108
  detailedIndexed.forEach((command) => {
1048
1109
  detailContent.push(
1049
- create(Text, {key: `${command.shortcut}-${command.label}`}, `${command.shortcut}. ${command.label} ${command.source === 'custom' ? kleur.magenta('(custom)') : command.source === 'framework' ? kleur.cyan('(framework)') : ''}`)
1110
+ create(Text, {key: `detail-${command.shortcut}-${command.label}`}, `${command.shortcut}. ${command.label} ${command.source === 'custom' ? kleur.magenta('(custom)') : command.source === 'framework' ? kleur.cyan('(framework)') : ''}`)
1050
1111
  );
1051
1112
  detailContent.push(create(Text, {dimColor: true}, ` ↳ ${command.command.join(' ')}`));
1052
1113
  });
@@ -1062,10 +1123,162 @@ function Compass({rootPath}) {
1062
1123
  detailContent.push(create(Text, {color: 'cyan'}, `Type label|cmd (Enter to save, Esc to cancel): ${customInput}`));
1063
1124
  }
1064
1125
 
1065
- const logNodes = logLines.length
1066
- ? logLines.map((line, index) => create(Text, {key: `${line}-${index}`}, line))
1126
+ const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}`;
1127
+ const artTileNodes = useMemo(() => {
1128
+ const selectedName = selectedProject?.name || 'Awaiting selection';
1129
+ const selectedType = selectedProject?.type || 'Unknown stack';
1130
+ const selectedLocation = selectedProject?.path ? path.relative(rootPath, selectedProject.path) || '.' : '—';
1131
+ const statusNarrative = running ? 'Running commands' : lastAction ? `Last: ${lastAction}` : 'Idle gallery';
1132
+ const workspaceName = path.basename(rootPath) || rootPath;
1133
+ const tileDefinition = [
1134
+ {
1135
+ label: 'Pulse',
1136
+ detail: projectCountLabel,
1137
+ accent: 'magenta',
1138
+ icon: '●',
1139
+ subtext: `Workspace · ${workspaceName}`
1140
+ },
1141
+ {
1142
+ label: 'Focus',
1143
+ detail: selectedName,
1144
+ accent: 'cyan',
1145
+ icon: '◆',
1146
+ subtext: `${selectedType} · ${selectedLocation}`
1147
+ },
1148
+ {
1149
+ label: 'Rhythm',
1150
+ detail: `${detailCommands.length} commands`,
1151
+ accent: 'yellow',
1152
+ icon: '■',
1153
+ subtext: statusNarrative
1154
+ }
1155
+ ];
1156
+ return tileDefinition.map((tile) =>
1157
+ create(
1158
+ Box,
1159
+ {
1160
+ key: tile.label,
1161
+ flexDirection: 'column',
1162
+ padding: 1,
1163
+ marginRight: 1,
1164
+ borderStyle: 'single',
1165
+ borderColor: tile.accent,
1166
+ minWidth: 24
1167
+ },
1168
+ create(Text, {color: tile.accent, bold: true}, `${tile.icon} ${tile.label}`),
1169
+ create(Text, {bold: true}, tile.detail),
1170
+ create(Text, {dimColor: true}, tile.subtext)
1171
+ )
1172
+ );
1173
+ }, [projectCountLabel, rootPath, selectedProject, detailCommands.length, running, lastAction]);
1174
+
1175
+ const artBoard = create(
1176
+ Box,
1177
+ {
1178
+ flexDirection: 'column',
1179
+ marginTop: 1,
1180
+ borderStyle: 'round',
1181
+ borderColor: 'gray',
1182
+ padding: 1
1183
+ },
1184
+ create(
1185
+ Box,
1186
+ {flexDirection: 'row', justifyContent: 'space-between'},
1187
+ create(Text, {color: 'magenta', bold: true}, 'Art-coded build atlas'),
1188
+ create(Text, {dimColor: true}, 'press ? for overlay help')
1189
+ ),
1190
+ create(
1191
+ Box,
1192
+ {flexDirection: 'row', marginTop: 1},
1193
+ ...ART_CHARS.map((char, index) =>
1194
+ create(Text, {key: `art-${index}`, color: ART_COLORS[index % ART_COLORS.length]}, char.repeat(2))
1195
+ )
1196
+ ),
1197
+ create(
1198
+ Box,
1199
+ {flexDirection: 'row', marginTop: 1},
1200
+ ...artTileNodes
1201
+ ),
1202
+ create(Text, {dimColor: true, marginTop: 1}, kleur.italic('The art board now follows your layout and stays inside the window.'))
1203
+ );
1204
+
1205
+ const logWindowStart = Math.max(0, logLines.length - OUTPUT_WINDOW_SIZE - logOffset);
1206
+ const logWindowEnd = Math.max(0, logLines.length - logOffset);
1207
+ const visibleLogs = logLines.slice(logWindowStart, logWindowEnd);
1208
+ const logNodes = visibleLogs.length
1209
+ ? visibleLogs.map((line, index) => create(Text, {key: `${logWindowStart + index}-${line}`}, line))
1067
1210
  : [create(Text, {dimColor: true}, 'Logs will appear here once you run a command.')];
1068
1211
 
1212
+ const helpCards = [
1213
+ {
1214
+ label: 'Navigation',
1215
+ color: 'magenta',
1216
+ body: [
1217
+ '↑/↓ select projects, Enter toggles detail view',
1218
+ 'Shift+↑/↓ or mouse wheel scrolls logs',
1219
+ '? toggles this overlay, left click cycles the list'
1220
+ ]
1221
+ },
1222
+ {
1223
+ label: 'Command flow',
1224
+ color: 'cyan',
1225
+ body: [
1226
+ 'B/T/R run build/test/run for the focused project',
1227
+ '1-9 execute the numbered detail commands',
1228
+ 'L reruns the last command, Ctrl+C aborts it',
1229
+ 'Type while a command is running to feed stdin'
1230
+ ]
1231
+ },
1232
+ {
1233
+ label: 'Recent runs',
1234
+ color: 'yellow',
1235
+ body: recentRuns.length
1236
+ ? recentRuns.map((run) => `${run.time} · ${run.project}: ${run.command}`)
1237
+ : ['No runs yet. Start one with B/T/R.']
1238
+ }
1239
+ ];
1240
+
1241
+ const helpSection = create(
1242
+ Box,
1243
+ {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between'},
1244
+ ...helpCards.map((card, index) =>
1245
+ create(
1246
+ Box,
1247
+ {
1248
+ key: card.label,
1249
+ flexGrow: 1,
1250
+ flexBasis: 0,
1251
+ marginRight: index < helpCards.length - 1 ? 1 : 0,
1252
+ borderStyle: 'round',
1253
+ borderColor: card.color,
1254
+ padding: 1
1255
+ },
1256
+ create(Text, {color: card.color, bold: true}, card.label),
1257
+ ...card.body.map((line, lineIndex) =>
1258
+ create(Text, {key: `${card.label}-${lineIndex}`, dimColor: card.color === 'yellow'}, line)
1259
+ )
1260
+ )
1261
+ )
1262
+ );
1263
+
1264
+ const helpOverlay = showHelp
1265
+ ? create(
1266
+ Box,
1267
+ {
1268
+ flexDirection: 'column',
1269
+ borderStyle: 'double',
1270
+ borderColor: 'cyan',
1271
+ marginTop: 1,
1272
+ padding: 1
1273
+ },
1274
+ create(Text, {color: 'cyan', bold: true}, 'Help overlay · press ? to hide'),
1275
+ create(Text, null, 'Clicking cycles selections and scrolling the mouse wheel moves the output view.'),
1276
+ create(Text, null, 'When a command is running you can type to feed stdin, Enter sends newline, Ctrl+C aborts.'),
1277
+ create(Text, null, 'Layout now keeps Projects and Details side-by-side with Output below so nothing stretches.'),
1278
+ create(Text, null, 'Use L to rerun the previous command and Shift + arrows or the mouse wheel to scroll the output window.')
1279
+ )
1280
+ : null;
1281
+
1069
1282
  const headerHint = viewMode === 'detail'
1070
1283
  ? `Detail mode · 1-${Math.max(detailedIndexed.length, 1)} to execute, C: add custom commands, Enter: back to list, q: quit`
1071
1284
  : `Quick run · B/T/R to build/test/run, Enter: view details, q: quit`;
@@ -1079,53 +1292,80 @@ function Compass({rootPath}) {
1079
1292
  create(
1080
1293
  Box,
1081
1294
  {flexDirection: 'column'},
1082
- create(Text, {color: 'cyan', bold: true}, 'Project Compass'),
1083
- create(Text, null, loading ? 'Scanning workspaces…' : `${projects.length} project(s) detected in ${rootPath}`)
1295
+ create(Text, {color: 'magenta', bold: true}, 'Project Compass'),
1296
+ create(Text, {dimColor: true}, loading ? 'Scanning workspaces…' : `${projectCountLabel} detected in ${rootPath}`),
1297
+ create(Text, {dimColor: true}, 'Mouse support: click to cycle, wheel to scroll output.')
1084
1298
  ),
1085
1299
  create(
1086
1300
  Box,
1087
1301
  {flexDirection: 'column', alignItems: 'flex-end'},
1088
- create(Text, null, running ? 'Busy 🔁' : lastAction ? `Last: ${lastAction}` : 'Idle'),
1302
+ create(Text, {color: running ? 'yellow' : 'green'}, running ? 'Busy streaming...' : lastAction ? `Last action · ${lastAction}` : 'Idle'),
1089
1303
  create(Text, {dimColor: true}, headerHint)
1090
1304
  )
1091
1305
  ),
1306
+ artBoard,
1092
1307
  create(
1093
1308
  Box,
1094
- {marginTop: 1},
1309
+ {marginTop: 1, flexDirection: 'row', alignItems: 'stretch'},
1095
1310
  create(
1096
1311
  Box,
1097
- {flexDirection: 'column', width: 60, marginRight: 2},
1098
- create(Text, {bold: true}, 'Projects'),
1312
+ {
1313
+ flex: 1,
1314
+ marginRight: 1,
1315
+ borderStyle: 'round',
1316
+ borderColor: 'magenta',
1317
+ padding: 1
1318
+ },
1319
+ create(Text, {bold: true, color: 'magenta'}, 'Projects'),
1099
1320
  create(Box, {flexDirection: 'column', marginTop: 1}, ...projectRows)
1100
1321
  ),
1101
1322
  create(
1102
1323
  Box,
1103
1324
  {
1104
- flexDirection: 'column',
1105
- width: 44,
1106
- marginRight: 2,
1325
+ flex: 1,
1107
1326
  borderStyle: 'round',
1108
- borderColor: 'gray',
1327
+ borderColor: 'cyan',
1109
1328
  padding: 1
1110
1329
  },
1111
- create(Text, {bold: true}, 'Details'),
1330
+ create(Text, {bold: true, color: 'cyan'}, 'Details'),
1112
1331
  ...detailContent
1332
+ )
1333
+ ),
1334
+ create(
1335
+ Box,
1336
+ {marginTop: 1, flexDirection: 'column'},
1337
+ create(
1338
+ Box,
1339
+ {flexDirection: 'row', justifyContent: 'space-between'},
1340
+ create(Text, {bold: true, color: 'yellow'}, `Output ${running ? '· Running' : ''}`),
1341
+ create(Text, {dimColor: true}, logOffset ? `Scrolled ${logOffset} lines` : 'Live log view')
1113
1342
  ),
1114
1343
  create(
1115
1344
  Box,
1116
1345
  {
1117
1346
  flexDirection: 'column',
1118
- flexGrow: 1,
1119
1347
  borderStyle: 'round',
1120
- borderColor: 'gray'
1348
+ borderColor: 'yellow',
1349
+ padding: 1,
1350
+ minHeight: OUTPUT_WINDOW_SIZE + 2,
1351
+ overflow: 'hidden'
1121
1352
  },
1122
- create(Text, {bold: true}, 'Output'),
1123
- create(Box, {flexDirection: 'column', marginTop: 1, height: 12, overflow: 'hidden'}, ...logNodes)
1353
+ ...logNodes
1354
+ ),
1355
+ create(
1356
+ Text,
1357
+ {dimColor: true, marginTop: 1},
1358
+ running
1359
+ ? 'Type to feed stdin, Enter submits, Ctrl+C aborts, Shift+Arrows or mouse wheel scrolls the buffer.'
1360
+ : 'Run a command or press ? for extra help.'
1124
1361
  )
1125
- )
1362
+ ),
1363
+ helpSection,
1364
+ helpOverlay
1126
1365
  );
1127
1366
  }
1128
1367
 
1368
+
1129
1369
  function parseArgs() {
1130
1370
  const args = {};
1131
1371
  const tokens = process.argv.slice(2);