project-compass 2.7.1 โ†’ 2.8.1

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/README.md +9 -16
  2. package/package.json +1 -1
  3. package/src/cli.js +167 -526
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Project Compass (v2.6.0)
1
+ # Project Compass (v2.8.0)
2
2
 
3
3
  Project Compass is a futuristic CLI navigator built with [Ink](https://github.com/vadimdemedes/ink) that scans your current folder tree for familiar code projects and gives you one-keystroke access to build, test, or run them.
4
4
 
@@ -9,6 +9,7 @@ Project Compass is a futuristic CLI navigator built with [Ink](https://github.co
9
9
  - ๐Ÿš€ **New Keyboard-Centric UX**: Shortcuts now use **Shift** instead of Ctrl to avoid terminal interference.
10
10
  - ๐Ÿ’ก **Refined Output**: Improved stdin buffer with proper spacing and reliable scrolling (Shift+โ†‘/โ†“).
11
11
  - ๐Ÿง  **Smart Detection**: Support for 20+ frameworks including **Spring Boot** (Maven/Gradle), **ASP.NET Core**, **Rocket/Actix** (Rust), **Laravel** (PHP), **Vite**, **Prisma**, and more.
12
+ - ๐Ÿ›ฐ๏ธ **Orbit Task Manager**: Run commands and **Detach** (**Shift+D**) them to the background. Manage multiple processes via **Shift+T**.
12
13
  - โš ๏ธ **Runtime Health**: Automatically checks if the required language/runtime (e.g., `node`, `python`, `cargo`) is installed and warns you if it's missing.
13
14
  - ๐Ÿ’Ž **Omni-Studio**: A new interactive environment intelligence mode to see all installed runtimes and versions.
14
15
  - ๐Ÿ“‚ **Log Management**: Clear output with **Shift+X** or export logs to a text file with **Shift+E**.
@@ -33,11 +34,13 @@ project-compass [--dir /path/to/workspace] [--studio]
33
34
  | โ†‘ / โ†“ | Move focus, **Enter**: toggle details |
34
35
  | B / T / R | Build / Test / Run |
35
36
  | 1โ€‘9 | Execute numbered detail commands |
37
+ | **Shift+T** | Open **Orbit Task Manager** |
38
+ | **Shift+D** | **Detach** from active task (runs in background) |
36
39
  | **Shift+A** | Open **Omni-Studio** (Environment View) |
37
40
  | **Shift+C** | Add a custom command (`label|cmd`) |
38
41
  | **Shift+X** | **Clear output logs** |
39
42
  | **Shift+E** | **Export logs to .txt** |
40
- | **Shift โ†‘ / โ†“** | Scroll output buffer (Intuitive Direction) |
43
+ | **Shift โ†‘ / โ†“** | Scroll output buffer |
41
44
  | **Shift+L** | Rerun last command |
42
45
  | **Shift+H** | Toggle help cards |
43
46
  | **Shift+S** | Toggle structure guide |
@@ -45,23 +48,13 @@ project-compass [--dir /path/to/workspace] [--studio]
45
48
  | ? | Toggle help overlay |
46
49
  | Ctrl+C | Interrupt running command |
47
50
 
48
- ## Omni-Studio
49
-
50
- Launch with `project-compass --studio` or press **Shift+A** inside the app. Omni-Studio provides real-time intelligence on your installed development environments, checking versions for Node, Python, Rust, Go, Java, and more.
51
+ ## Orbit Task Manager
51
52
 
52
- ## Layout & UX
53
+ Project Compass v2.8 introduces background task management. You can start a build, press **Shift+D** to detach and return to the navigator, then start another task. Switch between them or view logs via the Task Manager (**Shift+T**).
53
54
 
54
- Project Compass features a split layout where Projects and Details stay paired while Output takes a full-width band. The stdin buffer (at the bottom) now has a clear distinction between the label and your input for better readability. The help cards (Shift+H) have been refactored for a cleaner, more readable look.
55
-
56
- ## Frameworks
55
+ ## Omni-Studio
57
56
 
58
- Supports a wide array of modern stacks:
59
- - **Node.js**: Next.js, React, Vue, NestJS, Vite, Prisma, Tailwind
60
- - **Python**: FastAPI, Django, Flask
61
- - **Java/Kotlin**: Spring Boot (Maven & Gradle)
62
- - **Rust**: Rocket, Actix Web
63
- - **.NET**: ASP.NET Core
64
- - **PHP**: Laravel
57
+ Launch with `project-compass --studio` or press **Shift+A** inside the app. Omni-Studio provides real-time intelligence on your installed development environments, checking versions for Node, Python, Rust, Go, Java, and more.
65
58
 
66
59
  ## License
67
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-compass",
3
- "version": "2.7.1",
3
+ "version": "2.8.1",
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",
package/src/cli.js CHANGED
@@ -6,7 +6,7 @@ import fs from 'fs';
6
6
  import kleur from 'kleur';
7
7
  import {execa} from 'execa';
8
8
  import {discoverProjects, SCHEMA_GUIDE, checkBinary} from './projectDetection.js';
9
- import {CONFIG_PATH, PLUGIN_FILE, ensureConfigDir} from './configPaths.js';
9
+ import {CONFIG_PATH, ensureConfigDir} from './configPaths.js';
10
10
 
11
11
  const create = React.createElement;
12
12
  const DEFAULT_CONFIG = {customCommands: {}};
@@ -17,7 +17,6 @@ const OUTPUT_WINDOW_HEIGHT = OUTPUT_WINDOW_SIZE + 2;
17
17
  const PROJECTS_MIN_WIDTH = 32;
18
18
  const DETAILS_MIN_WIDTH = 44;
19
19
  const HELP_CARD_MIN_WIDTH = 28;
20
- const RECENT_RUN_LIMIT = 5;
21
20
  const ACTION_MAP = {
22
21
  b: 'build',
23
22
  t: 'test',
@@ -172,10 +171,9 @@ function Compass({rootPath, initialView = 'navigator'}) {
172
171
  const [selectedIndex, setSelectedIndex] = useState(0);
173
172
  const [viewMode, setViewMode] = useState('list');
174
173
  const [mainView, setMainView] = useState(initialView);
175
- const [logLines, setLogLines] = useState([]);
174
+ const [tasks, setTasks] = useState([]);
175
+ const [activeTaskId, setActiveTaskId] = useState(null);
176
176
  const [logOffset, setLogOffset] = useState(0);
177
- const [running, setRunning] = useState(false);
178
- const [lastAction, setLastAction] = useState(null);
179
177
  const [customMode, setCustomMode] = useState(false);
180
178
  const [customInput, setCustomInput] = useState('');
181
179
  const [customCursor, setCustomCursor] = useState(0);
@@ -185,18 +183,21 @@ function Compass({rootPath, initialView = 'navigator'}) {
185
183
  const [stdinBuffer, setStdinBuffer] = useState('');
186
184
  const [stdinCursor, setStdinCursor] = useState(0);
187
185
  const [showHelp, setShowHelp] = useState(false);
188
- const [recentRuns, setRecentRuns] = useState([]);
189
186
  const selectedProject = projects[selectedIndex] || null;
190
- const runningProcessRef = useRef(null);
187
+ const runningProcessMap = useRef(new Map());
191
188
  const lastCommandRef = useRef(null);
192
189
 
193
- const addLog = useCallback((line) => {
194
- setLogLines((prev) => {
190
+ const activeTask = useMemo(() => tasks.find(t => t.id === activeTaskId), [tasks, activeTaskId]);
191
+ const running = activeTask?.status === 'running';
192
+
193
+ const addLogToTask = useCallback((taskId, line) => {
194
+ setTasks(prev => prev.map(t => {
195
+ if (t.id !== taskId) return t;
195
196
  const normalized = typeof line === 'string' ? line : JSON.stringify(line);
196
197
  const lines = normalized.split(/\r?\n/).filter(l => l.trim().length > 0);
197
- const appended = [...prev, ...lines];
198
- return appended.length > 500 ? appended.slice(appended.length - 500) : appended;
199
- });
198
+ const nextLogs = [...t.logs, ...lines];
199
+ return { ...t, logs: nextLogs.length > 500 ? nextLogs.slice(-500) : nextLogs };
200
+ }));
200
201
  }, []);
201
202
 
202
203
  const detailCommands = useMemo(() => buildDetailCommands(selectedProject, config), [selectedProject, config]);
@@ -212,85 +213,65 @@ function Compass({rootPath, initialView = 'navigator'}) {
212
213
 
213
214
  const runProjectCommand = useCallback(async (commandMeta, targetProject = selectedProject) => {
214
215
  const project = targetProject || selectedProject;
215
- if (!project) {
216
- return;
217
- }
218
- if (!commandMeta || !Array.isArray(commandMeta.command) || commandMeta.command.length === 0) {
219
- addLog(kleur.gray('(no command configured)'));
220
- return;
221
- }
222
- if (running) {
223
- addLog(kleur.yellow('โ†’ Wait for the current task to finish.'));
224
- return;
225
- }
216
+ if (!project) return;
217
+ if (!commandMeta || !Array.isArray(commandMeta.command) || commandMeta.command.length === 0) return;
226
218
 
227
219
  const commandLabel = commandMeta.label || commandMeta.command.join(' ');
220
+ const taskId = `task-${Date.now()}`;
221
+ const newTask = {
222
+ id: taskId,
223
+ name: `${project.name} ยท ${commandLabel}`,
224
+ status: 'running',
225
+ logs: [kleur.cyan(`> ${commandMeta.command.join(' ')}`)],
226
+ project: project.name
227
+ };
228
+
229
+ setTasks(prev => [...prev, newTask]);
230
+ setActiveTaskId(taskId);
228
231
  lastCommandRef.current = {project, commandMeta};
229
- setRunning(true);
230
- setLastAction(`${project.name} ยท ${commandLabel}`);
231
- const fullCmd = commandMeta.command;
232
- addLog(kleur.cyan(`> ${fullCmd.join(' ')}`));
233
- setRecentRuns((prev) => {
234
- const entry = {project: project.name, command: commandLabel, time: new Date().toLocaleTimeString()};
235
- return [entry, ...prev].slice(0, RECENT_RUN_LIMIT);
236
- });
237
232
 
238
233
  try {
239
- const subprocess = execa(fullCmd[0], fullCmd.slice(1), {
234
+ const subprocess = execa(commandMeta.command[0], commandMeta.command.slice(1), {
240
235
  cwd: project.path,
241
236
  env: process.env,
242
237
  stdin: 'pipe'
243
238
  });
244
- runningProcessRef.current = subprocess;
239
+ runningProcessMap.current.set(taskId, subprocess);
245
240
 
246
- subprocess.stdout?.on('data', (chunk) => {
247
- addLog(chunk.toString());
248
- });
249
- subprocess.stderr?.on('data', (chunk) => {
250
- addLog(kleur.red(chunk.toString()));
251
- });
241
+ subprocess.stdout?.on('data', (chunk) => addLogToTask(taskId, chunk.toString()));
242
+ subprocess.stderr?.on('data', (chunk) => addLogToTask(taskId, kleur.red(chunk.toString())));
252
243
 
253
244
  await subprocess;
254
- addLog(kleur.green(`โœ“ ${commandLabel} finished`));
245
+ setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'finished'} : t));
246
+ addLogToTask(taskId, kleur.green(`โœ“ ${commandLabel} finished`));
255
247
  } catch (error) {
256
- addLog(kleur.red(`โœ— ${commandLabel} failed: ${error.shortMessage || error.message}`));
248
+ setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'failed'} : t));
249
+ addLogToTask(taskId, kleur.red(`โœ— ${commandLabel} failed: ${error.shortMessage || error.message}`));
257
250
  } finally {
258
- setRunning(false);
259
- setStdinBuffer('');
260
- setStdinCursor(0);
261
- runningProcessRef.current = null;
251
+ runningProcessMap.current.delete(taskId);
262
252
  }
263
- }, [addLog, running, selectedProject]);
253
+ }, [addLogToTask, selectedProject]);
264
254
 
265
255
  const handleAddCustomCommand = useCallback((label, commandTokens) => {
266
- if (!selectedProject) {
267
- return;
268
- }
256
+ if (!selectedProject) return;
269
257
  setConfig((prev) => {
270
258
  const projectKey = selectedProject.path;
271
259
  const existing = prev.customCommands?.[projectKey] || [];
272
- const nextCustom = [...existing, {label, command: commandTokens}];
273
260
  const nextConfig = {
274
261
  ...prev,
275
262
  customCommands: {
276
263
  ...prev.customCommands,
277
- [projectKey]: nextCustom
264
+ [projectKey]: [...existing, {label, command: commandTokens}]
278
265
  }
279
266
  };
280
267
  saveConfig(nextConfig);
281
268
  return nextConfig;
282
269
  });
283
- addLog(kleur.yellow(`Saved custom command "${label}" for ${selectedProject.name}`));
284
- }, [selectedProject, addLog]);
270
+ }, [selectedProject]);
285
271
 
