project-compass 2.7.1 โ†’ 2.8.0

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 -519
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.0",
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
@@ -172,9 +172,9 @@ function Compass({rootPath, initialView = 'navigator'}) {
172
172
  const [selectedIndex, setSelectedIndex] = useState(0);
173
173
  const [viewMode, setViewMode] = useState('list');
174
174
  const [mainView, setMainView] = useState(initialView);
175
- const [logLines, setLogLines] = useState([]);
175
+ const [tasks, setTasks] = useState([]);
176
+ const [activeTaskId, setActiveTaskId] = useState(null);
176
177
  const [logOffset, setLogOffset] = useState(0);
177
- const [running, setRunning] = useState(false);
178
178
  const [lastAction, setLastAction] = useState(null);
179
179
  const [customMode, setCustomMode] = useState(false);
180
180
  const [customInput, setCustomInput] = useState('');
@@ -187,16 +187,20 @@ function Compass({rootPath, initialView = 'navigator'}) {
187
187
  const [showHelp, setShowHelp] = useState(false);
188
188
  const [recentRuns, setRecentRuns] = useState([]);
189
189
  const selectedProject = projects[selectedIndex] || null;
190
- const runningProcessRef = useRef(null);
190
+ const runningProcessMap = useRef(new Map());
191
191
  const lastCommandRef = useRef(null);
192
192
 
193
- const addLog = useCallback((line) => {
194
- setLogLines((prev) => {
193
+ const activeTask = useMemo(() => tasks.find(t => t.id === activeTaskId), [tasks, activeTaskId]);
194
+ const running = activeTask?.status === 'running';
195
+
196
+ const addLogToTask = useCallback((taskId, line) => {
197
+ setTasks(prev => prev.map(t => {
198
+ if (t.id !== taskId) return t;
195
199
  const normalized = typeof line === 'string' ? line : JSON.stringify(line);
196
200
  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
- });
201
+ const nextLogs = [...t.logs, ...lines];
202
+ return { ...t, logs: nextLogs.length > 500 ? nextLogs.slice(-500) : nextLogs };
203
+ }));
200
204
  }, []);
201
205
 
202
206
  const detailCommands = useMemo(() => buildDetailCommands(selectedProject, config), [selectedProject, config]);
@@ -212,85 +216,71 @@ function Compass({rootPath, initialView = 'navigator'}) {
212
216
 
213
217
  const runProjectCommand = useCallback(async (commandMeta, targetProject = selectedProject) => {
214
218
  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
- }
219
+ if (!project) return;
220
+ if (!commandMeta || !Array.isArray(commandMeta.command) || commandMeta.command.length === 0) return;
226
221
 
227
222
  const commandLabel = commandMeta.label || commandMeta.command.join(' ');
223
+ const taskId = `task-${Date.now()}`;
224
+ const newTask = {
225
+ id: taskId,
226
+ name: `${project.name} ยท ${commandLabel}`,
227
+ status: 'running',
228
+ logs: [kleur.cyan(`> ${commandMeta.command.join(' ')}`)],
229
+ project: project.name
230
+ };
231
+
232
+ setTasks(prev => [...prev, newTask]);
233
+ setActiveTaskId(taskId);
228
234
  lastCommandRef.current = {project, commandMeta};
229
- setRunning(true);
230
- setLastAction(`${project.name} ยท ${commandLabel}`);
231
- const fullCmd = commandMeta.command;
232
- addLog(kleur.cyan(`> ${fullCmd.join(' ')}`));
235
+ setLastAction(newTask.name);
236
+
233
237
  setRecentRuns((prev) => {
234
238
  const entry = {project: project.name, command: commandLabel, time: new Date().toLocaleTimeString()};
235
239
  return [entry, ...prev].slice(0, RECENT_RUN_LIMIT);
236
240
  });
237
241
 
238
242
  try {
239
- const subprocess = execa(fullCmd[0], fullCmd.slice(1), {
243
+ const subprocess = execa(commandMeta.command[0], commandMeta.command.slice(1), {
240
244
  cwd: project.path,
241
245
  env: process.env,
242
246
  stdin: 'pipe'
243
247
  });
244
- runningProcessRef.current = subprocess;
248
+ runningProcessMap.current.set(taskId, subprocess);
245
249
 
246
- subprocess.stdout?.on('data', (chunk) => {
247
- addLog(chunk.toString());
248
- });
249
- subprocess.stderr?.on('data', (chunk) => {
250
- addLog(kleur.red(chunk.toString()));
251
- });
250
+ subprocess.stdout?.on('data', (chunk) => addLogToTask(taskId, chunk.toString()));
251
+ subprocess.stderr?.on('data', (chunk) => addLogToTask(taskId, kleur.red(chunk.toString())));
252
252
 
253
253
  await subprocess;
254
- addLog(kleur.green(`โœ“ ${commandLabel} finished`));
254
+ setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'finished'} : t));
255
+ addLogToTask(taskId, kleur.green(`โœ“ ${commandLabel} finished`));
255
256
  } catch (error) {
256
- addLog(kleur.red(`โœ— ${commandLabel} failed: ${error.shortMessage || error.message}`));
257
+ setTasks(prev => prev.map(t => t.id === taskId ? {...t, status: 'failed'} : t));
258
+ addLogToTask(taskId, kleur.red(`โœ— ${commandLabel} failed: ${error.shortMessage || error.message}`));
257
259
  } finally {
258
- setRunning(false);
259
- setStdinBuffer('');
260
- setStdinCursor(0);
261
- runningProcessRef.current = null;
260
+ runningProcessMap.current.delete(taskId);
262
261
  }
263
- }, [addLog, running, selectedProject]);
262
+ }, [addLogToTask, selectedProject]);
264
263
 
265
264
  const handleAddCustomCommand = useCallback((label, commandTokens) => {
266
- if (!selectedProject) {
267
- return;
268
- }
265
+ if (!selectedProject) return;
269
266
  setConfig((prev) => {
270
267
  const projectKey = selectedProject.path;
271
268
  const existing = prev.customCommands?.[projectKey] || [];
272
- const nextCustom = [...existing, {label, command: commandTokens}];
273
269
  const nextConfig = {
274
270
  ...prev,
275
271
  customCommands: {
276
272
  ...prev.customCommands,
277
- [projectKey]: nextCustom
273
+ [projectKey]: [...existing, {label, command: commandTokens}]
278
274
  }
279
275
  };
280
276
  saveConfig(nextConfig);
281
277
  return nextConfig;
282
278
  });
283
- addLog(kleur.yellow(`Saved custom command "${label}" for ${selectedProject.name}`));
284
- }, [selectedProject, addLog]);
279
+ }, [selectedProject]);
285
280
 
