project-compass 2.7.0 โ†’ 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 +171 -530
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.0",
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
@@ -142,9 +142,9 @@ function Studio() {
142
142
  {flexDirection: 'column'},
143
143
  ...runtimes.map(r => create(
144
144
  Box,
145
- {key: r.name, marginBottom: 1},
146
- create(Text, {width: 15, color: r.status === 'ok' ? 'green' : 'red'}, `${r.status === 'ok' ? 'โœ“' : 'โœ—'} ${r.name}`),
147
- create(Text, {dimColor: r.status !== 'ok'}, r.version)
145
+ {key: r.name, marginBottom: 0},
146
+ create(Text, {width: 20, color: r.status === 'ok' ? 'green' : 'red'}, `${r.status === 'ok' ? 'โœ“' : 'โœ—'} ${r.name}`),
147
+ create(Text, {dimColor: r.status !== 'ok'}, `: ${r.version}`)
148
148
  )),
149
149
  create(Text, {marginTop: 1, color: 'yellow'}, '๐Ÿ› ๏ธ Interactive Project Creator coming soon in v3.0'),
150
150
  create(Text, {dimColor: true}, 'Press Shift+A to return to Navigator.')
@@ -153,13 +153,6 @@ function Studio() {
153
153
  }
154
154
 
155
155
  function CursorText({value, cursorIndex, active = true}) {
156
- const [visible, setVisible] = useState(true);
157
- useEffect(() => {
158
- if (!active) return;
159
- const interval = setInterval(() => setVisible(v => !v), 500);
160
- return () => clearInterval(interval);
161
- }, [active]);
162
-
163
156
  const before = value.slice(0, cursorIndex);
164
157
  const charAt = value[cursorIndex] || ' ';
165
158
  const after = value.slice(cursorIndex + 1);
@@ -168,7 +161,7 @@ function CursorText({value, cursorIndex, active = true}) {
168
161
  Text,
169
162
  null,
170
163
  before,
171
- active && visible ? create(Text, {backgroundColor: 'white', color: 'black'}, charAt) : charAt,
164
+ active ? create(Text, {backgroundColor: 'white', color: 'black'}, charAt) : charAt,
172
165
  after
173
166
  );
174
167
  }
@@ -179,9 +172,9 @@ function Compass({rootPath, initialView = 'navigator'}) {
179
172
  const [selectedIndex, setSelectedIndex] = useState(0);
180
173
  const [viewMode, setViewMode] = useState('list');
181
174
  const [mainView, setMainView] = useState(initialView);
182
- const [logLines, setLogLines] = useState([]);
175
+ const [tasks, setTasks] = useState([]);
176
+ const [activeTaskId, setActiveTaskId] = useState(null);
183
177
  const [logOffset, setLogOffset] = useState(0);
184
- const [running, setRunning] = useState(false);
185
178
  const [lastAction, setLastAction] = useState(null);
186
179
  const [customMode, setCustomMode] = useState(false);
187
180
  const [customInput, setCustomInput] = useState('');
@@ -194,16 +187,20 @@ function Compass({rootPath, initialView = 'navigator'}) {
194
187
  const [showHelp, setShowHelp] = useState(false);
195
188
  const [recentRuns, setRecentRuns] = useState([]);
196
189
  const selectedProject = projects[selectedIndex] || null;
197
- const runningProcessRef = useRef(null);
190
+ const runningProcessMap = useRef(new Map());
198
191
  const lastCommandRef = useRef(null);
199
192
 
200
- const addLog = useCallback((line) => {
201
- 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;
202
199
  const normalized = typeof line === 'string' ? line : JSON.stringify(line);
203
200
  const lines = normalized.split(/\r?\n/).filter(l => l.trim().length > 0);
204
- const appended = [...prev, ...lines];
205
- return appended.length > 500 ? appended.slice(appended.length - 500) : appended;
206
- });
201
+ const nextLogs = [...t.logs, ...lines];
202
+ return { ...t, logs: nextLogs.length > 500 ? nextLogs.slice(-500) : nextLogs };
203
+ }));
207
204
  }, []);
208
205
 
209
206
  const detailCommands = useMemo(() => buildDetailCommands(selectedProject, config), [selectedProject, config]);
