helixmind 0.2.15 → 0.2.17

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.

@@ -8,7 +8,7 @@ import { analyzeProject } from '../context/project.js';
8
8
  import { assembleSystemPrompt } from '../context/assembler.js';
9
9
  import { renderLogo } from '../ui/logo.js';
10
10
  import { renderError, renderInfo, renderSpiralStatus, renderUserMessage, } from '../ui/chat-view.js';
11
- import { isInsideToolBlock } from '../ui/tool-output.js';
11
+ import { isInsideToolBlock, renderThinkingText } from '../ui/tool-output.js';
12
12
  import { renderFeedProgress, renderFeedSummary } from '../ui/progress.js';
13
13
  import { ActivityIndicator } from '../ui/activity.js';
14
14
  import { BottomChrome } from '../ui/bottom-chrome.js';
@@ -594,6 +594,9 @@ export async function chatCommand(options) {
594
594
  const { buildBaseline } = await import('../agent/monitor/baseline.js');
595
595
  const { runMonitorLoop } = await import('../agent/monitor/watcher.js');
596
596
  const { pushMonitorStatus: pushStatus } = await import('../brain/generator.js');
597
+ // Helper: log to both session buffer and terminal
598
+ const mLog = (msg) => { bgSession.capture(msg); renderInfo(`${chalk.hex('#ff6600')(icon)} ${chalk.dim(msg)}`); };
599
+ mLog('Phase 1: Scanning system...');
597
600
  const scanResult = await scanSystem({
598
601
  sendMessage: async (prompt) => {
599
602
  bgSession.controller.reset();
@@ -606,14 +609,17 @@ export async function chatCommand(options) {
606
609
  },
607
610
  isAborted: () => bgSession.controller.isAborted,
608
611
  onThreat: () => { }, onDefense: () => { },
609
- onScanComplete: (p) => bgSession.capture(`Scan: ${p}`),
612
+ onScanComplete: (p) => mLog(`Scan: ${p}`),
610
613
  onStatusUpdate: () => { }, updateStatus: () => { },
611
614
  });
612
615
  if (bgSession.controller.isAborted)
613
616
  return;
617
+ mLog('Phase 2: Building baseline...');
614
618
  const baseline = buildBaseline(scanResult);
619
+ mLog(`Baseline: ${baseline.processes.length} processes, ${baseline.ports.length} ports`);
615
620
  if (bgSession.controller.isAborted)
616
621
  return;
622
+ mLog(`Phase 3: Monitoring (${mode} mode)...`);
617
623
  await runMonitorLoop({
618
624
  sendMessage: async (prompt) => {
619
625
  bgSession.controller.reset();
@@ -625,9 +631,18 @@ export async function chatCommand(options) {
625
631
  return rth.text;
626
632
  },
627
633
  isAborted: () => bgSession.controller.isAborted,
628
- onThreat: (t) => bgSession.capture(`THREAT [${t.severity}]: ${t.title}`),
629
- onDefense: (d) => bgSession.capture(`DEFENSE: ${d.action} \u2192 ${d.target}`),
630
- onScanComplete: (p) => bgSession.capture(`Check: ${p}`),
634
+ onThreat: (t) => {
635
+ const msg = `THREAT [${t.severity}]: ${t.title}`;
636
+ bgSession.capture(msg);
637
+ const sc = t.severity === 'critical' ? '#ff0000' : t.severity === 'high' ? '#ff6600' : t.severity === 'medium' ? '#ffaa00' : '#888888';
638
+ renderInfo(`${chalk.hex(sc)('\u26A0')} ${chalk.hex(sc)(msg)}`);
639
+ },
640
+ onDefense: (d) => {
641
+ const msg = `DEFENSE: ${d.action} \u2192 ${d.target}`;
642
+ bgSession.capture(msg);
643
+ renderInfo(`${chalk.green('\u{1F6E1}\uFE0F')} ${chalk.green(msg)}`);
644
+ },
645
+ onScanComplete: (p) => mLog(`Check: ${p}`),
631
646
  onStatusUpdate: (state) => {
632
647
  pushStatus({ mode: state.mode, uptime: state.uptime, threatCount: state.threats.length, defenseCount: state.defenses.length, lastScan: state.lastScan });
633
648
  },
@@ -959,10 +974,15 @@ export async function chatCommand(options) {
959
974
  });
960
975
  // Ctrl+C behavior:
961
976
  // - If there's text on the line → clear the line (like a normal terminal)
977
+ // - If agent is running → interrupt agent
962
978
  // - If line is empty → count towards exit (double Ctrl+C = exit)
979
+ //
980
+ // IMPORTANT: Use rl.on('SIGINT') instead of process.on('SIGINT') because
981
+ // readline clears rl.line BEFORE emitting process SIGINT. The rl-level
982
+ // event fires while rl.line still has the original content.
963
983
  let ctrlCCount = 0;
964
984
  let ctrlCTimer = null;
965
- process.on('SIGINT', () => {
985
+ rl.on('SIGINT', () => {
966
986
  // If agent is running, treat Ctrl+C as interrupt
967
987
  if (agentRunning) {
968
988
  activity.stop('Stopped');
@@ -973,6 +993,7 @@ export async function chatCommand(options) {
973
993
  return;
974
994
  }
975
995
  // Check if readline has text on the current line
996
+ // (rl.line is still populated at this point — not yet cleared)
976
997
  const currentLine = rl.line || '';
977
998
  if (currentLine.length > 0) {
978
999
  // Clear current input — write a new line and re-prompt
@@ -984,6 +1005,20 @@ export async function chatCommand(options) {
984
1005
  ctrlCCount = 0;
985
1006
  return;
986
1007
  }
1008
+ // Also clear paste buffer if it has content
1009
+ if (pasteBuffer.length > 0) {
1010
+ pasteBuffer = [];
1011
+ if (pasteTimer) {
1012
+ clearTimeout(pasteTimer);
1013
+ pasteTimer = null;
1014
+ }
1015
+ process.stdout.write('\n');
1016
+ renderInfo(chalk.dim('Input cleared.'));
1017
+ isAtPrompt = true;
1018
+ rl.prompt();
1019
+ ctrlCCount = 0;
1020
+ return;
1021
+ }
987
1022
  // Empty line — count towards exit
988
1023
  ctrlCCount++;
989
1024
  if (ctrlCCount >= 2) {
@@ -1008,6 +1043,13 @@ export async function chatCommand(options) {
1008
1043
  clearTimeout(ctrlCTimer);
1009
1044
  ctrlCTimer = setTimeout(() => { ctrlCCount = 0; }, 2000);
1010
1045
  });
1046
+ // Fallback: OS-level SIGINT (e.g. kill -INT) — graceful exit
1047
+ process.on('SIGINT', () => {
1048
+ // Only handle if not already handled by rl.on('SIGINT')
1049
+ if (!process.stdin.isTTY) {
1050
+ process.exit(0);
1051
+ }
1052
+ });
1011
1053
  /** Register brain event handlers (voice, scope switch) — reusable for auto-start and /brain */
1012
1054
  async function registerBrainHandlers() {
1013
1055
  const { onBrainVoiceInput, onBrainScopeSwitch, pushScopeChange, onBrainModelActivate, pushModelActivated } = await import('../brain/generator.js');
@@ -1126,8 +1168,10 @@ export async function chatCommand(options) {
1126
1168
  }
1127
1169
  }
1128
1170
  // === ESC detection ===
1129
- // Single ESC stops immediately when agent is running
1130
- // Double ESC works as fallback anytime
1171
+ // Single ESC stops running agents immediately.
1172
+ // Double ESC opens the checkpoint Rewind browser.
1173
+ // IMPORTANT: Don't return after STOP — fall through so processKeypress
1174
+ // always sees the ESC for double-ESC detection.
1131
1175
  if (key.name === 'escape') {
1132
1176
  if (agentRunning || sessionMgr.hasBackgroundTasks || autonomousMode) {
1133
1177
  // Clear any suggestions
@@ -1148,42 +1192,51 @@ export async function chatCommand(options) {
1148
1192
  renderInfo(chalk.red('\u23F9 STOPPED') + chalk.dim(' \u2014 All agents interrupted.'));
1149
1193
  // Restore prompt so user can type again
1150
1194
  showPrompt();
1151
- return;
1195
+ // NOTE: no return — fall through to double-ESC detection below
1152
1196
  }
1153
1197
  }
1154
- // Double-ESC detection (for checkpoint browser when nothing is running)
1198
+ // Double-ESC detection (checkpoint Rewind browser)
1199
+ // processKeypress tracks ESC timing even when STOP ran above.
1155
1200
  const result = processKeypress(key, keyState);
1156
1201
  if (result.action === 'open_browser' && !agentRunning) {
1157
- // Open checkpoint browser deactivate chrome for fullscreen TUI
1158
- rl.pause();
1159
- chrome.deactivate();
1160
- try {
1161
- const browserResult = await runCheckpointBrowser({
1162
- store: checkpointStore,
1163
- agentHistory,
1164
- simpleMessages: messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : '' })),
1165
- isPaused: false,
1166
- });
1167
- if (browserResult.action === 'revert') {
1168
- const r = browserResult.result;
1169
- process.stdout.write('\n');
1170
- if (r.messagesRemoved > 0)
1171
- renderInfo(chalk.yellow(`${r.messagesRemoved} message(s) reverted`));
1172
- if (r.filesReverted > 0)
1173
- renderInfo(chalk.yellow(`${r.filesReverted} file(s) reverted`));
1174
- // Restore user text into readline input
1175
- if (browserResult.messageText) {
1176
- rl.line = browserResult.messageText;
1177
- rl.cursor = browserResult.messageText.length;
1202
+ // Check if there are any checkpoints before opening browser
1203
+ const allCps = checkpointStore.getAll();
1204
+ if (allCps.length === 0 || allCps.filter(cp => cp.type === 'chat').length === 0) {
1205
+ renderInfo(chalk.dim('No checkpoints yet \u2014 start chatting to create rewind points.'));
1206
+ showPrompt();
1207
+ }
1208
+ else {
1209
+ // Open checkpoint browser deactivate chrome for fullscreen TUI
1210
+ rl.pause();
1211
+ chrome.deactivate();
1212
+ try {
1213
+ const browserResult = await runCheckpointBrowser({
1214
+ store: checkpointStore,
1215
+ agentHistory,
1216
+ simpleMessages: messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : '' })),
1217
+ isPaused: false,
1218
+ });
1219
+ if (browserResult.action === 'revert') {
1220
+ const r = browserResult.result;
1221
+ process.stdout.write('\n');
1222
+ if (r.messagesRemoved > 0)
1223
+ renderInfo(chalk.yellow(`${r.messagesRemoved} message(s) reverted`));
1224
+ if (r.filesReverted > 0)
1225
+ renderInfo(chalk.yellow(`${r.filesReverted} file(s) reverted`));
1226
+ // Restore user text into readline input
1227
+ if (browserResult.messageText) {
1228
+ rl.line = browserResult.messageText;
1229
+ rl.cursor = browserResult.messageText.length;
1230
+ }
1178
1231
  }
1179
1232
  }
1233
+ catch {
1234
+ // Browser closed unexpectedly
1235
+ }
1236
+ chrome.activate();
1237
+ rl.resume();
1238
+ showPrompt();
1180
1239
  }
1181
- catch {
1182
- // Browser closed unexpectedly
1183
- }
1184
- chrome.activate();
1185
- rl.resume();
1186
- showPrompt();
1187
1240
  }
1188
1241
  });
1189
1242
  }
@@ -1440,8 +1493,13 @@ export async function chatCommand(options) {
1440
1493
  const { buildBaseline } = await import('../agent/monitor/baseline.js');
1441
1494
  const { runMonitorLoop } = await import('../agent/monitor/watcher.js');
1442
1495
  const { pushMonitorStatus } = await import('../brain/generator.js');
1496
+ // Helper: show monitor event in terminal (prefixed with icon)
1497
+ const monitorLog = (msg) => {
1498
+ bgSession.capture(msg);
1499
+ renderInfo(`${chalk.hex('#ff6600')(icon)} ${chalk.dim(msg)}`);
1500
+ };
1443
1501
  // Phase 1: Full system scan
1444
- bgSession.capture('Phase 1: Scanning system...');
1502
+ monitorLog('Phase 1: Scanning system...');
1445
1503
  const scanResult = await scanSystem({
1446
1504
  sendMessage: async (prompt) => {
1447
1505
  bgSession.controller.reset();
@@ -1458,20 +1516,20 @@ export async function chatCommand(options) {
1458
1516
  isAborted: () => bgSession.controller.isAborted,
1459
1517
  onThreat: () => { },
1460
1518
  onDefense: () => { },
1461
- onScanComplete: (phase) => bgSession.capture(`Scan: ${phase}`),
1519
+ onScanComplete: (phase) => monitorLog(`Scan: ${phase}`),
1462
1520
  onStatusUpdate: () => updateStatusBar(),
1463
1521
  updateStatus: () => updateStatusBar(),
1464
1522
  });
1465
1523
  if (bgSession.controller.isAborted)
1466
1524
  return;
1467
1525
  // Phase 2: Build baseline
1468
- bgSession.capture('Phase 2: Building security baseline...');
1526
+ monitorLog('Phase 2: Building security baseline...');
1469
1527
  const baseline = buildBaseline(scanResult);
1470
- bgSession.capture(`Baseline: ${baseline.processes.length} processes, ${baseline.ports.length} ports`);
1528
+ monitorLog(`Baseline: ${baseline.processes.length} processes, ${baseline.ports.length} ports`);
1471
1529
  if (bgSession.controller.isAborted)
1472
1530
  return;
1473
1531
  // Phase 3: Continuous watching
1474
- bgSession.capture(`Phase 3: Monitoring (${monitorMode} mode)...`);
1532
+ monitorLog(`Phase 3: Monitoring (${monitorMode} mode)...`);
1475
1533
  await runMonitorLoop({
1476
1534
  sendMessage: async (prompt) => {
1477
1535
  bgSession.controller.reset();
@@ -1487,12 +1545,20 @@ export async function chatCommand(options) {
1487
1545
  },
1488
1546
  isAborted: () => bgSession.controller.isAborted,
1489
1547
  onThreat: (threat) => {
1490
- bgSession.capture(`THREAT [${threat.severity}]: ${threat.title}`);
1548
+ const msg = `THREAT [${threat.severity}]: ${threat.title}`;
1549
+ bgSession.capture(msg);
1550
+ // Threats are always prominently shown
1551
+ const severityColor = threat.severity === 'critical' ? '#ff0000'
1552
+ : threat.severity === 'high' ? '#ff6600'
1553
+ : threat.severity === 'medium' ? '#ffaa00' : '#888888';
1554
+ renderInfo(`${chalk.hex(severityColor)('\u26A0')} ${chalk.hex(severityColor)(msg)}`);
1491
1555
  },
1492
1556
  onDefense: (defense) => {
1493
- bgSession.capture(`DEFENSE: ${defense.action} \u2192 ${defense.target}`);
1557
+ const msg = `DEFENSE: ${defense.action} \u2192 ${defense.target}`;
1558
+ bgSession.capture(msg);
1559
+ renderInfo(`${chalk.green('\u{1F6E1}\uFE0F')} ${chalk.green(msg)}`);
1494
1560
  },
1495
- onScanComplete: (phase) => bgSession.capture(`Check: ${phase}`),
1561
+ onScanComplete: (phase) => monitorLog(`Check: ${phase}`),
1496
1562
  onStatusUpdate: (state) => {
1497
1563
  pushMonitorStatus({
1498
1564
  mode: state.mode,
@@ -1720,36 +1786,10 @@ export async function chatCommand(options) {
1720
1786
  lastSuggestionCount = 0;
1721
1787
  }
1722
1788
  const trimmed = line.trim();
1723
- // If paste buffer has content and user pressed Enter on empty line → send it
1724
- if (!trimmed && pasteBuffer.length > 0) {
1725
- const assembled = pasteBuffer.join('\n').trim();
1726
- pasteBuffer = [];
1727
- if (pasteTimer) {
1728
- clearTimeout(pasteTimer);
1729
- pasteTimer = null;
1730
- }
1731
- process.stdout.write(`\x1b[2K\r`);
1732
- if (agentRunning) {
1733
- // Queue the entire paste as a single type-ahead entry
1734
- if (assembled) {
1735
- typeAheadBuffer.push(assembled);
1736
- const lineCount = assembled.split('\n').length;
1737
- process.stdout.write(` ${theme.dim('\u23F3 Queued:')} ${chalk.cyan(`[${lineCount} Zeilen]`)} ${theme.dim(assembled.split('\n')[0].slice(0, 50))}\n`);
1738
- }
1739
- rl.prompt();
1740
- }
1741
- else {
1742
- processInput(assembled);
1743
- }
1744
- return;
1745
- }
1746
- if (!trimmed) {
1747
- isAtPrompt = true;
1748
- rl.prompt();
1749
- return;
1750
- }
1751
- // Paste detection: if a timer is already running, this is a continuation
1752
- // (works both when agent is running and when idle)
1789
+ // Paste detection: if a timer is already running, this is a continuation.
1790
+ // ALL lines (including empty ones) are part of the paste block.
1791
+ // This MUST come before the empty-line-flush check so blank lines in
1792
+ // pasted text don't prematurely split the buffer into multiple messages.
1753
1793
  if (pasteTimer) {
1754
1794
  pasteBuffer.push(line);
1755
1795
  clearTimeout(pasteTimer);
@@ -1781,6 +1821,31 @@ export async function chatCommand(options) {
1781
1821
  }, PASTE_THRESHOLD_MS);
1782
1822
  return;
1783
1823
  }
1824
+ // If paste buffer has content and user pressed Enter on empty line → send it
1825
+ // (pasteTimer is null here, meaning the paste has ended and we're waiting for confirmation)
1826
+ if (!trimmed && pasteBuffer.length > 0) {
1827
+ const assembled = pasteBuffer.join('\n').trim();
1828
+ pasteBuffer = [];
1829
+ process.stdout.write(`\x1b[2K\r`);
1830
+ if (agentRunning) {
1831
+ // Queue the entire paste as a single type-ahead entry
1832
+ if (assembled) {
1833
+ typeAheadBuffer.push(assembled);
1834
+ const lineCount = assembled.split('\n').length;
1835
+ process.stdout.write(` ${theme.dim('\u23F3 Queued:')} ${chalk.cyan(`[${lineCount} Zeilen]`)} ${theme.dim(assembled.split('\n')[0].slice(0, 50))}\n`);
1836
+ }
1837
+ rl.prompt();
1838
+ }
1839
+ else {
1840
+ processInput(assembled);
1841
+ }
1842
+ return;
1843
+ }
1844
+ if (!trimmed) {
1845
+ isAtPrompt = true;
1846
+ rl.prompt();
1847
+ return;
1848
+ }
1784
1849
  // First line — start the paste timer
1785
1850
  pasteBuffer = [line];
1786
1851
  pasteTimer = setTimeout(() => {
@@ -2011,6 +2076,12 @@ async function sendAgentMessage(input, agentHistory, provider, project, spiralEn
2011
2076
  if (status === 'error')
2012
2077
  activity.setError();
2013
2078
  },
2079
+ onThinkingText: (text) => {
2080
+ // Show intermediate LLM reasoning before tool calls
2081
+ activity.pauseAnimation();
2082
+ renderThinkingText(text);
2083
+ activity.resumeAnimation();
2084
+ },
2014
2085
  onBeforeAnswer: () => {
2015
2086
  activity.stop(); // Writes colorful "HelixMind Done" replacing animation
2016
2087
  },