286
281
  const handleCustomSubmit = useCallback(() => {
287
282
  const raw = customInput.trim();
288
- if (!selectedProject) {
289
- setCustomMode(false);
290
- return;
291
- }
292
- if (!raw) {
293
- addLog(kleur.gray('Canceled custom command (empty).'));
283
+ if (!selectedProject || !raw) {
294
284
  setCustomMode(false);
295
285
  setCustomInput('');
296
286
  setCustomCursor(0);
@@ -299,44 +289,33 @@ function Compass({rootPath, initialView = 'navigator'}) {
299
289
  const [labelPart, commandPart] = raw.split('|');
300
290
  const commandTokens = (commandPart || labelPart).trim().split(/\s+/).filter(Boolean);
301
291
  if (!commandTokens.length) {
302
- addLog(kleur.red('Custom command needs at least one token.'));
303
292
  setCustomMode(false);
304
293
  setCustomInput('');
305
294
  setCustomCursor(0);
306
295
  return;
307
296
  }
308
297
  const label = commandPart ? labelPart.trim() : `Custom ${selectedProject.name}`;
309
- handleAddCustomCommand(label || 'Custom', commandTokens);
298
+ handleAddCustomCommand(label, commandTokens);
310
299
  setCustomMode(false);
311
300
  setCustomInput('');
312
301
  setCustomCursor(0);
313
- }, [customInput, selectedProject, handleAddCustomCommand, addLog]);
302
+ }, [customInput, selectedProject, handleAddCustomCommand]);
314
303
 
315
304
  const exportLogs = useCallback(() => {
316
- if (!logLines.length) {
317
- return;
318
- }
305
+ if (!activeTask || !activeTask.logs.length) return;
319
306
  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}`));
307
+ const exportPath = path.resolve(process.cwd(), `compass-${activeTask.id}.txt`);
308
+ fs.writeFileSync(exportPath, activeTask.logs.join('\n'));
309
+ addLogToTask(activeTaskId, kleur.green(`โœ“ Logs exported to ${exportPath}`));
323
310
  } catch (err) {
324
- addLog(kleur.red(`โœ— Export failed: ${err.message}`));
311
+ addLogToTask(activeTaskId, kleur.red(`โœ— Export failed: ${err.message}`));
325
312
  }
326
- }, [logLines, addLog]);
313
+ }, [activeTask, activeTaskId, addLogToTask]);
327
314
 
328
315
  useInput((input, key) => {
329
316
  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
- }
317
+ if (key.return) { handleCustomSubmit(); return; }
318
+ if (key.escape) { setCustomMode(false); setCustomInput(''); setCustomCursor(0); return; }
340
319
  if (key.backspace || key.delete) {
341
320
  if (customCursor > 0) {
342
321
  setCustomInput((prev) => prev.slice(0, customCursor - 1) + prev.slice(customCursor));
@@ -344,14 +323,8 @@ function Compass({rootPath, initialView = 'navigator'}) {
344
323
  }
345
324
  return;
346
325
  }
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
- }
326
+ if (key.leftArrow) { setCustomCursor(c => Math.max(0, c - 1)); return; }
327
+ if (key.rightArrow) { setCustomCursor(c => Math.min(customInput.length, c + 1)); return; }
355
328
  if (input) {
356
329
  setCustomInput((prev) => prev.slice(0, customCursor) + input + prev.slice(customCursor));
357
330
  setCustomCursor(c => c + input.length);
@@ -360,51 +333,28 @@ function Compass({rootPath, initialView = 'navigator'}) {
360
333
  }
361
334
 
362
335
  const normalizedInput = input?.toLowerCase();
363
- const ctrlCombo = (char) => key.ctrl && normalizedInput === char;
364
336
  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
- }
337
+
338
+ if (shiftCombo('h')) { setShowHelpCards((prev) => !prev); return; }
339
+ if (shiftCombo('s')) { setShowStructureGuide((prev) => !prev); return; }
340
+ if (shiftCombo('a')) { setMainView((prev) => (prev === 'navigator' ? 'studio' : 'navigator')); return; }
341
+ if (shiftCombo('x')) { setTasks(prev => prev.map(t => t.id === activeTaskId ? {...t, logs: []} : t)); setLogOffset(0); return; }
342
+ if (shiftCombo('e')) { exportLogs(); return; }
343
+ if (shiftCombo('d')) { setActiveTaskId(null); return; }
344
+ if (shiftCombo('t')) { setMainView('tasks'); return; }
387
345
 
388
346
  const scrollLogs = (delta) => {
389
347
  setLogOffset((prev) => {
390
- const maxScroll = Math.max(0, logLines.length - OUTPUT_WINDOW_SIZE);
348
+ const logs = activeTask?.logs || [];
349
+ const maxScroll = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE);
391
350
  return Math.max(0, Math.min(maxScroll, prev + delta));
392
351
  });
393
352
  };
394
353
 
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
- }
354
+ if (running && activeTaskId && runningProcessMap.current.has(activeTaskId)) {
355
+ const proc = runningProcessMap.current.get(activeTaskId);
356
+ if (key.ctrl && input === 'c') { proc.kill('SIGINT'); setStdinBuffer(''); setStdinCursor(0); return; }
357
+ if (key.return) { proc.stdin?.write(stdinBuffer + '\n'); setStdinBuffer(''); setStdinCursor(0); return; }
408
358
  if (key.backspace || key.delete) {
409
359
  if (stdinCursor > 0) {
410
360
  setStdinBuffer(prev => prev.slice(0, stdinCursor - 1) + prev.slice(stdinCursor));
@@ -412,14 +362,8 @@ function Compass({rootPath, initialView = 'navigator'}) {
412
362
  }
413
363
  return;
414
364
  }
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
- }
365
+ if (key.leftArrow) { setStdinCursor(c => Math.max(0, c - 1)); return; }
366
+ if (key.rightArrow) { setStdinCursor(c => Math.min(stdinBuffer.length, c + 1)); return; }
423
367
  if (input) {
424
368
  setStdinBuffer(prev => prev.slice(0, stdinCursor) + input + prev.slice(stdinCursor));
425
369
  setStdinCursor(c => c + input.length);
@@ -427,49 +371,29 @@ function Compass({rootPath, initialView = 'navigator'}) {
427
371
  return;
428
372
  }
429
373
 
430
- if (key.shift && key.upArrow) {
431
- scrollLogs(-1);
432
- return;
433
- }
434
- if (key.shift && key.downArrow) {
435
- scrollLogs(1);
436
- return;
437
- }
374
+ if (key.shift && key.upArrow) { scrollLogs(-1); return; }
375
+ if (key.shift && key.downArrow) { scrollLogs(1); return; }
438
376
 
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
- }
377
+ if (normalizedInput === '?') { setShowHelp((prev) => !prev); return; }
378
+ if (shiftCombo('l') && lastCommandRef.current) { runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project); return; }
447
379
 
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);
380
+ if (mainView === 'tasks') {
381
+ if (key.upArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) - 1 + tasks.length) % tasks.length]?.id); return; }
382
+ if (key.downArrow) { setActiveTaskId(prev => tasks[(tasks.findIndex(t => t.id === prev) + 1) % tasks.length]?.id); return; }
383
+ if (key.return || shiftCombo('t')) { setMainView('navigator'); return; }
454
384
  return;
455
385
  }
386
+
387
+ if (key.upArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length); return; }
388
+ if (key.downArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev + 1) % projects.length); return; }
456
389
  if (key.return) {
457
- if (!selectedProject) {
458
- return;
459
- }
390
+ if (!selectedProject) return;
460
391
  setViewMode((prev) => (prev === 'detail' ? 'list' : 'detail'));
461
392
  return;
462
393
  }
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
- }
394
+ if (shiftCombo('q')) { exit(); return; }
395
+ if (shiftCombo('c') && viewMode === 'detail' && selectedProject) { setCustomMode(true); setCustomInput(''); setCustomCursor(0); return; }
396
+
473
397
  const actionKey = normalizedInput && ACTION_MAP[normalizedInput];
474
398
  if (actionKey) {
475
399
  const commandMeta = selectedProject?.commands?.[actionKey];
@@ -481,18 +405,30 @@ function Compass({rootPath, initialView = 'navigator'}) {
481
405
  }
482
406
  });
483
407
 
484
- 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.'));
408
+ if (mainView === 'studio') return create(Studio);
409
+
410
+ if (mainView === 'tasks') {
411
+ return create(
412
+ Box,
413
+ {flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', padding: 1},
414
+ create(Text, {bold: true, color: 'yellow'}, '๐Ÿ›ฐ๏ธ Task Manager | Background Processes'),
415
+ create(Text, {dimColor: true, marginBottom: 1}, 'Select a task to view its output logs.'),
416
+ ...tasks.map(t => create(
417
+ Box,
418
+ {key: t.id, marginBottom: 0},
419
+ create(Text, {color: t.id === activeTaskId ? 'cyan' : 'white', bold: t.id === activeTaskId}, `${t.id === activeTaskId ? 'โ†’' : ' '} [${t.status.toUpperCase()}] ${t.name}`)
420
+ )),
421
+ !tasks.length && create(Text, {dimColor: true}, 'No active or background tasks.'),
422
+ create(Text, {marginTop: 1, dimColor: true}, 'Press Enter/Shift+T to return to Navigator, Up/Down to switch focus.')
423
+ );
494
424
  }
495
- if (!loading && mainView === 'navigator') {
425
+
426
+ const projectRows = [];
427
+ if (loading) projectRows.push(create(Text, {key: 'scanning', dimColor: true}, 'Scanning projectsโ€ฆ'));
428
+ if (error) projectRows.push(create(Text, {key: 'error', color: 'red'}, `Unable to scan: ${error}`));
429
+ if (!loading && !error && projects.length === 0) projectRows.push(create(Text, {key: 'empty', dimColor: true}, 'No recognizable project manifests found.'));
430
+
431
+ if (!loading) {
496
432
  projects.forEach((project, index) => {
497
433
  const isSelected = index === selectedIndex;
498
434
  const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
@@ -504,14 +440,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
504
440
  create(
505
441
  Box,
506
442
  {flexDirection: 'row'},
507
- create(
508
- Text,
509
- {
510
- color: isSelected ? 'cyan' : 'white',
511
- bold: isSelected
512
- },
513
- `${project.icon} ${project.name}`
514
- ),
443
+ create(Text, {color: isSelected ? 'cyan' : 'white', bold: isSelected}, `${project.icon} ${project.name}`),
515
444
  hasMissingRuntime && create(Text, {color: 'red', bold: true}, ' โš ๏ธ Runtime missing')
516
445
  ),
517
446
  create(Text, {dimColor: true}, ` ${project.type} ยท ${path.relative(rootPath, project.path) || '.'}`),
@@ -524,359 +453,88 @@ function Compass({rootPath, initialView = 'navigator'}) {
524
453
  const detailContent = [];
525
454
  if (viewMode === 'detail' && selectedProject) {
526
455
  detailContent.push(
527
- create(
528
- Box,
529
- {key: 'title-row', flexDirection: 'row'},
456
+ create(Box, {key: 'title-row', flexDirection: 'row'},
530
457
  create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
531
458
  selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0 && create(Text, {color: 'red', bold: true}, ' โš ๏ธ MISSING RUNTIME')
532
459
  ),
533
460
  create(Text, {key: 'manifest', dimColor: true}, `${selectedProject.type} ยท ${selectedProject.manifest || 'detected manifest'}`),
534
461
  create(Text, {key: 'loc', dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
535
462
  );
536
- if (selectedProject.description) {
537
- detailContent.push(create(Text, {key: 'desc'}, selectedProject.description));
538
- }
463
+ if (selectedProject.description) detailContent.push(create(Text, {key: 'desc'}, selectedProject.description));
539
464
  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
-
465
+ if (frameworks) detailContent.push(create(Text, {key: 'frames', dimColor: true}, `Frameworks: ${frameworks}`));
466
+
547
467
  if (selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0) {
548
468
  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.')
469
+ create(Text, {key: 'm-t', color: 'red', bold: true, marginTop: 1}, 'MISSING BINARIES:'),
470
+ create(Text, {key: 'm-l', color: 'red'}, `Please install: ${selectedProject.missingBinaries.join(', ')}`)
552
471
  );
553
472
  }
554
473
 
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
474
  detailContent.push(create(Text, {key: 'cmd-header', bold: true, marginTop: 1}, 'Commands'));
558
475
  detailedIndexed.forEach((command) => {
559
476
  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)') : ''}`)
477
+ create(Text, {key: `d-${command.shortcut}`}, `${command.shortcut}. ${command.label} ${command.source === 'custom' ? kleur.magenta('(custom)') : command.source === 'framework' ? kleur.cyan('(framework)') : ''}`),
478
+ create(Text, {key: `dl-${command.shortcut}`, dimColor: true}, ` โ†ณ ${command.command.join(' ')}`)
561
479
  );