@@ -219,85 +216,71 @@ function Compass({rootPath, initialView = 'navigator'}) {
219
216
 
220
217
  const runProjectCommand = useCallback(async (commandMeta, targetProject = selectedProject) => {
221
218
  const project = targetProject || selectedProject;
222
- if (!project) {
223
- return;
224
- }
225
- if (!commandMeta || !Array.isArray(commandMeta.command) || commandMeta.command.length === 0) {
226
- addLog(kleur.gray('(no command configured)'));
227
- return;
228
- }
229
- if (running) {
230
- addLog(kleur.yellow('โ†’ Wait for the current task to finish.'));
231
- return;
232
- }
219
+ if (!project) return;
220
+ if (!commandMeta || !Array.isArray(commandMeta.command) || commandMeta.command.length === 0) return;
233
221
 
234
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);
235
234
  lastCommandRef.current = {project, commandMeta};
236
- setRunning(true);
237
- setLastAction(`${project.name} ยท ${commandLabel}`);
238
- const fullCmd = commandMeta.command;
239
- addLog(kleur.cyan(`> ${fullCmd.join(' ')}`));
235
+ setLastAction(newTask.name);
236
+
240
237
  setRecentRuns((prev) => {
241
238
  const entry = {project: project.name, command: commandLabel, time: new Date().toLocaleTimeString()};
242
239
  return [entry, ...prev].slice(0, RECENT_RUN_LIMIT);
243
240
  });
244
241
 
245
242
  try {
246
- const subprocess = execa(fullCmd[0], fullCmd.slice(1), {
243
+ const subprocess = execa(commandMeta.command[0], commandMeta.command.slice(1), {
247
244
  cwd: project.path,
248
245
  env: process.env,
249
246
  stdin: 'pipe'
250
247
  });
251
- runningProcessRef.current = subprocess;
248
+ runningProcessMap.current.set(taskId, subprocess);
252
249
 
253
- subprocess.stdout?.on('data', (chunk) => {
254
- addLog(chunk.toString());
255
- });
256
- subprocess.stderr?.on('data', (chunk) => {
257
- addLog(kleur.red(chunk.toString()));
258
- });
250
+ subprocess.stdout?.on('data', (chunk) => addLogToTask(taskId, chunk.toString()));
251
+ subprocess.stderr?.on('data', (chunk) => addLogToTask(taskId, kleur.red(chunk.toString())));
259
252
 
260
253
  await subprocess;
261
- 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`));
262
256
  } catch (error) {
263
- 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}`));
264
259
  } finally {
265
- setRunning(false);
266
- setStdinBuffer('');
267
- setStdinCursor(0);
268
- runningProcessRef.current = null;
260
+ runningProcessMap.current.delete(taskId);
269
261
  }
270
- }, [addLog, running, selectedProject]);
262
+ }, [addLogToTask, selectedProject]);
271
263
 
272
264
  const handleAddCustomCommand = useCallback((label, commandTokens) => {
273
- if (!selectedProject) {
274
- return;
275
- }
265
+ if (!selectedProject) return;
276
266
  setConfig((prev) => {
277
267
  const projectKey = selectedProject.path;
278
268
  const existing = prev.customCommands?.[projectKey] || [];
279
- const nextCustom = [...existing, {label, command: commandTokens}];
280
269
  const nextConfig = {
281
270
  ...prev,
282
271
  customCommands: {
283
272
  ...prev.customCommands,
284
- [projectKey]: nextCustom
273
+ [projectKey]: [...existing, {label, command: commandTokens}]
285
274
  }
286
275
  };
287
276
  saveConfig(nextConfig);
288
277
  return nextConfig;
289
278
  });
290
- addLog(kleur.yellow(`Saved custom command "${label}" for ${selectedProject.name}`));
291
- }, [selectedProject, addLog]);
279
+ }, [selectedProject]);
292
280
 