286
272
  const handleCustomSubmit = useCallback(() => {
287
273
  const raw = customInput.trim();
288
- if (!selectedProject) {
289
- setCustomMode(false);
290
- return;
291
- }
292
- if (!raw) {
293
- addLog(kleur.gray('Canceled custom command (empty).'));
274
+ if (!selectedProject || !raw) {
294
275
  setCustomMode(false);
295
276
  setCustomInput('');
296
277
  setCustomCursor(0);
@@ -299,44 +280,33 @@ function Compass({rootPath, initialView = 'navigator'}) {
299
280
  const [labelPart, commandPart] = raw.split('|');
300
281
  const commandTokens = (commandPart || labelPart).trim().split(/\s+/).filter(Boolean);
301
282
  if (!commandTokens.length) {
302
- addLog(kleur.red('Custom command needs at least one token.'));
303
283
  setCustomMode(false);
304
284
  setCustomInput('');
305
285
  setCustomCursor(0);
306
286
  return;
307
287
  }
308
288
  const label = commandPart ? labelPart.trim() : `Custom ${selectedProject.name}`;
309
- handleAddCustomCommand(label || 'Custom', commandTokens);
289
+ handleAddCustomCommand(label, commandTokens);
310
290
  setCustomMode(false);
311
291
  setCustomInput('');
312
292
  setCustomCursor(0);
313
- }, [customInput, selectedProject, handleAddCustomCommand, addLog]);
293
+ }, [customInput, selectedProject, handleAddCustomCommand]);
314
294
 
315
295
  const exportLogs = useCallback(() => {
316
- if (!logLines.length) {
317
- return;
318
- }
296
+ if (!activeTask || !activeTask.logs.length) return;
319
297
  try {
320
- const exportPath = path.resolve(process.cwd(), `compass-logs-${Date.now()}.txt`);
321
- fs.writeFileSync(exportPath, logLines.join('\n'));
322
- addLog(kleur.green(`โœ“ Logs exported to ${exportPath}`));
298
+ const exportPath = path.resolve(process.cwd(), `compass-${activeTask.id}.txt`);
299
+ fs.writeFileSync(exportPath, activeTask.logs.join('\n'));
300
+ addLogToTask(activeTaskId, kleur.green(`โœ“ Logs exported to ${exportPath}`));
323
301
  } catch (err) {
324
- addLog(kleur.red(`โœ— Export failed: ${err.message}`));
302
+ addLogToTask(activeTaskId, kleur.red(`โœ— Export failed: ${err.message}`));
325
303
  }
326
- }, [logLines, addLog]);
304
+ }, [activeTask, activeTaskId, addLogToTask]);
327
305
 