562
- detailContent.push(create(Text, {key: `detail-line-${command.shortcut}-${command.label}`, dimColor: true}, ` โ†ณ ${command.command.join(' ')}`));
563
480
  });
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.'));
481
+ 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
482
  } else {
574
- detailContent.push(create(Text, {key: 'enter-hint', dimColor: true}, 'Press Enter on a project to reveal details (icons, commands, frameworks, custom actions).'));
483
+ detailContent.push(create(Text, {key: 'e-h', dimColor: true}, 'Press Enter on a project to reveal details.'));
575
484
  }
576
485
 
577
486
  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
- );
487
+ 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
488
  }
587
489
 
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.'))
490
+ const artTileNodes = [
491
+ {label: 'Pulse', detail: projectCountLabel, accent: 'magenta', icon: 'โ—', subtext: `Workspace ยท ${path.basename(rootPath) || rootPath}`},
492
+ {label: 'Focus', detail: selectedProject?.name || 'Selection', accent: 'cyan', icon: 'โ—†', subtext: `${selectedProject?.type || 'Stack'}`},
493
+ {label: 'Orbit', detail: `${tasks.length} active tasks`, accent: 'yellow', icon: 'โ– ', subtext: running ? 'Busy streaming...' : 'Idle'}
494
+ ].map(tile => create(Box, {key: tile.label, flexDirection: 'column', padding: 1, marginRight: 1, borderStyle: 'single', borderColor: tile.accent, minWidth: 24},
495
+ create(Text, {color: tile.accent, bold: true}, `${tile.icon} ${tile.label}`),
496
+ create(Text, {bold: true}, tile.detail),
497
+ create(Text, {dimColor: true}, tile.subtext)
498
+ ));
499
+
500
+ const artBoard = create(Box, {flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
501
+ 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')),
502
+ 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)))),
503
+ create(Box, {flexDirection: 'row', marginTop: 1}, ...artTileNodes)
664
504
  );