293
281
  const handleCustomSubmit = useCallback(() => {
294
282
  const raw = customInput.trim();
295
- if (!selectedProject) {
296
- setCustomMode(false);
297
- return;
298
- }
299
- if (!raw) {
300
- addLog(kleur.gray('Canceled custom command (empty).'));
283
+ if (!selectedProject || !raw) {
301
284
  setCustomMode(false);
302
285
  setCustomInput('');
303
286
  setCustomCursor(0);
@@ -306,44 +289,33 @@ function Compass({rootPath, initialView = 'navigator'}) {
306
289
  const [labelPart, commandPart] = raw.split('|');
307
290
  const commandTokens = (commandPart || labelPart).trim().split(/\s+/).filter(Boolean);
308
291
  if (!commandTokens.length) {
309
- addLog(kleur.red('Custom command needs at least one token.'));
310
292
  setCustomMode(false);
311
293
  setCustomInput('');
312
294
  setCustomCursor(0);
313
295
  return;
314
296
  }
315
297
  const label = commandPart ? labelPart.trim() : `Custom ${selectedProject.name}`;
316
- handleAddCustomCommand(label || 'Custom', commandTokens);
298
+ handleAddCustomCommand(label, commandTokens);
317
299
  setCustomMode(false);
318
300
  setCustomInput('');
319
301
  setCustomCursor(0);
320
- }, [customInput, selectedProject, handleAddCustomCommand, addLog]);
302
+ }, [customInput, selectedProject, handleAddCustomCommand]);
321
303
 
322
304
  const exportLogs = useCallback(() => {
323
- if (!logLines.length) {
324
- return;
325
- }
305
+ if (!activeTask || !activeTask.logs.length) return;
326
306
  try {
327
- const exportPath = path.resolve(process.cwd(), `compass-logs-${Date.now()}.txt`);
328
- fs.writeFileSync(exportPath, logLines.join('\n'));
329
- 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}`));
330
310
  } catch (err) {
331
- addLog(kleur.red(`โœ— Export failed: ${err.message}`));
311
+ addLogToTask(activeTaskId, kleur.red(`โœ— Export failed: ${err.message}`));
332
312
  }
333
- }, [logLines, addLog]);
313
+ }, [activeTask, activeTaskId, addLogToTask]);
334
314
 
335
315
  useInput((input, key) => {
336
316
  if (customMode) {
337
- if (key.return) {
338
- handleCustomSubmit();
339
- return;
340
- }
341
- if (key.escape) {
342
- setCustomMode(false);
343
- setCustomInput('');
344
- setCustomCursor(0);
345
- return;
346
- }
317
+ if (key.return) { handleCustomSubmit(); return; }
318
+ if (key.escape) { setCustomMode(false); setCustomInput(''); setCustomCursor(0); return; }
347
319
  if (key.backspace || key.delete) {
348
320
  if (customCursor > 0) {
349
321
  setCustomInput((prev) => prev.slice(0, customCursor - 1) + prev.slice(customCursor));
@@ -351,14 +323,8 @@ function Compass({rootPath, initialView = 'navigator'}) {
351
323
  }
352
324
  return;
353
325
  }
354
- if (key.leftArrow) {
355
- setCustomCursor(c => Math.max(0, c - 1));
356
- return;
357
- }
358
- if (key.rightArrow) {
359
- setCustomCursor(c => Math.min(customInput.length, c + 1));
360
- return;
361
- }
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; }
362
328
  if (input) {
363
329
  setCustomInput((prev) => prev.slice(0, customCursor) + input + prev.slice(customCursor));
364
330
  setCustomCursor(c => c + input.length);
@@ -367,51 +333,28 @@ function Compass({rootPath, initialView = 'navigator'}) {
367
333
  }
368
334
 
369
335
  const normalizedInput = input?.toLowerCase();
370
- const ctrlCombo = (char) => key.ctrl && normalizedInput === char;
371
336
  const shiftCombo = (char) => key.shift && normalizedInput === char;
372
- const toggleShortcut = (char) => shiftCombo(char);
373
- if (toggleShortcut('h')) {
374
- setShowHelpCards((prev) => !prev);
375
- return;
376
- }
377
- if (toggleShortcut('s')) {
378
- setShowStructureGuide((prev) => !prev);
379
- return;
380
- }
381
- if (toggleShortcut('a')) {
382
- setMainView((prev) => (prev === 'navigator' ? 'studio' : 'navigator'));
383
- return;
384
- }
385
- if (toggleShortcut('x')) {
386
- setLogLines([]);
387
- setLogOffset(0);
388
- return;
389
- }
390
- if (shiftCombo('e')) {
391
- exportLogs();
392
- return;
393
- }
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; }
394
345
 
395
346
  const scrollLogs = (delta) => {
396
347
  setLogOffset((prev) => {
397
- 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);
398
350
  return Math.max(0, Math.min(maxScroll, prev + delta));
399
351
  });
400
352
  };
401
353
 
402
- if (running && runningProcessRef.current) {
403
- if (key.ctrl && input === 'c') {
404
- runningProcessRef.current.kill('SIGINT');
405
- setStdinBuffer('');
406
- setStdinCursor(0);
407
- return;
408
- }
409
- if (key.return) {
410
- runningProcessRef.current.stdin?.write(stdinBuffer + '\n');
411
- setStdinBuffer('');
412
- setStdinCursor(0);
413
- return;
414
- }
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; }
415
358
  if (key.backspace || key.delete) {
416
359
  if (stdinCursor > 0) {
417
360
  setStdinBuffer(prev => prev.slice(0, stdinCursor - 1) + prev.slice(stdinCursor));
@@ -419,14 +362,8 @@ function Compass({rootPath, initialView = 'navigator'}) {
419
362
  }
420
363
  return;
421
364
  }
422
- if (key.leftArrow) {
423
- setStdinCursor(c => Math.max(0, c - 1));
424
- return;
425
- }
426
- if (key.rightArrow) {
427
- setStdinCursor(c => Math.min(stdinBuffer.length, c + 1));
428
- return;
429
- }
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; }
430
367
  if (input) {
431
368
  setStdinBuffer(prev => prev.slice(0, stdinCursor) + input + prev.slice(stdinCursor));
432
369
  setStdinCursor(c => c + input.length);
@@ -434,49 +371,29 @@ function Compass({rootPath, initialView = 'navigator'}) {
434
371
  return;
435
372
  }
436
373
 
437
- if (key.shift && key.upArrow) {
438
- scrollLogs(1);
439
- return;
440
- }
441
- if (key.shift && key.downArrow) {
442
- scrollLogs(-1);
443
- return;
444
- }
374
+ if (key.shift && key.upArrow) { scrollLogs(-1); return; }
375
+ if (key.shift && key.downArrow) { scrollLogs(1); return; }
445
376
 
446
- if (normalizedInput === '?') {
447
- setShowHelp((prev) => !prev);
448
- return;
449
- }
450
- if (shiftCombo('l') && lastCommandRef.current) {
451
- runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project);
452
- return;
453
- }
377
+ if (normalizedInput === '?') { setShowHelp((prev) => !prev); return; }
378
+ if (shiftCombo('l') && lastCommandRef.current) { runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project); return; }
454
379
 
455
- if (key.upArrow && !key.shift && projects.length > 0) {
456
- setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length);
457
- return;
458
- }
459
- if (key.downArrow && !key.shift && projects.length > 0) {
460
- 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; }
461
384
  return;
462
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; }
463
389
  if (key.return) {
464
- if (!selectedProject) {
465
- return;
466
- }
390
+ if (!selectedProject) return;
467
391
  setViewMode((prev) => (prev === 'detail' ? 'list' : 'detail'));
468
392
  return;
469
393
  }
470
- if (shiftCombo('q')) {
471
- exit();
472
- return;
473
- }
474
- if (shiftCombo('c') && viewMode === 'detail' && selectedProject) {
475
- setCustomMode(true);
476
- setCustomInput('');
477
- setCustomCursor(0);
478
- return;
479
- }
394
+ if (shiftCombo('q')) { exit(); return; }
395
+ if (shiftCombo('c') && viewMode === 'detail' && selectedProject) { setCustomMode(true); setCustomInput(''); setCustomCursor(0); return; }
396
+
480
397
  const actionKey = normalizedInput && ACTION_MAP[normalizedInput];
481
398
  if (actionKey) {
482
399
  const commandMeta = selectedProject?.commands?.[actionKey];
@@ -488,18 +405,30 @@ function Compass({rootPath, initialView = 'navigator'}) {
488
405
  }
489
406
  });
490
407
 
491
- const projectCountLabel = `${projects.length} project${projects.length === 1 ? '' : 's'}`;
492
- const projectRows = [];
493
- if (loading) {
494
- projectRows.push(create(Text, {key: 'scanning', dimColor: true}, 'Scanning projectsโ€ฆ'));
495
- }
496
- if (error) {
497
- projectRows.push(create(Text, {key: 'error', color: 'red'}, `Unable to scan: ${error}`));
498
- }
499
- if (!loading && !error && projects.length === 0) {
500
- 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
+ );
501
424
  }
502
- 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) {
503
432
  projects.forEach((project, index) => {
504
433
  const isSelected = index === selectedIndex;
505
434
  const frameworkBadges = (project.frameworks || []).map((frame) => `${frame.icon} ${frame.name}`).join(', ');
@@ -511,14 +440,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
511
440
  create(
512
441
  Box,
513
442
  {flexDirection: 'row'},
514
- create(
515
- Text,
516
- {
517
- color: isSelected ? 'cyan' : 'white',
518
- bold: isSelected
519
- },
520
- `${project.icon} ${project.name}`
521
- ),
443
+ create(Text, {color: isSelected ? 'cyan' : 'white', bold: isSelected}, `${project.icon} ${project.name}`),
522
444
  hasMissingRuntime && create(Text, {color: 'red', bold: true}, ' โš ๏ธ Runtime missing')
523
445
  ),
524
446
  create(Text, {dimColor: true}, ` ${project.type} ยท ${path.relative(rootPath, project.path) || '.'}`),
@@ -531,359 +453,88 @@ function Compass({rootPath, initialView = 'navigator'}) {
531
453
  const detailContent = [];
532
454
  if (viewMode === 'detail' && selectedProject) {
533
455
  detailContent.push(
534
- create(
535
- Box,
536
- {key: 'title-row', flexDirection: 'row'},
456
+ create(Box, {key: 'title-row', flexDirection: 'row'},
537
457
  create(Text, {color: 'cyan', bold: true}, `${selectedProject.icon} ${selectedProject.name}`),
538
458
  selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0 && create(Text, {color: 'red', bold: true}, ' โš ๏ธ MISSING RUNTIME')
539
459
  ),
540
460
  create(Text, {key: 'manifest', dimColor: true}, `${selectedProject.type} ยท ${selectedProject.manifest || 'detected manifest'}`),
541
461
  create(Text, {key: 'loc', dimColor: true}, `Location: ${path.relative(rootPath, selectedProject.path) || '.'}`)
542
462
  );
543
- if (selectedProject.description) {
544
- detailContent.push(create(Text, {key: 'desc'}, selectedProject.description));
545
- }
463
+ if (selectedProject.description) detailContent.push(create(Text, {key: 'desc'}, selectedProject.description));
546
464
  const frameworks = (selectedProject.frameworks || []).map((lib) => `${lib.icon} ${lib.name}`).join(', ');
547
- if (frameworks) {
548
- detailContent.push(create(Text, {key: 'frames', dimColor: true}, `Frameworks: ${frameworks}`));
549
- }
550
- if (selectedProject.extra?.scripts && selectedProject.extra.scripts.length) {
551
- detailContent.push(create(Text, {key: 'scripts', dimColor: true}, `Scripts: ${selectedProject.extra.scripts.join(', ')}`));
552
- }
553
-
465
+ if (frameworks) detailContent.push(create(Text, {key: 'frames', dimColor: true}, `Frameworks: ${frameworks}`));
466
+
554
467
  if (selectedProject.missingBinaries && selectedProject.missingBinaries.length > 0) {
555
468
  detailContent.push(
556
- create(Text, {key: 'missing-title', color: 'red', bold: true, marginTop: 1}, 'MISSING BINARIES:'),
557
- create(Text, {key: 'missing-list', color: 'red'}, `Please install: ${selectedProject.missingBinaries.join(', ')}`),
558
- 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(', ')}`)
559
471
  );
560
472
  }