328
306
  useInput((input, key) => {
329
307
  if (customMode) {
330
- if (key.return) {
331
- handleCustomSubmit();
332
- return;
333
- }
334
- if (key.escape) {
335
- setCustomMode(false);
336
- setCustomInput('');
337
- setCustomCursor(0);
338
- return;
339
- }
308
+ if (key.return) { handleCustomSubmit(); return; }
309
+ if (key.escape) { setCustomMode(false); setCustomInput(''); setCustomCursor(0); return; }
340
310
  if (key.backspace || key.delete) {
341
311
  if (customCursor > 0) {
342
312
  setCustomInput((prev) => prev.slice(0, customCursor - 1) + prev.slice(customCursor));
@@ -344,14 +314,8 @@ function Compass({rootPath, initialView = 'navigator'}) {
344
314
  }
345
315
  return;
346
316
  }
347
- if (key.leftArrow) {
348
- setCustomCursor(c => Math.max(0, c - 1));
349
- return;
350
- }
351
- if (key.rightArrow) {
352
- setCustomCursor(c => Math.min(customInput.length, c + 1));
353
- return;
354
- }
317
+ if (key.leftArrow) { setCustomCursor(c => Math.max(0, c - 1)); return; }
318
+ if (key.rightArrow) { setCustomCursor(c => Math.min(customInput.length, c + 1)); return; }
355
319
  if (input) {
356
320
  setCustomInput((prev) => prev.slice(0, customCursor) + input + prev.slice(customCursor));
357
321
  setCustomCursor(c => c + input.length);
@@ -360,51 +324,28 @@ function Compass({rootPath, initialView = 'navigator'}) {
360
324
  }
361
325
 
362
326
  const normalizedInput = input?.toLowerCase();
363
- const ctrlCombo = (char) => key.ctrl && normalizedInput === char;
364
327
  const shiftCombo = (char) => key.shift && normalizedInput === char;
365
- const toggleShortcut = (char) => shiftCombo(char);
366
- if (toggleShortcut('h')) {
367
- setShowHelpCards((prev) => !prev);
368
- return;
369
- }
370
- if (toggleShortcut('s')) {
371
- setShowStructureGuide((prev) => !prev);
372
- return;
373
- }
374
- if (toggleShortcut('a')) {
375
- setMainView((prev) => (prev === 'navigator' ? 'studio' : 'navigator'));
376
- return;
377
- }
378
- if (toggleShortcut('x')) {
379
- setLogLines([]);
380
- setLogOffset(0);
381
- return;
382
- }
383
- if (shiftCombo('e')) {
384
- exportLogs();
385
- return;
386
- }
328
+
329
+ if (shiftCombo('h')) { setShowHelpCards((prev) => !prev); return; }
330
+ if (shiftCombo('s')) { setShowStructureGuide((prev) => !prev); return; }
331
+ if (shiftCombo('a')) { setMainView((prev) => (prev === 'navigator' ? 'studio' : 'navigator')); return; }
332
+ if (shiftCombo('x')) { setTasks(prev => prev.map(t => t.id === activeTaskId ? {...t, logs: []} : t)); setLogOffset(0); return; }
333
+ if (shiftCombo('e')) { exportLogs(); return; }
334
+ if (shiftCombo('d')) { setActiveTaskId(null); return; }
335
+ if (shiftCombo('t')) { setMainView('tasks'); return; }
387
336
 
388
337
  const scrollLogs = (delta) => {
389
338
  setLogOffset((prev) => {
390
- const maxScroll = Math.max(0, logLines.length - OUTPUT_WINDOW_SIZE);
339
+ const logs = activeTask?.logs || [];
340
+ const maxScroll = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE);
391
341
  return Math.max(0, Math.min(maxScroll, prev + delta));
392
342
  });
393
343
  };
394
344
 
395
- if (running && runningProcessRef.current) {
396
- if (key.ctrl && input === 'c') {
397
- runningProcessRef.current.kill('SIGINT');
398
- setStdinBuffer('');
399
- setStdinCursor(0);
400
- return;
401
- }
402
- if (key.return) {
403
- runningProcessRef.current.stdin?.write(stdinBuffer + '\n');
404
- setStdinBuffer('');
405
- setStdinCursor(0);
406
- return;
407
- }
345
+ if (running && activeTaskId && runningProcessMap.current.has(activeTaskId)) {
346
+ const proc = runningProcessMap.current.get(activeTaskId);
347
+ if (key.ctrl && input === 'c') { proc.kill('SIGINT'); setStdinBuffer(''); setStdinCursor(0); return; }
348
+ if (key.return) { proc.stdin?.write(stdinBuffer + '\n'); setStdinBuffer(''); setStdinCursor(0); return; }
408
349
  if (key.backspace || key.delete) {
409
350
  if (stdinCursor > 0) {
410
351
  setStdinBuffer(prev => prev.slice(0, stdinCursor - 1) + prev.slice(stdinCursor));
@@ -412,14 +353,8 @@ function Compass({rootPath, initialView = 'navigator'}) {
412
353
  }
413
354
  return;
414
355
  }
415
- if (key.leftArrow) {
416
- setStdinCursor(c => Math.max(0, c - 1));
417
- return;
418
- }
419
- if (key.rightArrow) {
420
- setStdinCursor(c => Math.min(stdinBuffer.length, c + 1));
421
- return;
422
- }
356
+ if (key.leftArrow) { setStdinCursor(c => Math.max(0, c - 1)); return; }
357
+ if (key.rightArrow) { setStdinCursor(c => Math.min(stdinBuffer.length, c + 1)); return; }
423
358
  if (input) {
424
359
  setStdinBuffer(prev => prev.slice(0, stdinCursor) + input + prev.slice(stdinCursor));
425
360
  setStdinCursor(c => c + input.length);
@@ -427,49 +362,29 @@ function Compass({rootPath, initialView = 'navigator'}) {
427
362
  return;
428
363
  }
429
364
 
430
- if (key.shift && key.upArrow) {
431
- scrollLogs(-1);
432
- return;
433
- }
434
- if (key.shift && key.downArrow) {
435
- scrollLogs(1);
436
- return;
437
- }
365
+ if (key.shift && key.upArrow) { scrollLogs(-1); return; }
366
+ if (key.shift && key.downArrow) { scrollLogs(1); return; }
438
367
 
439
- if (normalizedInput === '?') {
440
- setShowHelp((prev) => !prev);
441
- return;
442
- }
443
- if (shiftCombo('l') && lastCommandRef.current) {
444
- runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project);
445
- return;
446
- }
368
+ if (normalizedInput === '?') { setShowHelp((prev) => !prev); return; }
369
+ if (shiftCombo('l') && lastCommandRef.current) { runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project); return; }
447
370
 
448
- if (key.upArrow && !key.shift && projects.length > 0) {
449
- setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length);
450
- return;
451
- }
452
- if (key.downArrow && !key.shift && projects.length > 0) {
453
- setSelectedIndex((prev) => (prev + 1) % projects.length);
371
+ if (mainView === 'tasks') {
372
+ if (key.upArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) - 1 + tasks.length) % tasks.length]?.id); return; }
373
+ if (key.downArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) + 1) % tasks.length]?.id); return; }
374
+ if (key.return || shiftCombo('t')) { setMainView('navigator'); return; }
454
375
  return;
