helixmind 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (101) hide show
  1. package/README.md +346 -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 +273 -83
  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 +40 -119
  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';
@@ -18,7 +19,7 @@ import { initializeTools } from '../agent/tools/registry.js';
18
19
  import { runAgentLoop, AgentController, AgentAbortError } from '../agent/loop.js';
19
20
  import { PermissionManager } from '../agent/permissions.js';
20
21
  import { UndoStack } from '../agent/undo.js';
21
- import { writeStatusBar, renderStatusBar, getGitInfo, truncateBar } from '../ui/statusbar.js';
22
+ import { writeStatusBar, renderStatusBar, getGitInfo, truncateBar, visibleLength } from '../ui/statusbar.js';
22
23
  import { CheckpointStore } from '../checkpoints/store.js';
23
24
  import { createKeybindingState, processKeypress } from '../checkpoints/keybinding.js';
24
25
  import { runCheckpointBrowser } from '../checkpoints/browser.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
  }
@@ -566,40 +676,81 @@ export async function chatCommand(options) {
566
676
  const ansiStart = '\x01'; // RL_PROMPT_START_IGNORE
567
677
  const ansiEnd = '\x02'; // RL_PROMPT_END_IGNORE
568
678
  // Wrap each ANSI escape sequence so readline ignores it for width calculation
569
- const gt = chalk.hex('#00d4ff').bold('>');
570
- const escaped = gt.replace(/(\x1b\[[0-9;]*m)/g, `${ansiStart}$1${ansiEnd}`);
571
- return `${escaped} `;
679
+ const pipe = chalk.hex('#00d4ff').dim('\u2502');
680
+ const gt = chalk.hex('#00d4ff').bold('\u276F');
681
+ const escapedPipe = pipe.replace(/(\x1b\[[0-9;]*m)/g, `${ansiStart}$1${ansiEnd}`);
682
+ const escapedGt = gt.replace(/(\x1b\[[0-9;]*m)/g, `${ansiStart}$1${ansiEnd}`);
683
+ return `${escapedPipe} ${escapedGt} `;
572
684
  }
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));
685
+ /** Build top border for input box: ┌──────────────────┐ */
686
+ function buildTopBorder(w) {
687
+ const dim = chalk.hex('#00d4ff').dim;
688
+ return dim('\u250C' + '\u2500'.repeat(w - 2) + '\u2510');
689
+ }
690
+ /** Build bottom border with embedded statusbar: └─ ◉ L1:7020 | ⚡ 636 tok ──┘ */
691
+ function buildBottomBorder(w, statusText) {
692
+ const dim = chalk.hex('#00d4ff').dim;
693
+ const prefix = dim('\u2514\u2500 ');
694
+ const statusVis = visibleLength(statusText);
695
+ const fill = Math.max(0, w - 4 - statusVis - 1); // 4 = "└─ " + space, 1 = "┘"
696
+ return `${prefix}${statusText} ${dim('\u2500'.repeat(fill) + '\u2518')}`;
697
+ }
698
+ /** Build hint line content for chrome row 2 */
699
+ function buildHintLine() {
586
700
  const data = getStatusBarData();
587
- const bar = renderStatusBar(data, w);
588
- // Build hint line
589
701
  const hints = [];
590
702
  if (data.permissionMode === 'yolo')
591
703
  hints.push(chalk.red('\u25B8\u25B8 yolo mode'));
592
704
  else if (data.permissionMode === 'skip')
593
- hints.push(chalk.yellow('\u25B8\u25B8 skip permissions'));
705
+ hints.push(chalk.yellow('\u25B8\u25B8 bypass permissions'));
594
706
  else
595
707
  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
708
+ hints.push(chalk.dim('shift+tab to cycle'));
709
+ hints.push(chalk.dim('esc to interrupt'));
710
+ return hints.join(chalk.dim(' \u00B7 '));
711
+ }
712
+ /**
713
+ * Show the full prompt area using sticky bottom chrome with input box:
714
+ * ┌──────────────────────────────────────┐ ← row N-2: top border (chrome row 0)
715
+ * │ ❯ input here_ ← cursor here (bottom of scroll region, row N-3)
716
+ * └─ ◉ L1:7020 | ⚡ 636 tok | safe ──────┘ ← row N-1: bottom border + status (chrome row 1)
717
+ * ▸▸ safe permissions · shift+tab · esc ← row N: hints (chrome row 2)
718
+ */
719
+ function showPrompt() {
720
+ const w = Math.max(20, (process.stdout.columns || 80) - 2);
721
+ // Build chrome row content
722
+ const topBorder = buildTopBorder(w);
723
+ const data = getStatusBarData();
724
+ const bar = renderStatusBar(data, w - 6); // narrower to fit in border frame
725
+ let statusText = bar;
726
+ if (sessionMgr.all.length > 1) {
727
+ const tabBar = truncateBar(sessionMgr.renderTabs(), Math.floor(w / 2));
728
+ statusText = `${tabBar} ${bar}`;
729
+ }
730
+ const bottomBorder = buildBottomBorder(w, statusText);
731
+ const hintLine = buildHintLine();
732
+ // Set top border content on activity indicator (for restore after agent work)
733
+ activity.setSeparatorContent(topBorder);
734
+ chrome.setRow(0, topBorder);
735
+ chrome.setRow(1, bottomBorder);
736
+ chrome.setRow(2, hintLine);
737
+ // Activate chrome if not already (sets scroll region + hooks stdout)
738
+ if (!chrome.isActive && !chrome.isInlineMode) {
739
+ chrome.activate();
740
+ }
600
741
  isAtPrompt = true;
601
- process.stdout.write(`\n${sep}\n ${hintLine}\n ${bar}\n`);
602
- rl.prompt();
742
+ if (chrome.isActive) {
743
+ // Position cursor at the bottom of the scroll region, then prompt
744
+ chrome.positionCursorForPrompt();
745
+ rl.prompt();
746
+ }
747
+ else {
748
+ // Inline fallback for small terminals
749
+ const dim = chalk.hex('#00d4ff').dim;
750
+ process.stdout.write(`\n${dim('\u250C' + '\u2500'.repeat(w - 2) + '\u2510')}\n`);
751
+ process.stdout.write(`${dim('\u2502')} `);
752
+ rl.prompt();
753
+ }
603
754
  }
604
755
  /** Build current status bar data object */
605
756
  function getStatusBarData() {
@@ -642,18 +793,22 @@ export async function chatCommand(options) {
642
793
  return [[], line];
643
794
  },
644
795
  });
