helixmind 0.1.2 → 0.2.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.

Potentially problematic release.


This version of helixmind might be problematic. Click here for more details.

Files changed (101) hide show
  1. package/README.md +316 -207
  2. package/dist/cli/agent/loop.d.ts +4 -0
  3. package/dist/cli/agent/loop.d.ts.map +1 -1
  4. package/dist/cli/agent/loop.js +9 -1
  5. package/dist/cli/agent/loop.js.map +1 -1
  6. package/dist/cli/agent/permissions.d.ts.map +1 -1
  7. package/dist/cli/agent/permissions.js +7 -0
  8. package/dist/cli/agent/permissions.js.map +1 -1
  9. package/dist/cli/agent/tools/browser-click.d.ts +2 -0
  10. package/dist/cli/agent/tools/browser-click.d.ts.map +1 -0
  11. package/dist/cli/agent/tools/browser-click.js +35 -0
  12. package/dist/cli/agent/tools/browser-click.js.map +1 -0
  13. package/dist/cli/agent/tools/browser-close.d.ts +2 -0
  14. package/dist/cli/agent/tools/browser-close.d.ts.map +1 -0
  15. package/dist/cli/agent/tools/browser-close.js +27 -0
  16. package/dist/cli/agent/tools/browser-close.js.map +1 -0
  17. package/dist/cli/agent/tools/browser-navigate.d.ts +2 -0
  18. package/dist/cli/agent/tools/browser-navigate.d.ts.map +1 -0
  19. package/dist/cli/agent/tools/browser-navigate.js +27 -0
  20. package/dist/cli/agent/tools/browser-navigate.js.map +1 -0
  21. package/dist/cli/agent/tools/browser-open.d.ts +2 -0
  22. package/dist/cli/agent/tools/browser-open.d.ts.map +1 -0
  23. package/dist/cli/agent/tools/browser-open.js +38 -0
  24. package/dist/cli/agent/tools/browser-open.js.map +1 -0
  25. package/dist/cli/agent/tools/browser-screenshot.d.ts +2 -0
  26. package/dist/cli/agent/tools/browser-screenshot.d.ts.map +1 -0
  27. package/dist/cli/agent/tools/browser-screenshot.js +68 -0
  28. package/dist/cli/agent/tools/browser-screenshot.js.map +1 -0
  29. package/dist/cli/agent/tools/browser-type.d.ts +2 -0
  30. package/dist/cli/agent/tools/browser-type.d.ts.map +1 -0
  31. package/dist/cli/agent/tools/browser-type.js +33 -0
  32. package/dist/cli/agent/tools/browser-type.js.map +1 -0
  33. package/dist/cli/agent/tools/registry.d.ts +10 -0
  34. package/dist/cli/agent/tools/registry.d.ts.map +1 -1
  35. package/dist/cli/agent/tools/registry.js +12 -0
  36. package/dist/cli/agent/tools/registry.js.map +1 -1
  37. package/dist/cli/brain/control-protocol.d.ts +83 -2
  38. package/dist/cli/brain/control-protocol.d.ts.map +1 -1
  39. package/dist/cli/brain/control-protocol.js.map +1 -1
  40. package/dist/cli/brain/generator.d.ts +9 -1
  41. package/dist/cli/brain/generator.d.ts.map +1 -1
  42. package/dist/cli/brain/generator.js +36 -0
  43. package/dist/cli/brain/generator.js.map +1 -1
  44. package/dist/cli/brain/relay-client.d.ts.map +1 -1
  45. package/dist/cli/brain/relay-client.js +8 -2
  46. package/dist/cli/brain/relay-client.js.map +1 -1
  47. package/dist/cli/brain/server.d.ts.map +1 -1
  48. package/dist/cli/brain/server.js +5 -0
  49. package/dist/cli/brain/server.js.map +1 -1
  50. package/dist/cli/brain/web-chat-handler.d.ts +26 -0
  51. package/dist/cli/brain/web-chat-handler.d.ts.map +1 -0
  52. package/dist/cli/brain/web-chat-handler.js +130 -0
  53. package/dist/cli/brain/web-chat-handler.js.map +1 -0
  54. package/dist/cli/browser/chrome-finder.d.ts +12 -0
  55. package/dist/cli/browser/chrome-finder.d.ts.map +1 -0
  56. package/dist/cli/browser/chrome-finder.js +74 -0
  57. package/dist/cli/browser/chrome-finder.js.map +1 -0
  58. package/dist/cli/browser/controller.d.ts +51 -0
  59. package/dist/cli/browser/controller.d.ts.map +1 -0
  60. package/dist/cli/browser/controller.js +152 -0
  61. package/dist/cli/browser/controller.js.map +1 -0
  62. package/dist/cli/browser/vision.d.ts +38 -0
  63. package/dist/cli/browser/vision.d.ts.map +1 -0
  64. package/dist/cli/browser/vision.js +123 -0
  65. package/dist/cli/browser/vision.js.map +1 -0
  66. package/dist/cli/bugs/journal.d.ts +2 -0
  67. package/dist/cli/bugs/journal.d.ts.map +1 -1
  68. package/dist/cli/bugs/journal.js +4 -0
  69. package/dist/cli/bugs/journal.js.map +1 -1
  70. package/dist/cli/checkpoints/store.d.ts.map +1 -1
  71. package/dist/cli/checkpoints/store.js +6 -0
  72. package/dist/cli/checkpoints/store.js.map +1 -1
  73. package/dist/cli/commands/chat.d.ts.map +1 -1
  74. package/dist/cli/commands/chat.js +256 -79
  75. package/dist/cli/commands/chat.js.map +1 -1
  76. package/dist/cli/context/session-buffer.d.ts.map +1 -1
  77. package/dist/cli/context/session-buffer.js +6 -0
  78. package/dist/cli/context/session-buffer.js.map +1 -1
  79. package/dist/cli/providers/openai.d.ts.map +1 -1
  80. package/dist/cli/providers/openai.js +36 -7
  81. package/dist/cli/providers/openai.js.map +1 -1
  82. package/dist/cli/providers/types.d.ts +10 -1
  83. package/dist/cli/providers/types.d.ts.map +1 -1
  84. package/dist/cli/ui/activity.d.ts +9 -32
  85. package/dist/cli/ui/activity.d.ts.map +1 -1
  86. package/dist/cli/ui/activity.js +30 -115
  87. package/dist/cli/ui/activity.js.map +1 -1
  88. package/dist/cli/ui/bottom-chrome.d.ts +73 -0
  89. package/dist/cli/ui/bottom-chrome.d.ts.map +1 -0
  90. package/dist/cli/ui/bottom-chrome.js +213 -0
  91. package/dist/cli/ui/bottom-chrome.js.map +1 -0
  92. package/dist/cli/ui/command-suggest.d.ts +5 -3
  93. package/dist/cli/ui/command-suggest.d.ts.map +1 -1
  94. package/dist/cli/ui/command-suggest.js +8 -6
  95. package/dist/cli/ui/command-suggest.js.map +1 -1
  96. package/dist/spiral/injection.d.ts.map +1 -1
  97. package/dist/spiral/injection.js +16 -5
  98. package/dist/spiral/injection.js.map +1 -1
  99. package/dist/utils/tokens.d.ts +1 -1
  100. package/dist/utils/tokens.js +1 -1
  101. package/package.json +4 -1