455
376
  }
377
+
378
+ if (key.upArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length); return; }
379
+ if (key.downArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev + 1) % projects.length); return; }
456
380
  if (key.return) {
457
- if (!selectedProject) {
458
- return;
459
- }
381
+ if (!selectedProject) return;
460
382
  setViewMode((prev) => (prev === 'detail' ? 'list' : 'detail'));
461
383
  return;
462
384
  }
463
- if (shiftCombo('q')) {
464
- exit();
465
- return;
466
- }
467
- if (shiftCombo('c') && viewMode === 'detail' && selectedProject) {
468
- setCustomMode(true);
469
- setCustomInput('');
470
- setCustomCursor(0);
471
- return;
472
- }
385
+ if (shiftCombo('q')) { exit(); return; }
386
+ if (shiftCombo('c') && viewMode === 'detail' && selectedProject) { setCustomMode(true); setCustomInput(''); setCustomCursor(0); return; }
387
+
473
388
  const actionKey = normalizedInput && ACTION_MAP[normalizedInput];
474
389
  if (actionKey) {
475
390
  const commandMeta = selectedProject?.commands?.[actionKey];
@@ -482,17 +397,31 @@ function Compass({rootPath, initialView = 'navigator'}) {
482
397
  });
483
398
 
484
399
  const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}`;
485
- const projectRows = [];
486
- if (loading) {
487
- projectRows.push(create(Text, {key: 'scanning', dimColor: true}, 'Scanning projectsโ€ฆ'));
488
- }
489
- if (error) {
490
- projectRows.push(create(Text, {key: 'error', color: 'red'}, `Unable to scan: ${error}`));
491
- }
492
- if (!loading && !error && projects.length === 0) {
493
- projectRows.push(create(Text, {key: 'empty', dimColor: true}, 'No recognizable project manifests found.'));
400
+
401
+ if (mainView === 'studio') return create(Studio);
402
+
403
+ if (mainView === 'tasks') {
404
+ return create(
405
+ Box,
406
+ {flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', padding: 1},
407
+ create(Text, {bold: true, color: 'yellow'}, '๐Ÿ›ฐ๏ธ Task Manager | Background Processes'),
408
+ create(Text, {dimColor: true, marginBottom: 1}, 'Select a task to view its output logs.'),
409
+ ...tasks.map(t => create(
410
+ Box,
411
+ {key: t.id, marginBottom: 0},
412
+ create(Text, {color: t.id === activeTaskId ? 'cyan' : 'white', bold: t.id === activeTaskId}, `${t.id === activeTaskId ? 'โ†’' : ' '} [${t.status.toUpperCase()}] ${t.name}`)
413
+ )),
414
+ !tasks.length && create(Text, {dimColor: true}, 'No active or background tasks.'),
415
+ create(Text, {marginTop: 1, dimColor: true}, 'Press Enter/Shift+T to return to Navigator, Up/Down to switch focus.')
416
+ );
494
417
  }
495
- if (!loading && mainView === 'navigator') {
418
+
419
+ const projectRows = [];
420
+ if (loading) projectRows.push(create(Text, {key: 'scanning', dimColor: true}, 'Scanning projectsโ€ฆ'));
421
+ if (error) projectRows.push(create(Text, {key: 'error', color: 'red'}, `Unable to scan: ${error}`));
422
+ if (!loading && !error && projects.length === 0) projectRows.push(create(Text, {key: 'empty', dimColor: true}, 'No recognizable project manifests found.'));
423
+
424
+ if (!loading) {
496
425
  projects.forEach((project, index) => {
497
426
  const isSelected = index === selectedIndex;
498
427
  const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
@@ -504,14 +433,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
504
433
  create(
505
434
  Box,
506
435
  {flexDirection: 'row'},
507
- create(
508
- Text,
509
- {
510
- color: isSelected ? 'cyan' : 'white',
511
- bold: isSelected
512
- },
513
- `${project.icon} ${project.name}`
514
- ),
436
+ create(Text, {color: isSelected ? 'cyan' : 'white', bold: isSelected}, `${project.icon} ${project.name}`),
515
437
  hasMissingRuntime && create(Text, {color: 'red', bold: true}, ' โš ๏ธ Runtime missing')
516
438
  ),
517
439
  create(Text, {dimColor: true}, ` ${project.type} ยท ${path.relative(rootPath, project.path) || '.'}`),
@@ -524,359 +446,88 @@ function Compass({rootPath, initialView = 'navigator'}) {
524
446
  const detailContent = [];
525
447
  if (viewMode === 'detail' && selectedProject) {
526
448
  detailContent.push(
527
- create(
528
- Box,
529
- {key: 'title-row', flexDirection: 'row'},
449
+ create(Box, {key: 'title-row', flexDirection: 'row'},
530
450
  create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
531
451
  selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0 && create(Text, {color: 'red', bold: true}, ' โš ๏ธ MISSING RUNTIME')
532
452
  ),
533
453
  create(Text, {key: 'manifest', dimColor: true}, `${selectedProject.type} ยท ${selectedProject.manifest || 'detected manifest'}`),
534
454
  create(Text, {key: 'loc', dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
535
455
  );
536
- if (selectedProject.description) {
537
- detailContent.push(create(Text, {key: 'desc'}, selectedProject.description));
538
- }
456
+ if (selectedProject.description) detailContent.push(create(Text, {key: 'desc'}, selectedProject.description));
539
457
  const frameworks = (selectedProject.frameworks || []).map((lib) => `${lib.icon} ${lib.name}`).join(', ');
540
- if (frameworks) {
541
- detailContent.push(create(Text, {key: 'frames', dimColor: true}, `Frameworks: ${frameworks}`));
542
- }
543
- if (selectedProject.extra?.scripts && selectedProject.extra.scripts.length) {
544
- detailContent.push(create(Text, {key: 'scripts', dimColor: true}, `Scripts: ${selectedProject.extra.scripts.join(', ')}`));
545
- }
546
-
458
+ if (frameworks) detailContent.push(create(Text, {key: 'frames', dimColor: true}, `Frameworks: ${frameworks}`));
459
+
547
460
  if (selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0) {
548
461
  detailContent.push(
549
- create(Text, {key: 'missing-title', color: 'red', bold: true, marginTop: 1}, 'MISSING BINARIES:'),
550
- create(Text, {key: 'missing-list', color: 'red'}, `Please install: ${selectedProject.missingBinaries.join(', ')}`),
551
- create(Text, {key: 'missing-hint', dimColor: true}, 'Project commands may fail until these are in your PATH.')
462
+ create(Text, {key: 'm-t', color: 'red', bold: true, marginTop: 1}, 'MISSING BINARIES:'),
463
+ create(Text, {key: 'm-l', color: 'red'}, `Please install: ${selectedProject.missingBinaries.join(', ')}`)
552
464
  );
553
465
  }
554
466
 
555
- detailContent.push(create(Text, {key: 'config-path', dimColor: true, marginTop: 1}, `Custom commands stored in ${CONFIG_PATH}`));
556
- detailContent.push(create(Text, {key: 'plugin-path', dimColor: true, marginBottom: 1}, `Extend frameworks via ${PLUGIN_FILE}`));
557
467
  detailContent.push(create(Text, {key: 'cmd-header', bold: true, marginTop: 1}, 'Commands'));
558
468
  detailedIndexed.forEach((command) => {
559
469
  detailContent.push(
560
- create(Text, {key: `detail-${command.shortcut}-${command.label}`}, `${command.shortcut}. ${command.label} ${command.source === 'custom' ? kleur.magenta('(custom)') : command.source === 'framework' ? kleur.cyan('(framework)') : command.source === 'plugin' ? kleur.green('(plugin)') : ''}`)
470
+ create(Text, {key: `d-${command.shortcut}`}, `${command.shortcut}. ${command.label} ${command.source === 'custom' ? kleur.magenta('(custom)') : command.source === 'framework' ? kleur.cyan('(framework)') : ''}`),
471
+ create(Text, {key: `dl-${command.shortcut}`, dimColor: true}, ` โ†ณ ${command.command.join(' ')}`)
561
472
  );
562
- detailContent.push(create(Text, {key: `detail-line-${command.shortcut}-${command.label}`, dimColor: true}, ` โ†ณ ${command.command.join(' ')}`));
563
473
  });
564
- if (!detailedIndexed.length) {
565
- detailContent.push(create(Text, {key: 'no-cmds', dimColor: true}, 'No built-in commands yet. Add a custom command with Shift+C.'));
566
- }
567
- const setupHints = selectedProject.extra?.setupHints || [];
568
- if (setupHints.length) {
569
- detailContent.push(create(Text, {key: 'setup-header', dimColor: true, marginTop: 1}, 'Setup hints:'));
570
- setupHints.forEach((hint, hidx) => detailContent.push(create(Text, {key: `hint-${hidx}`, dimColor: true}, ` โ€ข ${hint}`)));
571
- }
572
- detailContent.push(create(Text, {key: 'hint-line', dimColor: true}, 'Press Shift+C โ†’ label|cmd to save custom actions, Enter to close detail view.'));
474
+ detailContent.push(create(Text, {key: 'h-l', dimColor: true, marginTop: 1}, 'Press Shift+C โ†’ label|cmd to save custom actions, Enter to close detail view.'));
573
475
  } else {
574
- detailContent.push(create(Text, {key: 'enter-hint', dimColor: true}, 'Press Enter on a project to reveal details (icons, commands, frameworks, custom actions).'));
476
+ detailContent.push(create(Text, {key: 'e-h', dimColor: true}, 'Press Enter on a project to reveal details.'));
575
477
  }
576
478
 
577
479
  if (customMode) {
578
- detailContent.push(
579
- create(
580
- Box,
581
- {key: 'custom-input-box', flexDirection: 'row'},
582
- create(Text, {color: 'cyan'}, 'Type label|cmd (Enter: save, Esc: cancel): '),
583
- create(CursorText, {value: customInput, cursorIndex: customCursor})
584
- )
585
- );
480
+ detailContent.push(create(Box, {key: 'ci-box', flexDirection: 'row'}, create(Text, {color: 'cyan'}, 'Type label|cmd (Enter: save, Esc: cancel): '), create(CursorText, {value: customInput, cursorIndex: customCursor})));
586
481
  }
587
482
 
588
- const artTileNodes = useMemo(() => {
589
- const selectedName = selectedProject?.name || 'Awaiting selection';
590
- const selectedType = selectedProject?.type || 'Unknown stack';
591
- const selectedLocation = selectedProject?.path ? path.relative(rootPath, selectedProject.path) || '.' : 'โ€”';
592
- const statusNarrative = running ? 'Running commands' : lastAction ? `Last: ${lastAction}` : 'Idle gallery';
593
- const workspaceName = path.basename(rootPath) || rootPath;
594
- const tileDefinition = [
595
- {
596
- label: 'Pulse',
597
- detail: projectCountLabel,
598
- accent: 'magenta',
599
- icon: 'โ—',
600
- subtext: `Workspace ยท ${workspaceName}`
601
- },
602
- {
603
- label: 'Focus',
604
- detail: selectedName,
605
- accent: 'cyan',
606
- icon: 'โ—†',
607
- subtext: `${selectedType} ยท ${selectedLocation}`
608
- },
609
- {
610
- label: 'Rhythm',
611
- detail: `${detailCommands.length} commands`,
612
- accent: 'yellow',
613
- icon: 'โ– ',
614
- subtext: statusNarrative
615
- }
616
- ];
617
- return tileDefinition.map((tile) =>
618
- create(
619
- Box,
620
- {
621
- key: tile.label,
622
- flexDirection: 'column',
623
- padding: 1,
624
- marginRight: 1,
625
- borderStyle: 'single',
626
- borderColor: tile.accent,
627
- minWidth: 24
628
- },
629
- create(Text, {color: tile.accent, bold: true}, `${tile.icon} ${tile.label}`),
630
- create(Text, {bold: true}, tile.detail),
631
- create(Text, {dimColor: true}, tile.subtext)
632
- )
633
- );
634
- }, [projectCountLabel, rootPath, selectedProject, detailCommands.length, running, lastAction]);
635
-
636
- const artBoard = create(
637
- Box,
638
- {
639
- flexDirection: 'column',
640
- marginTop: 1,
641
- borderStyle: 'round',
642
- borderColor: 'gray',
643
- padding: 1
644
- },
645
- create(
646
- Box,
647
- {flexDirection: 'row', justifyContent: 'space-between'},
648
- create(Text, {color: 'magenta', bold: true}, 'Art-coded build atlas'),
649
- create(Text, {dimColor: true}, 'press ? for overlay help')
650
- ),
651
- create(
652
- Box,
653
- {flexDirection: 'row', marginTop: 1},
654
- ...ART_CHARS.map((char, index) =>
655
- create(Text, {key: `art-${index}`, color: ART_COLORS[index % ART_COLORS.length]}, char.repeat(2))
656
- )
657
- ),
658
- create(
659
- Box,
660
- {flexDirection: 'row', marginTop: 1},
661
- ...artTileNodes
662
- ),
663
- create(Text, {dimColor: true, marginTop: 1}, kleur.italic('The art board now follows your layout and stays inside the window.'))
483
+ const artTileNodes = [
484
+ {label: 'Pulse', detail: projectCountLabel, accent: 'magenta', icon: 'โ—', subtext: `Workspace ยท ${path.basename(rootPath) || rootPath}`},
485
+ {label: 'Focus', detail: selectedProject?.name || 'Selection', accent: 'cyan', icon: 'โ—†', subtext: `${selectedProject?.type || 'Stack'}`},
486
+ {label: 'Orbit', detail: `${tasks.length} active tasks`, accent: 'yellow', icon: 'โ– ', subtext: running ? 'Busy streaming...' : 'Idle'}
487
+ ].map(tile => create(Box, {key: tile.label, flexDirection: 'column', padding: 1, marginRight: 1, borderStyle: 'single', borderColor: tile.accent, minWidth: 24},
488
+ create(Text, {color: tile.accent, bold: true}, `${tile.icon} ${tile.label}`),
489
+ create(Text, {bold: true}, tile.detail),
490
+ create(Text, {dimColor: true}, tile.subtext)
491
+ ));
492
+
493
+ const artBoard = create(Box, {flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
494
+ create(Box, {flexDirection: 'row', justifyContent: 'space-between'}, create(Text, {color: 'magenta', bold: true}, 'Art-coded build atlas'), create(Text, {dimColor: true}, 'press ? for overlay help')),
495
+ create(Box, {flexDirection: 'row', marginTop: 1}, ...ART_CHARS.map((char, i) => create(Text, {key: i, color: ART_COLORS[i % ART_COLORS.length]}, char.repeat(2)))),
496
+ create(Box, {flexDirection: 'row', marginTop: 1}, ...artTileNodes)
664
497
  );
665
498
 
666
- const logWindowStart = Math.max(0, logLines.length - OUTPUT_WINDOW_SIZE - logOffset);
667
- const logWindowEnd = Math.max(0, logLines.length - logOffset);
668
- const visibleLogs = logLines.slice(logWindowStart, logWindowEnd);
669
- const logNodes = visibleLogs.length
670
- ? visibleLogs.map((line, index) => create(Text, {key: index}, line))
671
- : [create(Text, {key: 'no-logs', dimColor: true}, 'Logs will appear here once you run a command.')];
499
+ const logs = activeTask?.logs || [];
500
+ const logWindowStart = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE - logOffset);
501
+ const logWindowEnd = Math.max(0, logs.length - logOffset);
502
+ const visibleLogs = logs.slice(logWindowStart, logWindowEnd);
503
+ const logNodes = visibleLogs.length ? visibleLogs.map((line, i) => create(Text, {key: i}, line)) : [create(Text, {dimColor: true}, 'Select a task or run a command to see logs.')];
672
504
 
673
505
  const helpCards = [
674
- {
675
- label: 'Navigation',
676
- color: 'magenta',
677
- body: [
678
- 'โ†‘ / โ†“ move focus, Enter: details',
679
- 'Shift+โ†‘ / โ†“ scroll output',
680
- 'Shift+H toggle help cards',
681
- '? opens the overlay help'
682
- ]
683
- },
684
- {
685
- label: 'Command flow',
686
- color: 'cyan',
687
- body: [
688
- 'B / T / R build/test/run',
689
- '1-9 run detail commands',
690
- 'Shift+L rerun last command',
691
- 'Shift+X clear / Shift+E export'
692
- ]
693
- },
694
- {
695
- label: 'System & Studio',
696
- color: 'yellow',
697
- body: [
698
- 'Shift+A open Omni-Studio',
699
- 'Shift+S toggle structure guide',
700
- 'Shift+C save custom action',
701
- 'Shift+Q quit application'
702
- ]
703
- }
506
+ {label: 'Navigation', color: 'magenta', body: ['โ†‘ / โ†“ move focus, Enter: details', 'Shift+โ†‘ / โ†“ scroll output', 'Shift+H toggle help cards', 'Shift+D detach from task']},
507
+ {label: 'Commands', color: 'cyan', body: ['B / T / R build/test/run', '1-9 run detail commands', 'Shift+L rerun last command', 'Shift+X clear / Shift+E export']},
508
+ {label: 'Orbit & Studio', color: 'yellow', body: ['Shift+T task manager', 'Shift+A open Omni-Studio', 'Shift+C save custom action', 'Shift+Q quit application']}
704
509
  ];
705
- const helpSection = showHelpCards
706
- ? create(
707
- Box,
708
- {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between', flexWrap: 'wrap'},
709
- ...helpCards.map((card, index) =>
710
- create(
711
- Box,
712
- {
713
- key: card.label,
714
- flexGrow: 1,
715
- flexBasis: 0,
716
- minWidth: HELP_CARD_MIN_WIDTH,
717
- marginRight: index < helpCards.length - 1 ? 1 : 0,
718
- marginBottom: 1,
719
- borderStyle: 'round',
720
- borderColor: card.color,
721
- padding: 1,
722
- flexDirection: 'column'
723
- },
724
- create(Text, {color: card.color, bold: true, marginBottom: 1}, card.label),
725
- ...card.body.map((line, lineIndex) =>
726
- create(Text, {key: `${card.label}-${lineIndex}`, dimColor: card.color === 'yellow'}, line)
727
- )
728
- )
729
- )
730
- )
731
- : create(Text, {key: 'help-hint', dimColor: true, marginTop: 1}, 'Help cards hidden ยท press Shift+H to show navigation, command flow, and recent runs.');
732
-
733
- const structureGuide = showStructureGuide
734
- ? create(
735
- Box,
736
- {
737
- flexDirection: 'column',
738
- borderStyle: 'round',
739
- borderColor: 'blue',
740
- marginTop: 1,
741
- padding: 1
742
- },
743
- create(Text, {color: 'cyan', bold: true}, 'Project structure guide ยท press Shift+S to hide'),
744
- ...SCHEMA_GUIDE.map((entry) =>
745
- create(Text, {key: entry.type, dimColor: true}, `โ€ข ${entry.icon} ${entry.label}: ${entry.files.join(', ')}`)
746
- ),
747
- create(Text, {dimColor: true, marginTop: 1}, 'SCHEMAS describe the manifests that trigger each language type.')
748
- )
749
- : null;
750
-
751
- const helpOverlay = showHelp
752
- ? create(
753
- Box,
754
- {
755
- flexDirection: 'column',
756
- borderStyle: 'double',
757
- borderColor: 'cyan',
758
- marginTop: 1,
759
- padding: 1
760
- },
761
- create(Text, {color: 'cyan', bold: true}, 'Help overlay ยท press ? to hide'),
762
- create(Text, null, 'Shift+โ†‘/โ†“ scrolls logs; Shift+X clears; Shift+E exports to file; Shift+A Omni-Studio.'),
763
- create(Text, null, 'B/T/R run build/test/run; 1-9 detail commands; Shift+L reruns previous command.'),
764
- create(Text, null, 'Shift+H help cards, Shift+S structure guide, ? overlay, Shift+Q quits.'),
765
- create(Text, null, 'Projects + Details stay paired while Output keeps its own full-width band.'),
766
- create(Text, null, 'Structure guide lists the manifests that trigger each language detection.')
767
- )
768
- : null;
769
-
770
- if (mainView === 'studio') {
771
- return create(Studio);
772
- }
773
510
 
774
- const toggleHint = showHelpCards ? 'Shift+H hides the help cards' : 'Shift+H shows the help cards';
775
- const headerHint = viewMode === 'detail'
776
- ? `Detail mode ยท 1-${Math.max(detailedIndexed.length, 1)} to execute, Shift+C: add custom commands, Enter: back to list, Shift+Q: quit ยท ${toggleHint}, Shift+S toggles structure guide`
777
- : `Quick run ยท B/T/R to build/test/run, Enter: view details, Shift+Q: quit ยท ${toggleHint}, Shift+S toggles structure guide`;
778
-
779
- return create(
780
- Box,
781
- {flexDirection: 'column', padding: 1},
782
- create(
783
- Box,
784
- {justifyContent: 'space-between'},
785
- create(
786
- Box,
787
- {flexDirection: 'column'},
788
- create(Text, {color: 'magenta', bold: true}, 'Project Compass'),
789
- create(Text, {dimColor: true}, loading ? 'Scanning workspacesโ€ฆ' : `${projectCountLabel} detected in ${rootPath}`),
790
- create(Text, {dimColor: true}, 'Use keyboard arrows to navigate, ? for help.')
791
- ),
792
- create(
793
- Box,
794
- {flexDirection: 'column', alignItems: 'flex-end'},
795
- create(Text, {color: running ? 'yellow' : 'green'}, running ? 'Busy streaming...' : lastAction ? `Last action ยท ${lastAction}` : 'Idle'),
796
- create(Text, {dimColor: true}, headerHint)
797
- )
511
+ const toggleHint = showHelpCards ? 'Shift+H hide help' : 'Shift+H show help';
512
+ return create(Box, {flexDirection: 'column', padding: 1},
513
+ create(Box, {justifyContent: 'space-between'},
514
+ create(Box, {flexDirection: 'column'}, create(Text, {color: 'magenta', bold: true}, 'Project Compass'), create(Text, {dimColor: true}, `${projectCountLabel} detected in ${rootPath}`)),
515
+ create(Box, {flexDirection: 'column', alignItems: 'flex-end'}, create(Text, {color: running ? 'yellow' : 'green'}, activeTask ? `[${activeTask.status.toUpperCase()}] ${activeTask.name}` : 'Idle Navigator'), create(Text, {dimColor: true}, `${toggleHint}, Shift+T: Tasks, Shift+Q: Quit`))
798
516
  ),
799
517
  artBoard,
800
- create(
801
- Box,
802
- {marginTop: 1, flexDirection: 'row', alignItems: 'stretch', width: '100%', flexWrap: 'wrap'},
803
- create(
804
- Box,
805
- {
806
- flexGrow: 1,
807
- flexBasis: 0,
808
- flexShrink: 1,
809
- minWidth: PROJECTS_MIN_WIDTH,
810
- marginRight: 1,
811
- borderStyle: 'round',
812
- borderColor: 'magenta',
813
- padding: 1
814
- },
815
- create(Text, {bold: true, color: 'magenta'}, 'Projects'),
816
- create(Box, {flexDirection: 'column', marginTop: 1}, ...projectRows)
817
- ),
818
- create(
819
- Box,
820
- {
821
- flexGrow: 1.3,
822
- flexBasis: 0,
823
- flexShrink: 1,
824
- minWidth: DETAILS_MIN_WIDTH,
825
- borderStyle: 'round',
826
- borderColor: 'cyan',
827
- padding: 1,
828
- flexDirection: 'column'
829
- },
830
- create(Text, {bold: true, color: 'cyan'}, 'Details'),
831
- ...detailContent
832
- )
518
+ create(Box, {marginTop: 1, flexDirection: 'row', alignItems: 'stretch', width: '100%', flexWrap: 'wrap'},
519
+ create(Box, {flexGrow: 1, flexBasis: 0, minWidth: PROJECTS_MIN_WIDTH, marginRight: 1, borderStyle: 'round', borderColor: 'magenta', padding: 1}, create(Text, {bold: true, color: 'magenta'}, 'Projects'), create(Box, {flexDirection: 'column', marginTop: 1}, ...projectRows)),
520
+ create(Box, {flexGrow: 1.3, flexBasis: 0, minWidth: DETAILS_MIN_WIDTH, borderStyle: 'round', borderColor: 'cyan', padding: 1, flexDirection: 'column'}, create(Text, {bold: true, color: 'cyan'}, 'Details'), ...detailContent)
833
521
  ),
834
- create(
835
- Box,
836
- {marginTop: 1, flexDirection: 'column'},
837
- create(
838
- Box,
839
- {flexDirection: 'row', justifyContent: 'space-between'},
840
- create(Text, {bold: true, color: 'yellow'}, `Output ${running ? 'ยท Running' : ''}`),
841
- create(Text, {dimColor: true}, logOffset ? `Scrolled ${logOffset} lines` : 'Live log view')
842
- ),
843
- create(
844
- Box,
845
- {
846
- flexDirection: 'column',
847
- borderStyle: 'round',
848
- borderColor: 'yellow',
849
- padding: 1,
850
- minHeight: OUTPUT_WINDOW_HEIGHT,
851
- maxHeight: OUTPUT_WINDOW_HEIGHT,
852
- height: OUTPUT_WINDOW_HEIGHT,
853
- overflow: 'hidden',
854
- flexShrink: 0
855
- },
856
- ...logNodes
857
- ),
858
- create(
859
- Box,
860
- {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between'},
861
- create(Text, {dimColor: true}, running ? 'Type to feed stdin; Enter submits, Ctrl+C aborts.' : 'Run a command or press ? for extra help.'),
862
- create(Text, {dimColor: true}, `${toggleHint}, Shift+S toggles the structure guide`)
863
- ),
864
- create(
865
- Box,
866
- {
867
- marginTop: 1,
868
- flexDirection: 'row',
869
- borderStyle: 'round',
870
- borderColor: running ? 'green' : 'gray',
871
- paddingX: 1
872
- },
873
- create(Text, {bold: true, color: running ? 'green' : 'white'}, running ? ' Stdin buffer ' : ' Input ready '),
874
- create(Box, {marginLeft: 1}, create(CursorText, {value: stdinBuffer || (running ? '' : 'Start a command to feed stdin'), cursorIndex: stdinCursor, active: running}))
875
- )
522
+ create(Box, {marginTop: 1, flexDirection: 'column'},
523
+ create(Box, {flexDirection: 'row', justifyContent: 'space-between'}, create(Text, {bold: true, color: 'yellow'}, `Output: ${activeTask?.name || 'None'}`), create(Text, {dimColor: true}, logOffset ? `Scrolled ${logOffset} lines` : 'Live log view')),
524
+ create(Box, {flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', padding: 1, minHeight: OUTPUT_WINDOW_HEIGHT, maxHeight: OUTPUT_WINDOW_HEIGHT, height: OUTPUT_WINDOW_HEIGHT, overflow: 'hidden'}, ...logNodes),
525
+ create(Box, {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between'}, create(Text, {dimColor: true}, running ? 'Type to feed stdin; Enter: submit, Ctrl+C: abort.' : 'Run a command or press Shift+T to switch tasks.'), create(Text, {dimColor: true}, `${toggleHint}, Shift+S: Structure Guide`)),
526
+ create(Box, {marginTop: 1, flexDirection: 'row', borderStyle: 'round', borderColor: running ? 'green' : 'gray', paddingX: 1}, create(Text, {bold: true, color: running ? 'green' : 'white'}, running ? ' Stdin buffer ' : ' Input ready '), create(Box, {marginLeft: 1}, create(CursorText, {value: stdinBuffer || (running ? '' : 'Start a command to feed stdin'), cursorIndex: stdinCursor, active: running})))
876
527
  ),
877
- helpSection,
878
- structureGuide,
879
- helpOverlay
528
+ showHelpCards && create(Box, {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between', flexWrap: 'wrap'}, ...helpCards.map((card, idx) => create(Box, {key: card.label, flexGrow: 1, flexBasis: 0, minWidth: HELP_CARD_MIN_WIDTH, marginRight: idx < 2 ? 1 : 0, marginBottom: 1, borderStyle: 'round', borderColor: card.color, padding: 1, flexDirection: 'column'}, create(Text, {color: card.color, bold: true, marginBottom: 1}, card.label), ...card.body.map((line, lidx) => create(Text, {key: lidx, dimColor: card.color === 'yellow'}, line))))),
529
+ showStructureGuide && create(Box, {flexDirection: 'column', borderStyle: 'round', borderColor: 'blue', marginTop: 1, padding: 1}, create(Text, {color: 'cyan', bold: true}, 'Structure guide ยท press Shift+S to hide'), ...SCHEMA_GUIDE.map(e => create(Text, {key: e.type, dimColor: true}, `โ€ข ${e.icon} ${e.label}: ${e.files.join(', ')}`))),
530
+ showHelp && create(Box, {flexDirection: 'column', borderStyle: 'double', borderColor: 'cyan', marginTop: 1, padding: 1}, create(Text, {color: 'cyan', bold: true}, 'Help overlay'), create(Text, null, 'Shift+โ†‘/โ†“ scrolls logs; Shift+X clears; Shift+E exports; Shift+A Studio; Shift+T Tasks; Shift+D Detach.'))
880
531
  );
881
532
  }
882
533
 
@@ -886,17 +537,10 @@ function parseArgs() {
886
537
  const tokens = process.argv.slice(2);
887
538
  for (let i = 0; i < tokens.length; i += 1) {
888
539
  const token = tokens[i];
889
- if ((token === '--dir' || token === '--path') && tokens[i + 1]) {
890
- args.root = tokens[i + 1];
891
- i += 1;
892
- } else if (token === '--mode' && tokens[i + 1]) {
893
- args.mode = tokens[i + 1];
894
- i += 1;
895
- } else if (token === '--help' || token === '-h') {
896
- args.help = true;
897
- } else if (token === '--studio') {
898
- args.view = 'studio';
899
- }
540
+ if ((token === '--dir' || token === '--path') && tokens[i + 1]) { args.root = tokens[i + 1]; i += 1; }
541
+ else if (token === '--mode' && tokens[i + 1]) { args.mode = tokens[i + 1]; i += 1; }
542
+ else if (token === '--help' || token === '-h') args.help = true;
543
+ else if (token === '--studio') args.view = 'studio';
900
544
  }
901
545
  return args;
902
546
  }
@@ -918,9 +562,11 @@ async function main() {
918
562
  console.log(' โ†‘ / โ†“ Move project focus');
919
563
  console.log(' Enter Toggle detail view for selected project');
920
564
  console.log(' Shift+A Switch to Omni-Studio (Environment Health)');
921
- console.log(' Shift+X Clear the output log buffer');
565
+ console.log(' Shift+T Open Orbit Task Manager (Manage background processes)');
566
+ console.log(' Shift+D Detach from active task (Keep it running in background)');
567
+ console.log(' Shift+X Clear active task output log');
922
568
  console.log(' Shift+E Export current logs to a .txt file');
923
- console.log(' Shift+โ†‘ / โ†“ Scroll the output logs back/forward');
569
+ console.log(' Shift+โ†‘ / โ†“ Scroll the output logs');
924
570
  console.log(' Shift+Q Quit application');
925
571
  console.log('');
926
572
  console.log(kleur.bold('Execution shortcuts:'));
@@ -936,16 +582,11 @@ async function main() {
936
582
  if (args.mode === 'test') {
937
583
  const projects = await discoverProjects(rootPath);
938
584
  console.log(`Detected ${projects.length} project(s) under ${rootPath}`);
939
- projects.forEach((project) => {
940
- console.log(` โ€ข [${project.type}] ${project.name} (${project.path})`);
941
- });
585
+ projects.forEach((project) => { console.log(` โ€ข [${project.type}] ${project.name} (${project.path})`); });
942
586
  return;
943
587
  }
944
588
 
945
589
  render(create(Compass, {rootPath, initialView: args.view || 'navigator'}));
946
590
  }
947
591
 
948
- main().catch((error) => {
949
- console.error(error);
950
- process.exit(1);
951
- });
592
+ main().catch((error) => { console.error(error); process.exit(1); });