561
473
 
562
- detailContent.push(create(Text, {key: 'config-path', dimColor: true, marginTop: 1}, `Custom commands stored in ${CONFIG_PATH}`));
563
- detailContent.push(create(Text, {key: 'plugin-path', dimColor: true, marginBottom: 1}, `Extend frameworks via ${PLUGIN_FILE}`));
564
474
  detailContent.push(create(Text, {key: 'cmd-header', bold: true, marginTop: 1}, 'Commands'));
565
475
  detailedIndexed.forEach((command) => {
566
476
  detailContent.push(
567
- 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(' ')}`)
568
479
  );
569
- detailContent.push(create(Text, {key: `detail-line-${command.shortcut}-${command.label}`, dimColor: true}, ` โ†ณ ${command.command.join(' ')}`));
570
480
  });
571
- if (!detailedIndexed.length) {
572
- detailContent.push(create(Text, {key: 'no-cmds', dimColor: true}, 'No built-in commands yet. Add a custom command with Shift+C.'));
573
- }
574
- const setupHints = selectedProject.extra?.setupHints || [];
575
- if (setupHints.length) {
576
- detailContent.push(create(Text, {key: 'setup-header', dimColor: true, marginTop: 1}, 'Setup hints:'));
577
- setupHints.forEach((hint, hidx) => detailContent.push(create(Text, {key: `hint-${hidx}`, dimColor: true}, ` โ€ข ${hint}`)));
578
- }
579
- 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.'));
580
482
  } else {
581
- 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.'));
582
484
  }
583
485
 
584
486
  if (customMode) {
585
- detailContent.push(
586
- create(
587
- Box,
588
- {key: 'custom-input-box', flexDirection: 'row'},
589
- create(Text, {color: 'cyan'}, 'Type label|cmd (Enter: save, Esc: cancel): '),
590
- create(CursorText, {value: customInput, cursorIndex: customCursor})
591
- )
592
- );
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})));
593
488
  }
594
489
 
595
- const artTileNodes = useMemo(() => {
596
- const selectedName = selectedProject?.name || 'Awaiting selection';
597
- const selectedType = selectedProject?.type || 'Unknown stack';
598
- const selectedLocation = selectedProject?.path ? path.relative(rootPath, selectedProject.path) || '.' : 'โ€”';
599
- const statusNarrative = running ? 'Running commands' : lastAction ? `Last: ${lastAction}` : 'Idle gallery';
600
- const workspaceName = path.basename(rootPath) || rootPath;
601
- const tileDefinition = [
602
- {
603
- label: 'Pulse',
604
- detail: projectCountLabel,
605
- accent: 'magenta',
606
- icon: 'โ—',
607
- subtext: `Workspace ยท ${workspaceName}`
608
- },
609
- {
610
- label: 'Focus',
611
- detail: selectedName,
612
- accent: 'cyan',
613
- icon: 'โ—†',
614
- subtext: `${selectedType} ยท ${selectedLocation}`
615
- },
616
- {
617
- label: 'Rhythm',
618
- detail: `${detailCommands.length} commands`,
619
- accent: 'yellow',
620
- icon: 'โ– ',
621
- subtext: statusNarrative
622
- }
623
- ];
624
- return tileDefinition.map((tile) =>
625
- create(
626
- Box,
627
- {
628
- key: tile.label,
629
- flexDirection: 'column',
630
- padding: 1,
631
- marginRight: 1,
632
- borderStyle: 'single',
633
- borderColor: tile.accent,
634
- minWidth: 24
635
- },
636
- create(Text, {color: tile.accent, bold: true}, `${tile.icon} ${tile.label}`),
637
- create(Text, {bold: true}, tile.detail),
638
- create(Text, {dimColor: true}, tile.subtext)
639
- )
640
- );
641
- }, [projectCountLabel, rootPath, selectedProject, detailCommands.length, running, lastAction]);
642
-
643
- const artBoard = create(
644
- Box,
645
- {
646
- flexDirection: 'column',
647
- marginTop: 1,
648
- borderStyle: 'round',
649
- borderColor: 'gray',
650
- padding: 1
651
- },
652
- create(
653
- Box,
654
- {flexDirection: 'row', justifyContent: 'space-between'},
655
- create(Text, {color: 'magenta', bold: true}, 'Art-coded build atlas'),
656
- create(Text, {dimColor: true}, 'press ? for overlay help')
657
- ),
658
- create(
659
- Box,
660
- {flexDirection: 'row', marginTop: 1},
661
- ...ART_CHARS.map((char, index) =>
662
- create(Text, {key: `art-${index}`, color: ART_COLORS[index % ART_COLORS.length]}, char.repeat(2))
663
- )
664
- ),
665
- create(
666
- Box,
667
- {flexDirection: 'row', marginTop: 1},
668
- ...artTileNodes
669
- ),
670
- 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)
671
504
  );
672
505
 
673
- const logWindowStart = Math.max(0, logLines.length - OUTPUT_WINDOW_SIZE - logOffset);
674
- const logWindowEnd = Math.max(0, logLines.length - logOffset);
675
- const visibleLogs = logLines.slice(logWindowStart, logWindowEnd);
676
- const logNodes = visibleLogs.length
677
- ? visibleLogs.map((line, index) => create(Text, {key: index}, line))
678
- : [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.')];
679
511
 
680
512
  const helpCards = [
681
- {
682
- label: 'Navigation',
683
- color: 'magenta',
684
- body: [
685
- 'โ†‘ / โ†“ move focus, Enter: details',
686
- 'Shift+โ†‘ / โ†“ scroll output',
687
- 'Shift+H toggle help cards',
688
- '? opens the overlay help'
689
- ]
690
- },
691
- {
692
- label: 'Command flow',
693
- color: 'cyan',
694
- body: [
695
- 'B / T / R build/test/run',
696
- '1-9 run detail commands',
697
- 'Shift+L rerun last command',
698
- 'Shift+X clear / Shift+E export'
699
- ]
700
- },
701
- {
702
- label: 'System & Studio',
703
- color: 'yellow',
704
- body: [
705
- 'Shift+A open Omni-Studio',
706
- 'Shift+S toggle structure guide',
707
- 'Shift+C save custom action',
708
- 'Shift+Q quit application'
709
- ]
710
- }
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']}
711
516
  ];
712
- const helpSection = showHelpCards
713
- ? create(
714
- Box,
715
- {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between', flexWrap: 'wrap'},
716
- ...helpCards.map((card, index) =>
717
- create(
718
- Box,
719
- {
720
- key: card.label,
721
- flexGrow: 1,
722
- flexBasis: 0,
723
- minWidth: HELP_CARD_MIN_WIDTH,
724
- marginRight: index < helpCards.length - 1 ? 1 : 0,
725
- marginBottom: 1,
726
- borderStyle: 'round',
727
- borderColor: card.color,
728
- padding: 1,
729
- flexDirection: 'column'
730
- },
731
- create(Text, {color: card.color, bold: true, marginBottom: 1}, card.label),
732
- ...card.body.map((line, lineIndex) =>
733
- create(Text, {key: `${card.label}-${lineIndex}`, dimColor: card.color === 'yellow'}, line)
734
- )
735
- )
736
- )
737
- )
738
- : create(Text, {key: 'help-hint', dimColor: true, marginTop: 1}, 'Help cards hidden ยท press Shift+H to show navigation, command flow, and recent runs.');
739
-
740
- const structureGuide = showStructureGuide
741
- ? create(
742
- Box,
743
- {
744
- flexDirection: 'column',
745
- borderStyle: 'round',
746
- borderColor: 'blue',
747
- marginTop: 1,
748
- padding: 1
749
- },
750
- create(Text, {color: 'cyan', bold: true}, 'Project structure guide ยท press Shift+S to hide'),
751
- ...SCHEMA_GUIDE.map((entry) =>
752
- create(Text, {key: entry.type, dimColor: true}, `โ€ข ${entry.icon} ${entry.label}: ${entry.files.join(', ')}`)
753
- ),
754
- create(Text, {dimColor: true, marginTop: 1}, 'SCHEMAS describe the manifests that trigger each language type.')
755
- )
756
- : null;
757
-
758
- const helpOverlay = showHelp
759
- ? create(
760
- Box,
761
- {
762
- flexDirection: 'column',
763
- borderStyle: 'double',
764
- borderColor: 'cyan',
765
- marginTop: 1,
766
- padding: 1
767
- },
768
- create(Text, {color: 'cyan', bold: true}, 'Help overlay ยท press ? to hide'),
769
- create(Text, null, 'Shift+โ†‘/โ†“ scrolls logs; Shift+X clears; Shift+E exports to file; Shift+A Omni-Studio.'),
770
- create(Text, null, 'B/T/R run build/test/run; 1-9 detail commands; Shift+L reruns previous command.'),
771
- create(Text, null, 'Shift+H help cards, Shift+S structure guide, ? overlay, Shift+Q quits.'),
772
- create(Text, null, 'Projects + Details stay paired while Output keeps its own full-width band.'),
773
- create(Text, null, 'Structure guide lists the manifests that trigger each language detection.')
774
- )
775
- : null;
776
-
777
- if (mainView === 'studio') {
778
- return create(Studio);
779
- }
780
517
 
781
- const toggleHint = showHelpCards ? 'Shift+H hides the help cards' : 'Shift+H shows the help cards';
782
- const headerHint = viewMode === 'detail'
783
- ? `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`
784
- : `Quick run ยท B/T/R to build/test/run, Enter: view details, Shift+Q: quit ยท ${toggleHint}, Shift+S toggles structure guide`;
785
-
786
- return create(
787
- Box,
788
- {flexDirection: 'column', padding: 1},
789
- create(
790
- Box,
791
- {justifyContent: 'space-between'},
792
- create(
793
- Box,
794
- {flexDirection: 'column'},
795
- create(Text, {color: 'magenta', bold: true}, 'Project Compass'),
796
- create(Text, {dimColor: true}, loading ? 'Scanning workspacesโ€ฆ' : `${projectCountLabel} detected in ${rootPath}`),
797
- create(Text, {dimColor: true}, 'Use keyboard arrows to navigate, ? for help.')
798
- ),
799
- create(
800
- Box,
801
- {flexDirection: 'column', alignItems: 'flex-end'},
802
- create(Text, {color: running ? 'yellow' : 'green'}, running ? 'Busy streaming...' : lastAction ? `Last action ยท ${lastAction}` : 'Idle'),
803
- create(Text, {dimColor: true}, headerHint)
804
- )
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`))
805
523
  ),