@@ -10,6 +10,7 @@ import { renderError, renderInfo, renderSpiralStatus, renderUserMessage, } from
10
10
  import { isInsideToolBlock } from '../ui/tool-output.js';
11
11
  import { renderFeedProgress, renderFeedSummary } from '../ui/progress.js';
12
12
  import { ActivityIndicator } from '../ui/activity.js';
13
+ import { BottomChrome } from '../ui/bottom-chrome.js';
13
14
  import { theme } from '../ui/theme.js';
14
15
  import { detectFeedIntent } from '../feed/intent.js';
15
16
  import { runFeedPipeline } from '../feed/pipeline.js';
@@ -32,6 +33,8 @@ import { getSuggestions, writeSuggestions, clearSuggestions } from '../ui/comman
32
33
  import { selectMenu } from '../ui/select-menu.js';
33
34
  import { BugJournal } from '../bugs/journal.js';
34
35
  import { detectBugReport } from '../bugs/detector.js';
36
+ import { BrowserController } from '../browser/controller.js';
37
+ import { VisionProcessor } from '../browser/vision.js';
35
38
  import { classifyTask } from '../validation/classifier.js';
36
39
  import { generateCriteria } from '../validation/criteria.js';
37
40
  import { validationLoop } from '../validation/autofix.js';
@@ -100,6 +103,13 @@ const HELP_CATEGORIES = [
100
103
  { cmd: '/bugfix', label: '/bugfix', description: 'Review & fix all open bugs' },
101
104
  ],
102
105
  },