665
505
 
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.')];
506
+ const logs = activeTask?.logs || [];
507
+ const logWindowStart = Math.max(0, logs.length - OUTPUT_WINDOW_SIZE - logOffset);
508
+ const logWindowEnd = Math.max(0, logs.length - logOffset);
509
+ const visibleLogs = logs.slice(logWindowStart, logWindowEnd);
510
+ 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
511
 
673
512
  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
- }
513
+ {label: 'Navigation', color: 'magenta', body: ['โ†‘ / โ†“ move focus, Enter: details', 'Shift+โ†‘ / โ†“ scroll output', 'Shift+H toggle help cards', 'Shift+D detach from task']},
514
+ {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']},
515
+ {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
516
  ];
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
517
 
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
- )
518
+ const toggleHint = showHelpCards ? 'Shift+H hide help' : 'Shift+H show help';
519
+ return create(Box, {flexDirection: 'column', padding: 1},
520
+ create(Box, {justifyContent: 'space-between'},
521
+ create(Box, {flexDirection: 'column'}, create(Text, {color: 'magenta', bold: true}, 'Project Compass'), create(Text, {dimColor: true}, `${projectCountLabel} detected in ${rootPath}`)),
522
+ 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
523
  ),
799
524
  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
- )
525
+ create(Box, {marginTop: 1, flexDirection: 'row', alignItems: 'stretch', width: '100%', flexWrap: 'wrap'},
526
+ 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)),
527
+ 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
528
  ),
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
- )
529
+ create(Box, {marginTop: 1, flexDirection: 'column'},
530
+ 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')),
531
+ 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),
532
+ 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`)),
533
+ 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
534
  ),
877
- helpSection,
878
- structureGuide,
879
- helpOverlay
535
+ 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))))),
536
+ 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(', ')}`))),
537
+ 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
538
  );