806
524
  artBoard,
807
- create(
808
- Box,
809
- {marginTop: 1, flexDirection: 'row', alignItems: 'stretch', width: '100%', flexWrap: 'wrap'},
810
- create(
811
- Box,
812
- {
813
- flexGrow: 1,
814
- flexBasis: 0,
815
- flexShrink: 1,
816
- minWidth: PROJECTS_MIN_WIDTH,
817
- marginRight: 1,
818
- borderStyle: 'round',
819
- borderColor: 'magenta',
820
- padding: 1
821
- },
822
- create(Text, {bold: true, color: 'magenta'}, 'Projects'),
823
- create(Box, {flexDirection: 'column', marginTop: 1}, ...projectRows)
824
- ),
825
- create(
826
- Box,
827
- {
828
- flexGrow: 1.3,
829
- flexBasis: 0,
830
- flexShrink: 1,
831
- minWidth: DETAILS_MIN_WIDTH,
832
- borderStyle: 'round',
833
- borderColor: 'cyan',
834
- padding: 1,
835
- flexDirection: 'column'
836
- },
837
- create(Text, {bold: true, color: 'cyan'}, 'Details'),
838
- ...detailContent
839
- )
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)
840
528
  ),
841
- create(
842
- Box,
843
- {marginTop: 1, flexDirection: 'column'},
844
- create(
845
- Box,
846
- {flexDirection: 'row', justifyContent: 'space-between'},
847
- create(Text, {bold: true, color: 'yellow'}, `Output ${running ? 'ยท Running' : ''}`),
848
- create(Text, {dimColor: true}, logOffset ? `Scrolled ${logOffset} lines` : 'Live log view')
849
- ),
850
- create(
851
- Box,
852
- {
853
- flexDirection: 'column',
854
- borderStyle: 'round',
855
- borderColor: 'yellow',
856
- padding: 1,
857
- minHeight: OUTPUT_WINDOW_HEIGHT,
858
- maxHeight: OUTPUT_WINDOW_HEIGHT,
859
- height: OUTPUT_WINDOW_HEIGHT,
860
- overflow: 'hidden',
861
- flexShrink: 0
862
- },
863
- ...logNodes
864
- ),
865
- create(
866
- Box,
867
- {marginTop: 1, flexDirection: 'row', justifyContent: 'space-between'},
868
- create(Text, {dimColor: true}, running ? 'Type to feed stdin; Enter submits, Ctrl+C aborts.' : 'Run a command or press ? for extra help.'),
869
- create(Text, {dimColor: true}, `${toggleHint}, Shift+S toggles the structure guide`)
870
- ),
871
- create(
872
- Box,
873
- {
874
- marginTop: 1,
875
- flexDirection: 'row',
876
- borderStyle: 'round',
877
- borderColor: running ? 'green' : 'gray',
878
- paddingX: 1
879
- },
880
- create(Text, {bold: true, color: running ? 'green' : 'white'}, running ? ' Stdin buffer ' : ' Input ready '),
881
- create(Box, {marginLeft: 1}, create(CursorText, {value: stdinBuffer || (running ? '' : 'Start a command to feed stdin'), cursorIndex: stdinCursor, active: running}))
882
- )
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})))
883
534
  ),
