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.
- package/README.md +316 -207
- package/dist/cli/agent/loop.d.ts +4 -0
- package/dist/cli/agent/loop.d.ts.map +1 -1
- package/dist/cli/agent/loop.js +9 -1
- package/dist/cli/agent/loop.js.map +1 -1
- package/dist/cli/agent/permissions.d.ts.map +1 -1
- package/dist/cli/agent/permissions.js +7 -0
- package/dist/cli/agent/permissions.js.map +1 -1
- package/dist/cli/agent/tools/browser-click.d.ts +2 -0
- package/dist/cli/agent/tools/browser-click.d.ts.map +1 -0
- package/dist/cli/agent/tools/browser-click.js +35 -0
- package/dist/cli/agent/tools/browser-click.js.map +1 -0
- package/dist/cli/agent/tools/browser-close.d.ts +2 -0
- package/dist/cli/agent/tools/browser-close.d.ts.map +1 -0
- package/dist/cli/agent/tools/browser-close.js +27 -0
- package/dist/cli/agent/tools/browser-close.js.map +1 -0
- package/dist/cli/agent/tools/browser-navigate.d.ts +2 -0
- package/dist/cli/agent/tools/browser-navigate.d.ts.map +1 -0
- package/dist/cli/agent/tools/browser-navigate.js +27 -0
- package/dist/cli/agent/tools/browser-navigate.js.map +1 -0
- package/dist/cli/agent/tools/browser-open.d.ts +2 -0
- package/dist/cli/agent/tools/browser-open.d.ts.map +1 -0
- package/dist/cli/agent/tools/browser-open.js +38 -0
- package/dist/cli/agent/tools/browser-open.js.map +1 -0
- package/dist/cli/agent/tools/browser-screenshot.d.ts +2 -0
- package/dist/cli/agent/tools/browser-screenshot.d.ts.map +1 -0
- package/dist/cli/agent/tools/browser-screenshot.js +68 -0
- package/dist/cli/agent/tools/browser-screenshot.js.map +1 -0
- package/dist/cli/agent/tools/browser-type.d.ts +2 -0
- package/dist/cli/agent/tools/browser-type.d.ts.map +1 -0
- package/dist/cli/agent/tools/browser-type.js +33 -0
- package/dist/cli/agent/tools/browser-type.js.map +1 -0
- package/dist/cli/agent/tools/registry.d.ts +10 -0
- package/dist/cli/agent/tools/registry.d.ts.map +1 -1
- package/dist/cli/agent/tools/registry.js +12 -0
- package/dist/cli/agent/tools/registry.js.map +1 -1
- package/dist/cli/brain/control-protocol.d.ts +83 -2
- package/dist/cli/brain/control-protocol.d.ts.map +1 -1
- package/dist/cli/brain/control-protocol.js.map +1 -1
- package/dist/cli/brain/generator.d.ts +9 -1
- package/dist/cli/brain/generator.d.ts.map +1 -1
- package/dist/cli/brain/generator.js +36 -0
- package/dist/cli/brain/generator.js.map +1 -1
- package/dist/cli/brain/relay-client.d.ts.map +1 -1
- package/dist/cli/brain/relay-client.js +8 -2
- package/dist/cli/brain/relay-client.js.map +1 -1
- package/dist/cli/brain/server.d.ts.map +1 -1
- package/dist/cli/brain/server.js +5 -0
- package/dist/cli/brain/server.js.map +1 -1
- package/dist/cli/brain/web-chat-handler.d.ts +26 -0
- package/dist/cli/brain/web-chat-handler.d.ts.map +1 -0
- package/dist/cli/brain/web-chat-handler.js +130 -0
- package/dist/cli/brain/web-chat-handler.js.map +1 -0
- package/dist/cli/browser/chrome-finder.d.ts +12 -0
- package/dist/cli/browser/chrome-finder.d.ts.map +1 -0
- package/dist/cli/browser/chrome-finder.js +74 -0
- package/dist/cli/browser/chrome-finder.js.map +1 -0
- package/dist/cli/browser/controller.d.ts +51 -0
- package/dist/cli/browser/controller.d.ts.map +1 -0
- package/dist/cli/browser/controller.js +152 -0
- package/dist/cli/browser/controller.js.map +1 -0
- package/dist/cli/browser/vision.d.ts +38 -0
- package/dist/cli/browser/vision.d.ts.map +1 -0
- package/dist/cli/browser/vision.js +123 -0
- package/dist/cli/browser/vision.js.map +1 -0
- package/dist/cli/bugs/journal.d.ts +2 -0
- package/dist/cli/bugs/journal.d.ts.map +1 -1
- package/dist/cli/bugs/journal.js +4 -0
- package/dist/cli/bugs/journal.js.map +1 -1
- package/dist/cli/checkpoints/store.d.ts.map +1 -1
- package/dist/cli/checkpoints/store.js +6 -0
- package/dist/cli/checkpoints/store.js.map +1 -1
- package/dist/cli/commands/chat.d.ts.map +1 -1
- package/dist/cli/commands/chat.js +256 -79
- package/dist/cli/commands/chat.js.map +1 -1
- package/dist/cli/context/session-buffer.d.ts.map +1 -1
- package/dist/cli/context/session-buffer.js +6 -0
- package/dist/cli/context/session-buffer.js.map +1 -1
- package/dist/cli/providers/openai.d.ts.map +1 -1
- package/dist/cli/providers/openai.js +36 -7
- package/dist/cli/providers/openai.js.map +1 -1
- package/dist/cli/providers/types.d.ts +10 -1
- package/dist/cli/providers/types.d.ts.map +1 -1
- package/dist/cli/ui/activity.d.ts +9 -32
- package/dist/cli/ui/activity.d.ts.map +1 -1
- package/dist/cli/ui/activity.js +30 -115
- package/dist/cli/ui/activity.js.map +1 -1
- package/dist/cli/ui/bottom-chrome.d.ts +73 -0
- package/dist/cli/ui/bottom-chrome.d.ts.map +1 -0
- package/dist/cli/ui/bottom-chrome.js +213 -0
- package/dist/cli/ui/bottom-chrome.js.map +1 -0
- package/dist/cli/ui/command-suggest.d.ts +5 -3
- package/dist/cli/ui/command-suggest.d.ts.map +1 -1
- package/dist/cli/ui/command-suggest.js +8 -6
- package/dist/cli/ui/command-suggest.js.map +1 -1
- package/dist/spiral/injection.d.ts.map +1 -1
- package/dist/spiral/injection.js +16 -5
- package/dist/spiral/injection.js.map +1 -1
- package/dist/utils/tokens.d.ts +1 -1
- package/dist/utils/tokens.js +1 -1
- 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
|
-
//
|
|
264
|
-
|
|
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
|
-
|
|
489
|
-
|
|
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) => {
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
|
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('
|
|
597
|
-
hints.push(chalk.dim('
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
602
|
-
|
|
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
|
|
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
|
|
655
|
-
//
|
|
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
|
-
|
|
928
|
+
updateStatusBar();
|
|
787
929
|
return;
|
|
788
930
|
}
|
|
789
931
|
if (key.ctrl && key.name === 'pagedown') {
|
|
790
932
|
sessionMgr.switchNext();
|
|
791
|
-
|
|
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
|
|
871
|
-
//
|
|
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
|
-
|
|
877
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
883
|
-
|
|
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
|
|
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 && !
|
|
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,
|