881
539
  }
882
540
 
@@ -886,17 +544,10 @@ function parseArgs() {
886
544
  const tokens = process.argv.slice(2);
887
545
  for (let i = 0; i < tokens.length; i += 1) {
888
546
  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
- }
547
+ if ((token === '--dir' || token === '--path') && tokens[i + 1]) { args.root = tokens[i + 1]; i += 1; }
548
+ else if (token === '--mode' && tokens[i + 1]) { args.mode = tokens[i + 1]; i += 1; }
549
+ else if (token === '--help' || token === '-h') args.help = true;
550
+ else if (token === '--studio') args.view = 'studio';
900
551
  }
901
552
  return args;
902
553
  }
@@ -918,9 +569,11 @@ async function main() {
918
569
  console.log(' โ†‘ / โ†“ Move project focus');
919
570
  console.log(' Enter Toggle detail view for selected project');
920
571
  console.log(' Shift+A Switch to Omni-Studio (Environment Health)');
921
- console.log(' Shift+X Clear the output log buffer');
572
+ console.log(' Shift+T Open Orbit Task Manager (Manage background processes)');
573
+ console.log(' Shift+D Detach from active task (Keep it running in background)');
574
+ console.log(' Shift+X Clear active task output log');
922
575
  console.log(' Shift+E Export current logs to a .txt file');
923
- console.log(' Shift+โ†‘ / โ†“ Scroll the output logs back/forward');
576
+ console.log(' Shift+โ†‘ / โ†“ Scroll the output logs');
924
577
  console.log(' Shift+Q Quit application');
925
578
  console.log('');
926
579
  console.log(kleur.bold('Execution shortcuts:'));
@@ -936,16 +589,11 @@ async function main() {
936
589
  if (args.mode === 'test') {
937
590
  const projects = await discoverProjects(rootPath);
938
591
  console.log(`Detected ${projects.length} project(s) under ${rootPath}`);
939
- projects.forEach((project) => {
940
- console.log(` โ€ข [${project.type}] ${project.name} (${project.path})`);
941
- });
592
+ projects.forEach((project) => { console.log(` โ€ข [${project.type}] ${project.name} (${project.path})`); });
942
593
  return;
943
594
  }
944
595
 
945
596
  render(create(Compass, {rootPath, initialView: args.view || 'navigator'}));
946
597
  }
947
598
 
948
- main().catch((error) => {
949
- console.error(error);
950
- process.exit(1);
951
- });
599
+ main().catch((error) => { console.error(error); process.exit(1); });