106
+ {
107
+ category: 'Browser', color: '#ff8800',
108
+ items: [
109
+ { cmd: '/browser', label: '/browser [url]', description: 'Open browser (optional URL)' },
110
+ { cmd: '/browser close', label: '/browser close', description: 'Close the browser' },
111
+ ],
112
+ },
103
113
  {
104
114
  category: 'Code & Git', color: '#8a2be2',
105
115
  items: [
@@ -260,14 +270,21 @@ export async function chatCommand(options) {
260
270
  const sessionBuffer = new SessionBuffer();
261
271
  // Bug journal (persistent bug tracking)
262
272
  const bugJournal = new BugJournal(process.cwd());
263
- // Activity indicator (replaces spinner)
264
- const activity = new ActivityIndicator();
273
+ // Browser controller (lazy — instantiated on /browser or agent tool use)
274
+ let browserController;
275
+ let visionProcessor;
276
+ // Bottom chrome (3 fixed rows at terminal bottom: separator, hints, statusbar)
277
+ const chrome = new BottomChrome();
278
+ // Activity indicator (renders on chrome row 0 during agent work)
279
+ const activity = new ActivityIndicator(chrome);
265
280
  // Agent controller for pause/resume
266
281
  const agentController = new AgentController();
267
282
  let agentRunning = false;
268
283
  let autonomousMode = false;
269
284
  // Forward-declared findings handler (reassigned by control protocol if active)
270
285
  let pushFindingsToBrainFn = null;
286
+ // Forward-declared browser screenshot handler (reassigned when brain server is active)
287
+ let pushScreenshotToBrainFn = null;
271
288
  // Session Manager — manages background sessions (security, auto, etc.)
272
289
  const sessionMgr = new SessionManager({
273
290
  flags: {
@@ -340,7 +357,7 @@ export async function chatCommand(options) {
340
357
  });
341
358
  // Single message mode
342
359
  if (options.message) {
343
- await sendAgentMessage(options.message, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal);
360
+ await sendAgentMessage(options.message, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; }, undefined, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal, browserController, visionProcessor, pushScreenshotToBrainFn);
344
361
  spiralEngine?.close();
345
362
  return;
346
363
  }
@@ -380,7 +397,7 @@ export async function chatCommand(options) {
380
397
  // === Register CLI ↔ Web control protocol ===
381
398
  if (brainUrl) {
382
399
  try {
383
- const { registerControlHandlers, setInstanceMeta, getBrainToken, pushSessionCreated, pushSessionUpdate, pushSessionRemoved, pushOutputLine, startRelayClient, } = await import('../brain/generator.js');
400
+ const { registerControlHandlers, setInstanceMeta, getBrainToken, pushSessionCreated, pushSessionUpdate, pushSessionRemoved, pushOutputLine, pushBugCreated, pushBugUpdated, pushBrowserScreenshot, pushControlEvent, startRelayClient, } = await import('../brain/generator.js');
384
401
  const { serializeSession, buildInstanceMeta, resetInstanceStartTime } = await import('../brain/control-protocol.js');
385
402
  resetInstanceStartTime();
386
403
  // Collected findings for getFindings() handler
@@ -484,12 +501,84 @@ export async function chatCommand(options) {
484
501
  pushSessionUpdate(serializeSession(session));
485
502
  return true;
486
503
  },
487
- sendChat: (text) => {
488
- // Queue chat text to be processed as if the user typed it
489
- typeAheadBuffer.push(text);
504
+ sendChat: (text, chatId, mode) => {
505
+ const effectiveChatId = chatId || `web-${Date.now()}`;
506
+ // Import web chat handler and run asynchronously
507
+ import('../brain/web-chat-handler.js').then(({ handleWebChat }) => {
508
+ handleWebChat(text, effectiveChatId, {
509
+ provider,
510
+ spiralEngine,
511
+ project,
512
+ config,
513
+ checkpointStore,
514
+ bugJournal,
515
+ }, {
516
+ onStarted: (cid) => {
517
+ pushControlEvent({ type: 'chat_started', chatId: cid, timestamp: Date.now() });
518
+ },
519
+ onTextChunk: (cid, chunk) => {
520
+ pushControlEvent({ type: 'chat_text_chunk', chatId: cid, text: chunk, timestamp: Date.now() });
521
+ },
522
+ onToolStart: (cid, stepNum, toolName, toolInput) => {
523
+ pushControlEvent({ type: 'chat_tool_start', chatId: cid, stepNum, toolName, toolInput, timestamp: Date.now() });
524
+ },
525
+ onToolEnd: (cid, stepNum, toolName, status, result) => {
526
+ pushControlEvent({ type: 'chat_tool_end', chatId: cid, stepNum, toolName, status, result, timestamp: Date.now() });
527
+ },
528
+ onComplete: (cid, fullText, steps, tokensUsed) => {
529
+ pushControlEvent({ type: 'chat_complete', chatId: cid, text: fullText, steps, tokensUsed, timestamp: Date.now() });
530
+ },
531
+ onError: (cid, error) => {
532
+ pushControlEvent({ type: 'chat_error', chatId: cid, error, timestamp: Date.now() });
533
+ },
534
+ }).catch((err) => {
535
+ pushControlEvent({ type: 'chat_error', chatId: effectiveChatId, error: err?.message || 'Unknown error', timestamp: Date.now() });
536
+ });
537
+ }).catch(() => { });
490
538
  },
491
539
  getFindings: () => [...collectedFindings],
540
+ getBugs: () => bugJournal.getAllBugs().map(b => ({
541
+ id: b.id,
542
+ description: b.description,
543
+ file: b.file,
544
+ line: b.line,
545
+ status: b.status,
546
+ createdAt: b.createdAt,
547
+ updatedAt: b.updatedAt,
548
+ fixedAt: b.fixedAt,
549
+ fixDescription: b.fixDescription,
550
+ })),
551
+ });
552
+ // Wire bug journal change events to brain server
553
+ bugJournal.setOnChange((event, bug) => {
554
+ const bugInfo = {
555
+ id: bug.id,
556
+ description: bug.description,
557
+ file: bug.file,
558
+ line: bug.line,
559
+ status: bug.status,
560
+ createdAt: bug.createdAt,
561
+ updatedAt: bug.updatedAt,
562
+ fixedAt: bug.fixedAt,
563
+ fixDescription: bug.fixDescription,
564
+ };
565
+ if (event === 'bug_created') {
566
+ pushBugCreated(bugInfo);
567
+ }
568
+ else {
569
+ pushBugUpdated(bugInfo);
570
+ }
492
571
  });
572
+ // Wire browser screenshots to brain server
573
+ pushScreenshotToBrainFn = (info) => {
574
+ pushBrowserScreenshot({
575
+ url: info.url,
576
+ title: info.title,
577
+ timestamp: Date.now(),
578
+ imageBase64: info.imageBase64,
579
+ analysis: info.analysis,
580
+ });
581
+ };
493
582
  // Override forward-reference to also collect findings for control protocol
494
583
  pushFindingsToBrainFn = (session) => {
495
584
  pushFindingsToBrain(session);
@@ -533,8 +622,29 @@ export async function chatCommand(options) {
533
622
  startAuto: (goal) => { /* relay delegates to local handlers — already registered */ return ''; },
534
623
  startSecurity: () => '',
535
624
  abortSession: (id) => { sessionMgr.abort(id); return true; },
536
- sendChat: (text) => { typeAheadBuffer.push(text); },
625
+ sendChat: (text, chatId, mode) => {
626
+ const effectiveChatId = chatId || `relay-${Date.now()}`;
627
+ import('../brain/web-chat-handler.js').then(({ handleWebChat }) => {
628
+ handleWebChat(text, effectiveChatId, {
629
+ provider, spiralEngine, project, config, checkpointStore, bugJournal,
630
+ }, {
631
+ onStarted: (cid) => { pushControlEvent({ type: 'chat_started', chatId: cid, timestamp: Date.now() }); },
632
+ onTextChunk: (cid, chunk) => { pushControlEvent({ type: 'chat_text_chunk', chatId: cid, text: chunk, timestamp: Date.now() }); },
633
+ onToolStart: (cid, sn, tn, ti) => { pushControlEvent({ type: 'chat_tool_start', chatId: cid, stepNum: sn, toolName: tn, toolInput: ti, timestamp: Date.now() }); },
634
+ onToolEnd: (cid, sn, tn, st, r) => { pushControlEvent({ type: 'chat_tool_end', chatId: cid, stepNum: sn, toolName: tn, status: st, result: r, timestamp: Date.now() }); },
635
+ onComplete: (cid, ft, s, tu) => { pushControlEvent({ type: 'chat_complete', chatId: cid, text: ft, steps: s, tokensUsed: tu, timestamp: Date.now() }); },
636
+ onError: (cid, e) => { pushControlEvent({ type: 'chat_error', chatId: cid, error: e, timestamp: Date.now() }); },
637
+ }).catch((err) => {
638
+ pushControlEvent({ type: 'chat_error', chatId: effectiveChatId, error: err?.message || 'Unknown error', timestamp: Date.now() });
639
+ });
640
+ }).catch(() => { });
641
+ },
537
642
  getFindings: () => [...collectedFindings],
643
+ getBugs: () => bugJournal.getAllBugs().map(b => ({
644
+ id: b.id, description: b.description, file: b.file, line: b.line,
645
+ status: b.status, createdAt: b.createdAt, updatedAt: b.updatedAt,
646
+ fixedAt: b.fixedAt, fixDescription: b.fixDescription,
647
+ })),
538
648
  }, updateMeta).catch(() => { });
539
649
  }
540
650
  }
@@ -570,36 +680,64 @@ export async function chatCommand(options) {
570
680
  const escaped = gt.replace(/(\x1b\[[0-9;]*m)/g, `${ansiStart}$1${ansiEnd}`);
571
681
  return `${escaped} `;
572
682
  }
573
- /**
574
- * Show the full prompt area:
575
- * ──────────────────
576
- * ▸▸ safe permissions · esc = stop · /help
577
- * 🌀 L1:... | tokens | model | git
578
- * > _ ← cursor here (last line)
579
- *
580
- * Info is written ABOVE the prompt as normal scrolling text.
581
- * The prompt is always the last line — no ANSI cursor tricks needed.
582
- */
583
- function showPrompt() {
584
- const w = Math.max(20, (process.stdout.columns || 80) - 2);
585
- const sep = chalk.hex('#00d4ff').dim('\u2500'.repeat(w));
683
+ /** Build separator content for chrome row 0 */
684
+ function buildSeparator(w) {
685
+ return chalk.hex('#00d4ff').dim('\u2500'.repeat(w));
686
+ }
687
+ /** Build hint line content for chrome row 1 */
688
+ function buildHintLine() {
586
689
  const data = getStatusBarData();
587
- const bar = renderStatusBar(data, w);
588
- // Build hint line
589
690
  const hints = [];
590
691
  if (data.permissionMode === 'yolo')
591
692
  hints.push(chalk.red('\u25B8\u25B8 yolo mode'));
592
693
  else if (data.permissionMode === 'skip')
593
- hints.push(chalk.yellow('\u25B8\u25B8 skip permissions'));
694
+ hints.push(chalk.yellow('\u25B8\u25B8 bypass permissions'));
594
695
  else
595
696
  hints.push(chalk.green('\u25B8\u25B8 safe permissions'));
596
- hints.push(chalk.dim('esc = stop'));
597
- hints.push(chalk.dim('/help'));
598
- const hintLine = hints.join(chalk.dim(' \u00B7 '));
599
- // Write info above the prompt, then the prompt as the last line
697
+ hints.push(chalk.dim('shift+tab to cycle'));
698
+ hints.push(chalk.dim('esc to interrupt'));
699
+ return hints.join(chalk.dim(' \u00B7 '));
700
+ }
701
+ /**
702
+ * Show the full prompt area using sticky bottom chrome:
703
+ * > _ ← cursor here (bottom of scroll region, row N-3)
704
+ * ────────────────── ← row N-2: separator (fixed chrome row 0)
705
+ * ▸▸ safe · esc = stop ← row N-1: hints (fixed chrome row 1)
706
+ * 🌀 L1:... | statusbar ← row N: statusbar (fixed chrome row 2)
707
+ */
708
+ function showPrompt() {
709
+ const w = Math.max(20, (process.stdout.columns || 80) - 2);
710
+ // Update chrome row content
711
+ const sep = buildSeparator(w);
712
+ const hintLine = buildHintLine();
713
+ const data = getStatusBarData();
714
+ const bar = renderStatusBar(data, w);
715
+ // Set separator content on activity indicator (for restore after agent work)
716
+ activity.setSeparatorContent(sep);
717
+ // Combine tab bar into statusbar row when sessions exist
718
+ let statusContent = bar;
719
+ if (sessionMgr.all.length > 1) {
720
+ const tabBar = truncateBar(sessionMgr.renderTabs(), w);
721
+ statusContent = `${tabBar} ${bar}`;
722
+ }
723
+ chrome.setRow(0, sep);
724
+ chrome.setRow(1, hintLine);
725
+ chrome.setRow(2, statusContent);
726
+ // Activate chrome if not already (sets scroll region + hooks stdout)
727
+ if (!chrome.isActive && !chrome.isInlineMode) {
728
+ chrome.activate();
729
+ }
600
730
  isAtPrompt = true;
601
- process.stdout.write(`\n${sep}\n ${hintLine}\n ${bar}\n`);
602
- rl.prompt();
731
+ if (chrome.isActive) {
732
+ // Position cursor at the bottom of the scroll region, then prompt
733
+ chrome.positionCursorForPrompt();
734
+ rl.prompt();
735
+ }
736
+ else {
737
+ // Inline fallback for small terminals
738
+ process.stdout.write(`\n${sep}\n ${hintLine}\n ${bar}\n`);
739
+ rl.prompt();
740
+ }
603
741
  }
604
742
  /** Build current status bar data object */
605
743
  function getStatusBarData() {
@@ -642,18 +780,22 @@ export async function chatCommand(options) {
642
780
  return [[], line];
643
781
  },
644
782
  });
645
- // Update prompt and activity scroll region on terminal resize
783
+ // Update prompt, chrome, and activity on terminal resize
646
784
  process.stdout.on('resize', () => {
647
785
  rl.setPrompt(makePrompt());
786
+ chrome.handleResize();
648
787
  activity.handleResize();
788
+ if (isAtPrompt) {
789
+ chrome.positionCursorForPrompt();
790
+ rl.prompt();
791
+ }
649
792
  });
650
793
  // Track suggestion overlay state
651
794
  let lastSuggestionCount = 0;
652
795
  permissions.setReadline(rl);
653
796
  permissions.setPromptCallback((active) => { isAtPrompt = active; });
654
- // Activity indicator renders on the bottom terminal row (absolute positioned,
655
- // same row as statusbar). The footer timer already skips statusbar draws when
656
- // activity.isAnimating is true, so there's no conflict.
797
+ // Activity indicator renders on chrome row 0 (separator/activity row, terminal row N-2).
798
+ // BottomChrome manages the scroll region and stdout hook for all 3 fixed rows.
657
799
  // Ctrl+C behavior:
658
800
  // - If there's text on the line → clear the line (like a normal terminal)
659
801
  // - If line is empty → count towards exit (double Ctrl+C = exit)
@@ -783,12 +925,12 @@ export async function chatCommand(options) {
783
925
  // === Tab switching: Ctrl+PageUp / Ctrl+PageDown ===
784
926
  if (key.ctrl && key.name === 'pageup') {
785
927
  sessionMgr.switchPrev();
786
- writeTabBar();
928
+ updateStatusBar();
787
929
  return;
788
930
  }
789
931
  if (key.ctrl && key.name === 'pagedown') {
790
932
  sessionMgr.switchNext();
791
- writeTabBar();
933
+ updateStatusBar();
792
934
  return;
793
935
  }
794
936
  // === Command Suggestions ===
@@ -841,8 +983,9 @@ export async function chatCommand(options) {
841
983
  // Double-ESC detection (for checkpoint browser when nothing is running)
842
984
  const result = processKeypress(key, keyState);
843
985
  if (result.action === 'open_browser' && !agentRunning) {
844
- // Open checkpoint browser
986
+ // Open checkpoint browser — deactivate chrome for fullscreen TUI
845
987
  rl.pause();
988
+ chrome.deactivate();
846
989
  try {
847
990
  const browserResult = await runCheckpointBrowser({
848
991
  store: checkpointStore,
@@ -862,56 +1005,34 @@ export async function chatCommand(options) {
862
1005
  catch {
863
1006
  // Browser closed unexpectedly
864
1007
  }
1008
+ chrome.activate();
865
1009
  rl.resume();
866
1010
  showPrompt();
867
1011
  }
868
1012
  });
869
1013
  }
870
- // Update statusbar uses save/restore cursor (DECSC/DECRC).
871
- // Only called during agent work to update token counts etc.
1014
+ // Update statusbar via BottomChrome row 2.
1015
+ // Called during agent work by the footer timer to update token counts etc.
872
1016
  function updateStatusBar() {
873
1017
  if (!process.stdout.isTTY)
874
1018
  return;
875
1019
  const data = getStatusBarData();
876
- writeStatusBar(data);
877
- // Draw tab bar if there are background sessions, otherwise clear stale tab bar
1020
+ const w = (process.stdout.columns || 80) - 2;
1021
+ const bar = renderStatusBar(data, w);
1022
+ // Combine tab bar into statusbar row when background sessions exist
1023
+ let statusContent = bar;
878
1024
  if (sessionMgr.all.length > 1) {
879
- writeTabBar();
1025
+ const tabBar = truncateBar(sessionMgr.renderTabs(), w);
1026
+ statusContent = `${tabBar} ${bar}`;
1027
+ }
1028
+ if (chrome.isActive) {
1029
+ chrome.setRow(2, statusContent);
880
1030
  }
881
1031
  else {
882
- // Clear the tab bar row when no background sessions remain
883
- clearTabBarRow();
1032
+ // Inline fallback
1033
+ writeStatusBar(data);
884
1034
  }
885
1035
  }
886
- /** Clear the tab bar row (row N-1) to remove stale tab bar text */
887
- function clearTabBarRow() {
888
- if (!process.stdout.isTTY)
889
- return;
890
- const termHeight = process.stdout.rows || 24;
891
- process.stdout.write(`\x1b7` + // Save cursor
892
- `\x1b[${termHeight - 1};0H` + // Move to tab bar row
893
- `\x1b[2K` + // Clear line
894
- `\x1b8`);
895
- }
896
- /** Draw the session tab bar above the statusbar */
897
- function writeTabBar() {
898
- if (!process.stdout.isTTY)
899
- return;
900
- if (sessionMgr.all.length <= 1)
901
- return;
902
- const tabBar = sessionMgr.renderTabs();
903
- const termHeight = process.stdout.rows || 24;
904
- const termWidth = (process.stdout.columns || 80) - 2;
905
- // Truncate tab bar to terminal width to prevent overflow into other rows
906
- const safeTabBar = truncateBar(tabBar, termWidth);
907
- // Write tab bar above the statusbar (termHeight - 1)
908
- // Layout: ..., tabbar(N-1), statusbar(N)
909
- process.stdout.write(`\x1b7` + // Save cursor
910
- `\x1b[${termHeight - 1};0H` + // Move to row above statusbar
911
- `\x1b[2K` + // Clear line
912
- ` ${safeTabBar}` + // Tab bar (truncated to fit)
913
- `\x1b8`);
914
- }
915
1036
  /** Push session findings to brain visualization */
916
1037
  function pushFindingsToBrain(session) {
917
1038
  import('../brain/generator.js').then(mod => {
@@ -958,13 +1079,14 @@ export async function chatCommand(options) {
958
1079
  const PASTE_THRESHOLD_MS = 100; // Lines arriving faster than this = paste (100ms for Windows compat)
959
1080
  // Show full prompt area on startup (separator + status + > prompt)
960
1081
  showPrompt();
961
- // Footer timer — redraws status bar during agent work (absolute positioning).
1082
+ // Footer timer — redraws statusbar on chrome row 2 during agent work.
962
1083
  // Skipped when:
963
1084
  // - user is at readline prompt (isAtPrompt) — prevents cursor-jumping
964
- // - activity indicator is animating — prevents flicker collision
965
1085
  // - inline progress active (inlineProgressActive) — prevents flicker over feed progress
1086
+ // Note: activity.isAnimating guard no longer needed — activity uses chrome row 0,
1087
+ // statusbar uses chrome row 2, they don't collide.
966
1088
  const footerTimer = setInterval(() => {
967
- if (process.stdout.isTTY && !isAtPrompt && !activity.isAnimating && !inlineProgressActive)
1089
+ if (process.stdout.isTTY && !isAtPrompt && !inlineProgressActive)
968
1090
  updateStatusBar();
969
1091
  }, 500);
970
1092
  footerTimer.unref();
@@ -999,6 +1121,46 @@ export async function chatCommand(options) {
999
1121
  showPrompt();
1000
1122
  return;
1001
1123
  }
1124
+ // Handle /browser directly (needs access to browserController closure)
1125
+ if (input.startsWith('/browser')) {
1126
+ const browserParts = input.split(/\s+/);
1127
+ const browserArg = browserParts.slice(1).join(' ').trim();
1128
+ if (browserArg === 'close') {
1129
+ if (browserController?.isOpen()) {
1130
+ await browserController.close();
1131
+ renderInfo('Browser closed.');
1132
+ }
1133
+ else {
1134
+ renderInfo('Browser is not open.');
1135
+ }
1136
+ showPrompt();
1137
+ return;
1138
+ }
1139
+ // Initialize browser controller if needed
1140
+ if (!browserController) {
1141
+ browserController = new BrowserController();
1142
+ }
1143
+ if (browserController.isOpen() && !browserArg) {
1144
+ renderInfo(`Browser already open at: ${browserController.getUrl() || 'about:blank'}`);
1145
+ showPrompt();
1146
+ return;
1147
+ }
1148
+ try {
1149
+ if (!browserController.isOpen()) {
1150
+ await browserController.launch();
1151
+ renderInfo('Browser opened.');
1152
+ }
1153
+ if (browserArg) {
1154
+ const result = await browserController.navigate(browserArg);
1155
+ renderInfo(`Navigated to: ${result.title} (${result.url})`);
1156
+ }
1157
+ }
1158
+ catch (err) {
1159
+ renderInfo(`Browser error: ${err instanceof Error ? err.message : String(err)}`);
1160
+ }
1161
+ showPrompt();
1162
+ return;
1163
+ }
1002
1164
  // Handle slash commands
1003
1165
  if (input.startsWith('/')) {
1004
1166
  const handled = await handleSlashCommand(input, messages, agentHistory, config, spiralEngine, store, rl, permissions, undoStack, checkpointStore, sessionBuffer, { input: sessionTokensInput, output: sessionTokensOutput }, sessionToolCalls, (newProvider) => { provider = newProvider; config = store.getAll(); }, async (newScope) => {
@@ -1213,7 +1375,7 @@ export async function chatCommand(options) {
1213
1375
  // Activity started — readline stays active for type-ahead buffering
1214
1376
  // but we do NOT show a visible prompt (it would collide with tool output)
1215
1377
  isAtPrompt = false;
1216
- }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal);
1378
+ }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal, browserController, visionProcessor, pushScreenshotToBrainFn);
1217
1379
  agentRunning = false;
1218
1380
  // Keep simple message history for state persistence
1219
1381
  messages.push({ role: 'user', content: input });
@@ -1229,7 +1391,7 @@ export async function chatCommand(options) {
1229
1391
  agentRunning = true;
1230
1392
  agentController.reset();
1231
1393
  updateStatusBar();
1232
- await sendAgentMessage(buffered.trim(), agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, () => { isAtPrompt = true; rl.prompt(); }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal);
1394
+ await sendAgentMessage(buffered.trim(), agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, agentController, activity, sessionBuffer, (inp, out) => { sessionTokensInput += inp; sessionTokensOutput += out; }, () => { sessionToolCalls++; roundToolCalls++; }, () => { isAtPrompt = true; rl.prompt(); }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal, browserController, visionProcessor, pushScreenshotToBrainFn);
1233
1395
  agentRunning = false;
1234
1396
  messages.push({ role: 'user', content: buffered.trim() });
1235
1397
  }
@@ -1356,6 +1518,7 @@ export async function chatCommand(options) {
1356
1518
  }
1357
1519
  rl.on('close', async () => {
1358
1520
  clearInterval(footerTimer);
1521
+ chrome.deactivate();
1359
1522
  if (spiralEngine) {
1360
1523
  // Persist session buffer (goals, entities, decisions) into spiral brain
1361
1524
  // so next session with the same brain can recall them
@@ -1377,6 +1540,13 @@ export async function chatCommand(options) {
1377
1540
  catch { /* best effort */ }
1378
1541
  spiralEngine.close();
1379
1542
  }
1543
+ // Close browser if open
1544
+ if (browserController?.isOpen()) {
1545
+ try {
1546
+ await browserController.close();
1547
+ }
1548
+ catch { /* best effort */ }
1549
+ }
1380
1550
  process.stdout.write('\n');
1381
1551
  process.exit(0);
1382
1552
  });
@@ -1404,7 +1574,7 @@ async function runBackgroundSession(session, prompt, provider, project, spiralEn
1404
1574
  durationMs: session.elapsed,
1405
1575
  };
1406
1576
  }
1407
- async function sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, controller, activity, sessionBuffer, onTokens, onToolCall, onAgentStart, validationOpts, bugJournal) {
1577
+ async function sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, controller, activity, sessionBuffer, onTokens, onToolCall, onAgentStart, validationOpts, bugJournal, browserController, visionProcessor, onBrowserScreenshot) {
1408
1578
  // User message was rendered by renderUserMessage() in the caller before entering here.
1409
1579
  // Intent Detection: Check if user wants to feed the codebase
1410
1580
  const feedIntent = detectFeedIntent(input);
@@ -1471,6 +1641,10 @@ async function sendAgentMessage(input, agentHistory, provider, project, spiralEn
1471
1641
  activity.start();
1472
1642
  // Notify caller so it can show the readline prompt for type-ahead
1473
1643
  onAgentStart?.();
1644
+ // Lazy-init vision processor when browser is available
1645
+ if (browserController && !visionProcessor) {
1646
+ visionProcessor = new VisionProcessor(provider);
1647
+ }
1474
1648
  try {
1475
1649
  const result = await runAgentLoop(input, agentHistory, {
1476
1650
  provider,
@@ -1481,6 +1655,9 @@ async function sendAgentMessage(input, agentHistory, provider, project, spiralEn
1481
1655
  undoStack,
1482
1656
  spiralEngine,
1483
1657
  bugJournal,
1658
+ browserController,
1659
+ visionProcessor,
1660
+ onBrowserScreenshot: onBrowserScreenshot ?? undefined,
1484
1661
  },
1485
1662
  checkpointStore,
1486
1663
  sessionBuffer,