645
- // Update prompt and activity scroll region on terminal resize
796
+ // Update prompt, chrome, and activity on terminal resize
646
797
  process.stdout.on('resize', () => {
647
798
  rl.setPrompt(makePrompt());
799
+ chrome.handleResize();
648
800
  activity.handleResize();
801
+ if (isAtPrompt) {
802
+ chrome.positionCursorForPrompt();
803
+ rl.prompt();
804
+ }
649
805
  });
650
806
  // Track suggestion overlay state
651
807
  let lastSuggestionCount = 0;
652
808
  permissions.setReadline(rl);
653
809
  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.
810
+ // Activity indicator renders on chrome row 0 (separator/activity row, terminal row N-2).
811
+ // BottomChrome manages the scroll region and stdout hook for all 3 fixed rows.
657
812
  // Ctrl+C behavior:
658
813
  // - If there's text on the line → clear the line (like a normal terminal)
659
814
  // - If line is empty → count towards exit (double Ctrl+C = exit)
@@ -783,12 +938,12 @@ export async function chatCommand(options) {
783
938
  // === Tab switching: Ctrl+PageUp / Ctrl+PageDown ===
784
939
  if (key.ctrl && key.name === 'pageup') {
785
940
  sessionMgr.switchPrev();
786
- writeTabBar();
941
+ updateStatusBar();
787
942
  return;
788
943
  }
789
944
  if (key.ctrl && key.name === 'pagedown') {
790
945
  sessionMgr.switchNext();
791
- writeTabBar();
946
+ updateStatusBar();
792
947
  return;
793
948
  }
794
949
  // === Command Suggestions ===
@@ -841,8 +996,9 @@ export async function chatCommand(options) {
841
996
  // Double-ESC detection (for checkpoint browser when nothing is running)
842
997
  const result = processKeypress(key, keyState);
843
998
  if (result.action === 'open_browser' && !agentRunning) {
844
- // Open checkpoint browser
999
+ // Open checkpoint browser — deactivate chrome for fullscreen TUI
845
1000
  rl.pause();
1001
+ chrome.deactivate();
846
1002
  try {
847
1003
  const browserResult = await runCheckpointBrowser({
848
1004
  store: checkpointStore,
@@ -862,56 +1018,34 @@ export async function chatCommand(options) {
862
1018
  catch {
863
1019
  // Browser closed unexpectedly
864
1020
  }
1021
+ chrome.activate();
865
1022
  rl.resume();
866
1023
  showPrompt();
867
1024
  }
868
1025
  });
869
1026
  }
870
- // Update statusbar uses save/restore cursor (DECSC/DECRC).
871
- // Only called during agent work to update token counts etc.
1027
+ // Update statusbar via BottomChrome row 1 (bottom border with embedded status).
1028
+ // Called during agent work by the footer timer to update token counts etc.
872
1029
  function updateStatusBar() {
873
1030
  if (!process.stdout.isTTY)
874
1031
  return;
875
1032
  const data = getStatusBarData();
876
- writeStatusBar(data);
877
- // Draw tab bar if there are background sessions, otherwise clear stale tab bar
1033
+ const w = (process.stdout.columns || 80) - 2;
1034
+ const bar = renderStatusBar(data, w - 6); // narrower to fit in border frame
1035
+ // Combine tab bar into status text when background sessions exist
1036
+ let statusText = bar;
878
1037
  if (sessionMgr.all.length > 1) {
879
- writeTabBar();
1038
+ const tabBar = truncateBar(sessionMgr.renderTabs(), Math.floor(w / 2));
1039
+ statusText = `${tabBar} ${bar}`;
1040
+ }
1041
+ if (chrome.isActive) {
1042
+ chrome.setRow(1, buildBottomBorder(w, statusText));
880
1043
  }
881
1044
  else {
882
- // Clear the tab bar row when no background sessions remain
883
- clearTabBarRow();
1045
+ // Inline fallback
1046
+ writeStatusBar(data);
884
1047
  }
885
1048
  }
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
1049
  /** Push session findings to brain visualization */
916
1050
  function pushFindingsToBrain(session) {
917
1051
  import('../brain/generator.js').then(mod => {
@@ -958,13 +1092,14 @@ export async function chatCommand(options) {
958
1092
  const PASTE_THRESHOLD_MS = 100; // Lines arriving faster than this = paste (100ms for Windows compat)
959
1093
  // Show full prompt area on startup (separator + status + > prompt)
960
1094
  showPrompt();
961
- // Footer timer — redraws status bar during agent work (absolute positioning).
1095
+ // Footer timer — redraws statusbar on chrome row 2 during agent work.
962
1096
  // Skipped when:
963
1097
  // - user is at readline prompt (isAtPrompt) — prevents cursor-jumping
964
- // - activity indicator is animating — prevents flicker collision
965
1098
  // - inline progress active (inlineProgressActive) — prevents flicker over feed progress
1099
+ // Note: activity.isAnimating guard no longer needed — activity uses chrome row 0,
1100
+ // statusbar uses chrome row 2, they don't collide.
966
1101
  const footerTimer = setInterval(() => {
967
- if (process.stdout.isTTY && !isAtPrompt && !activity.isAnimating && !inlineProgressActive)
1102
+ if (process.stdout.isTTY && !isAtPrompt && !inlineProgressActive)
968
1103
  updateStatusBar();
969
1104
  }, 500);
970
1105
  footerTimer.unref();
@@ -999,6 +1134,46 @@ export async function chatCommand(options) {
999
1134
  showPrompt();
1000
1135
  return;
1001
1136
  }
1137
+ // Handle /browser directly (needs access to browserController closure)
1138
+ if (input.startsWith('/browser')) {
1139
+ const browserParts = input.split(/\s+/);
1140
+ const browserArg = browserParts.slice(1).join(' ').trim();
1141
+ if (browserArg === 'close') {
1142
+ if (browserController?.isOpen()) {
1143
+ await browserController.close();
1144
+ renderInfo('Browser closed.');
1145
+ }
1146
+ else {
1147
+ renderInfo('Browser is not open.');
1148
+ }
1149
+ showPrompt();
1150
+ return;
1151
+ }
1152
+ // Initialize browser controller if needed
1153
+ if (!browserController) {
1154
+ browserController = new BrowserController();
1155
+ }
1156
+ if (browserController.isOpen() && !browserArg) {
1157
+ renderInfo(`Browser already open at: ${browserController.getUrl() || 'about:blank'}`);
1158
+ showPrompt();
1159
+ return;
1160
+ }
1161
+ try {
1162
+ if (!browserController.isOpen()) {
1163
+ await browserController.launch();
1164
+ renderInfo('Browser opened.');
1165
+ }
1166
+ if (browserArg) {
1167
+ const result = await browserController.navigate(browserArg);
1168
+ renderInfo(`Navigated to: ${result.title} (${result.url})`);
1169
+ }
1170
+ }
1171
+ catch (err) {
1172
+ renderInfo(`Browser error: ${err instanceof Error ? err.message : String(err)}`);
1173
+ }
1174
+ showPrompt();
1175
+ return;
1176
+ }
1002
1177
  // Handle slash commands
1003
1178
  if (input.startsWith('/')) {
1004
1179
  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 +1388,7 @@ export async function chatCommand(options) {
1213
1388
  // Activity started — readline stays active for type-ahead buffering
1214
1389
  // but we do NOT show a visible prompt (it would collide with tool output)
1215
1390
  isAtPrompt = false;
1216
- }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal);
1391
+ }, { enabled: validationEnabled, verbose: validationVerbose, strict: validationStrict }, bugJournal, browserController, visionProcessor, pushScreenshotToBrainFn);
1217
1392
  agentRunning = false;
1218
1393
  // Keep simple message history for state persistence
1219
1394
  messages.push({ role: 'user', content: input });
@@ -1229,7 +1404,7 @@ export async function chatCommand(options) {
1229
1404
  agentRunning = true;
1230
1405
  agentController.reset();
1231
1406
  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);
1407
+ 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
1408
  agentRunning = false;
1234
1409
  messages.push({ role: 'user', content: buffered.trim() });
1235
1410
  }
@@ -1356,6 +1531,7 @@ export async function chatCommand(options) {
1356
1531
  }
1357
1532
  rl.on('close', async () => {
1358
1533
  clearInterval(footerTimer);
1534
+ chrome.deactivate();
1359
1535
  if (spiralEngine) {
1360
1536
  // Persist session buffer (goals, entities, decisions) into spiral brain
1361
1537
  // so next session with the same brain can recall them
@@ -1377,6 +1553,13 @@ export async function chatCommand(options) {
1377
1553
  catch { /* best effort */ }
1378
1554
  spiralEngine.close();
1379
1555
  }
1556
+ // Close browser if open
1557
+ if (browserController?.isOpen()) {
1558
+ try {
1559
+ await browserController.close();
1560
+ }
1561
+ catch { /* best effort */ }
1562
+ }
1380
1563
  process.stdout.write('\n');
1381
1564
  process.exit(0);
1382
1565
  });
@@ -1404,7 +1587,7 @@ async function runBackgroundSession(session, prompt, provider, project, spiralEn
1404
1587
  durationMs: session.elapsed,
1405
1588
  };
1406
1589
  }
1407
- async function sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, controller, activity, sessionBuffer, onTokens, onToolCall, onAgentStart, validationOpts, bugJournal) {
1590
+ async function sendAgentMessage(input, agentHistory, provider, project, spiralEngine, config, permissions, undoStack, checkpointStore, controller, activity, sessionBuffer, onTokens, onToolCall, onAgentStart, validationOpts, bugJournal, browserController, visionProcessor, onBrowserScreenshot) {
1408
1591
  // User message was rendered by renderUserMessage() in the caller before entering here.
1409
1592
  // Intent Detection: Check if user wants to feed the codebase
1410
1593
  const feedIntent = detectFeedIntent(input);
@@ -1471,6 +1654,10 @@ async function sendAgentMessage(input, agentHistory, provider, project, spiralEn
1471
1654
  activity.start();
1472
1655
  // Notify caller so it can show the readline prompt for type-ahead
1473
1656
  onAgentStart?.();
1657
+ // Lazy-init vision processor when browser is available
1658
+ if (browserController && !visionProcessor) {
1659
+ visionProcessor = new VisionProcessor(provider);
1660
+ }
1474
1661
  try {
1475
1662
  const result = await runAgentLoop(input, agentHistory, {
1476
1663
  provider,
@@ -1481,6 +1668,9 @@ async function sendAgentMessage(input, agentHistory, provider, project, spiralEn
1481
1668
  undoStack,
1482
1669
  spiralEngine,
1483
1670
  bugJournal,
1671
+ browserController,
1672
+ visionProcessor,
1673
+ onBrowserScreenshot: onBrowserScreenshot ?? undefined,
1484
1674
  },
1485
1675
  checkpointStore,
1486
1676
  sessionBuffer,