884
- helpSection,
885
- structureGuide,
886
- 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.'))
887
538
  );
888
539
  }
889
540
 
@@ -893,17 +544,10 @@ function parseArgs() {
893
544
  const tokens = process.argv.slice(2);
894
545
  for (let i = 0; i < tokens.length; i += 1) {
895
546
  const token = tokens[i];
896
- if ((token === '--dir' || token === '--path') && tokens[i + 1]) {
897
- args.root = tokens[i + 1];
898
- i += 1;
899
- } else if (token === '--mode' && tokens[i + 1]) {
900
- args.mode = tokens[i + 1];
901
- i += 1;
902
- } else if (token === '--help' || token === '-h') {
903
- args.help = true;
904
- } else if (token === '--studio') {
905
- args.view = 'studio';
906
- }
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';
907
551
  }
908
552
  return args;
909
553
  }
@@ -925,9 +569,11 @@ async function main() {
925
569
  console.log(' โ†‘ / โ†“ Move project focus');
926
570
  console.log(' Enter Toggle detail view for selected project');
927
571
  console.log(' Shift+A Switch to Omni-Studio (Environment Health)');
928
- 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');
929
575
  console.log(' Shift+E Export current logs to a .txt file');
930
- console.log(' Shift+โ†‘ / โ†“ Scroll the output logs back/forward');
576
+ console.log(' Shift+โ†‘ / โ†“ Scroll the output logs');
931
577
  console.log(' Shift+Q Quit application');
932
578
  console.log('');
933
579
  console.log(kleur.bold('Execution shortcuts:'));
@@ -943,16 +589,11 @@ async function main() {
943
589
  if (args.mode === 'test') {
944
590
  const projects = await discoverProjects(rootPath);
945
591
  console.log(`Detected ${projects.length} project(s) under ${rootPath}`);
946
- projects.forEach((project) => {
947
- console.log(` โ€ข [${project.type}] ${project.name} (${project.path})`);
948
- });
592
+ projects.forEach((project) => { console.log(` โ€ข [${project.type}] ${project.name} (${project.path})`); });
949
593
  return;
950
594
  }
951
595
 
952
596
  render(create(Compass, {rootPath, initialView: args.view || 'navigator'}));
953
597
  }
954
598
 
955
- main().catch((error) => {
956
- console.error(error);
957
- process.exit(1);
958
- });
599
+ main().catch((error) => { console.error(error); process.exit(1); });