flowmind 1.4.1 → 1.4.3
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.
- package/bin/flowmind.js +95 -36
- package/package.json +1 -1
- package/tui/app.jsx +9 -2
- package/tui/components/ChatPanel.jsx +3 -3
- package/tui/components/Sidebar.jsx +2 -1
package/bin/flowmind.js
CHANGED
|
@@ -15,23 +15,43 @@ const { execSync } = require('child_process');
|
|
|
15
15
|
const FlowMind = require('../core');
|
|
16
16
|
const HonorEngine = require('../core/honor-engine');
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Restore terminal to sane state (cancel raw mode, show cursor, reset colors)
|
|
20
|
+
*/
|
|
21
|
+
function restoreTerminal() {
|
|
22
|
+
try {
|
|
23
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
24
|
+
process.stdin.setRawMode(false);
|
|
25
|
+
}
|
|
26
|
+
} catch (e) { /* ignore */ }
|
|
27
|
+
// Show cursor, reset colors, clear scroll region
|
|
28
|
+
process.stdout.write('\x1b[?25h\x1b[0m\x1b[r');
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
// Global error handlers to prevent silent CLI crashes
|
|
19
32
|
process.on('uncaughtException', (err) => {
|
|
20
33
|
console.error(chalk.red('\nUncaught Exception:'), err.message);
|
|
21
|
-
|
|
22
|
-
process.stdin.setRawMode(false);
|
|
23
|
-
}
|
|
34
|
+
restoreTerminal();
|
|
24
35
|
process.exit(1);
|
|
25
36
|
});
|
|
26
37
|
|
|
27
38
|
process.on('unhandledRejection', (reason) => {
|
|
28
39
|
console.error(chalk.red('\nUnhandled Rejection:'), reason?.message || reason);
|
|
29
|
-
|
|
30
|
-
process.stdin.setRawMode(false);
|
|
31
|
-
}
|
|
40
|
+
restoreTerminal();
|
|
32
41
|
process.exit(1);
|
|
33
42
|
});
|
|
34
43
|
|
|
44
|
+
// Handle SIGINT (Ctrl+C) to restore terminal state
|
|
45
|
+
process.on('SIGINT', () => {
|
|
46
|
+
restoreTerminal();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
process.on('SIGTERM', () => {
|
|
51
|
+
restoreTerminal();
|
|
52
|
+
process.exit(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
35
55
|
// Package info
|
|
36
56
|
const packageJson = require('../package.json');
|
|
37
57
|
|
|
@@ -506,6 +526,8 @@ program
|
|
|
506
526
|
}
|
|
507
527
|
} catch (error) {
|
|
508
528
|
console.error(chalk.red('Error:'), error.message);
|
|
529
|
+
} finally {
|
|
530
|
+
restoreTerminal();
|
|
509
531
|
}
|
|
510
532
|
});
|
|
511
533
|
|
|
@@ -889,35 +911,47 @@ async function runInteractiveMode(fm) {
|
|
|
889
911
|
showBanner();
|
|
890
912
|
console.log(chalk.cyan('Interactive mode started. Type "exit" to quit.\n'));
|
|
891
913
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
914
|
+
try {
|
|
915
|
+
while (true) {
|
|
916
|
+
let input;
|
|
917
|
+
try {
|
|
918
|
+
const answers = await inquirer.prompt([
|
|
919
|
+
{
|
|
920
|
+
type: 'input',
|
|
921
|
+
name: 'input',
|
|
922
|
+
message: chalk.green('You:'),
|
|
923
|
+
prefix: ''
|
|
924
|
+
}
|
|
925
|
+
]);
|
|
926
|
+
input = answers.input;
|
|
927
|
+
} catch (promptErr) {
|
|
928
|
+
// inquirer throws on SIGINT (Ctrl+C)
|
|
929
|
+
console.log(chalk.cyan('\nGoodbye! 👋\n'));
|
|
930
|
+
break;
|
|
899
931
|
}
|
|
900
|
-
]);
|
|
901
932
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
933
|
+
if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
|
|
934
|
+
console.log(chalk.cyan('\nGoodbye! FlowMind will remember your preferences. 👋\n'));
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
906
937
|
|
|
907
|
-
|
|
938
|
+
if (!input.trim()) continue;
|
|
908
939
|
|
|
909
|
-
|
|
940
|
+
const spinner = ora('Thinking...').start();
|
|
910
941
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
942
|
+
try {
|
|
943
|
+
const result = await fm.process(input);
|
|
944
|
+
spinner.stop();
|
|
945
|
+
displayResult(result);
|
|
946
|
+
} catch (error) {
|
|
947
|
+
spinner.stop();
|
|
948
|
+
console.error(chalk.red('Error:'), error.message);
|
|
949
|
+
}
|
|
919
950
|
|
|
920
|
-
|
|
951
|
+
console.log(''); // Empty line for spacing
|
|
952
|
+
}
|
|
953
|
+
} finally {
|
|
954
|
+
restoreTerminal();
|
|
921
955
|
}
|
|
922
956
|
}
|
|
923
957
|
|
|
@@ -1351,6 +1385,7 @@ program
|
|
|
1351
1385
|
.description('Launch enhanced TUI with split panels, skill browser, and dragon display')
|
|
1352
1386
|
.action(async () => {
|
|
1353
1387
|
let stdinWrapper = null;
|
|
1388
|
+
let stdinForwarder = null;
|
|
1354
1389
|
try {
|
|
1355
1390
|
// Register .jsx extension for CJS
|
|
1356
1391
|
require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
|
|
@@ -1378,11 +1413,18 @@ program
|
|
|
1378
1413
|
}
|
|
1379
1414
|
return stdinWrapper;
|
|
1380
1415
|
};
|
|
1381
|
-
// Forward real stdin data to the wrapper
|
|
1416
|
+
// Forward real stdin data to the wrapper (store reference for cleanup)
|
|
1382
1417
|
if (realStdin.readable) {
|
|
1383
|
-
|
|
1384
|
-
if (!stdinWrapper.destroyed)
|
|
1385
|
-
|
|
1418
|
+
stdinForwarder = (chunk) => {
|
|
1419
|
+
if (!stdinWrapper.destroyed) {
|
|
1420
|
+
try {
|
|
1421
|
+
stdinWrapper.write(chunk);
|
|
1422
|
+
} catch (e) {
|
|
1423
|
+
// Ignore write-after-destroy errors
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
realStdin.on('data', stdinForwarder);
|
|
1386
1428
|
}
|
|
1387
1429
|
|
|
1388
1430
|
const { unmount, waitUntilExit } = render(
|
|
@@ -1397,9 +1439,14 @@ program
|
|
|
1397
1439
|
console.log(chalk.yellow('Try running: npm install ink@3 react ink-text-input ink-spinner'));
|
|
1398
1440
|
}
|
|
1399
1441
|
} finally {
|
|
1442
|
+
// Clean up stdin listener to prevent leak
|
|
1443
|
+
if (stdinForwarder) {
|
|
1444
|
+
process.stdin.removeListener('data', stdinForwarder);
|
|
1445
|
+
}
|
|
1400
1446
|
if (stdinWrapper && !stdinWrapper.destroyed) {
|
|
1401
1447
|
stdinWrapper.destroy();
|
|
1402
1448
|
}
|
|
1449
|
+
restoreTerminal();
|
|
1403
1450
|
}
|
|
1404
1451
|
});
|
|
1405
1452
|
|
|
@@ -1409,6 +1456,7 @@ program
|
|
|
1409
1456
|
.description('Launch real-time monitoring dashboard for MCP activity and events')
|
|
1410
1457
|
.action(async () => {
|
|
1411
1458
|
let stdinWrapper = null;
|
|
1459
|
+
let stdinForwarder = null;
|
|
1412
1460
|
try {
|
|
1413
1461
|
// Register .jsx extension for CJS
|
|
1414
1462
|
require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
|
|
@@ -1437,9 +1485,14 @@ program
|
|
|
1437
1485
|
return stdinWrapper;
|
|
1438
1486
|
};
|
|
1439
1487
|
if (realStdin.readable) {
|
|
1440
|
-
|
|
1441
|
-
if (!stdinWrapper.destroyed)
|
|
1442
|
-
|
|
1488
|
+
stdinForwarder = (chunk) => {
|
|
1489
|
+
if (!stdinWrapper.destroyed) {
|
|
1490
|
+
try {
|
|
1491
|
+
stdinWrapper.write(chunk);
|
|
1492
|
+
} catch (e) { /* ignore write-after-destroy */ }
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
realStdin.on('data', stdinForwarder);
|
|
1443
1496
|
}
|
|
1444
1497
|
|
|
1445
1498
|
const { unmount, waitUntilExit } = render(
|
|
@@ -1454,9 +1507,13 @@ program
|
|
|
1454
1507
|
console.log(chalk.yellow('Try running: npm install ink@3 react'));
|
|
1455
1508
|
}
|
|
1456
1509
|
} finally {
|
|
1510
|
+
if (stdinForwarder) {
|
|
1511
|
+
process.stdin.removeListener('data', stdinForwarder);
|
|
1512
|
+
}
|
|
1457
1513
|
if (stdinWrapper && !stdinWrapper.destroyed) {
|
|
1458
1514
|
stdinWrapper.destroy();
|
|
1459
1515
|
}
|
|
1516
|
+
restoreTerminal();
|
|
1460
1517
|
}
|
|
1461
1518
|
});
|
|
1462
1519
|
|
|
@@ -1592,6 +1649,8 @@ if (!process.argv.slice(2).length) {
|
|
|
1592
1649
|
await runInteractiveMode(fm);
|
|
1593
1650
|
} catch (error) {
|
|
1594
1651
|
console.error(chalk.red('Error:'), error.message);
|
|
1652
|
+
} finally {
|
|
1653
|
+
restoreTerminal();
|
|
1595
1654
|
}
|
|
1596
1655
|
})();
|
|
1597
1656
|
}
|
package/package.json
CHANGED
package/tui/app.jsx
CHANGED
|
@@ -8,6 +8,7 @@ const StatusBar = require('./components/StatusBar.jsx');
|
|
|
8
8
|
function App({ flowmind }) {
|
|
9
9
|
const [results, setResults] = React.useState([]);
|
|
10
10
|
const [isProcessing, setIsProcessing] = React.useState(false);
|
|
11
|
+
const [focusPanel, setFocusPanel] = React.useState('chat'); // 'chat' | 'sidebar'
|
|
11
12
|
const mountedRef = React.useRef(true);
|
|
12
13
|
const { exit } = useApp();
|
|
13
14
|
|
|
@@ -15,11 +16,16 @@ function App({ flowmind }) {
|
|
|
15
16
|
return () => { mountedRef.current = false; };
|
|
16
17
|
}, []);
|
|
17
18
|
|
|
19
|
+
// Ctrl+C always exits; Tab switches focus between panels
|
|
18
20
|
useInput((input, key) => {
|
|
19
21
|
if (key.ctrl && input === 'c') exit();
|
|
22
|
+
if (key.tab) {
|
|
23
|
+
setFocusPanel(prev => prev === 'chat' ? 'sidebar' : 'chat');
|
|
24
|
+
}
|
|
20
25
|
});
|
|
21
26
|
|
|
22
27
|
const handleCommand = React.useCallback(async (input, addResponse) => {
|
|
28
|
+
if (!mountedRef.current) return;
|
|
23
29
|
setIsProcessing(true);
|
|
24
30
|
try {
|
|
25
31
|
const result = await flowmind.process(input);
|
|
@@ -41,6 +47,7 @@ function App({ flowmind }) {
|
|
|
41
47
|
}, [flowmind]);
|
|
42
48
|
|
|
43
49
|
const handleSkillSelect = React.useCallback((skill) => {
|
|
50
|
+
if (!mountedRef.current) return;
|
|
44
51
|
try {
|
|
45
52
|
setResults(prev => [...prev, {
|
|
46
53
|
type: 'result',
|
|
@@ -55,9 +62,9 @@ function App({ flowmind }) {
|
|
|
55
62
|
return (
|
|
56
63
|
React.createElement(Box, { flexDirection: 'column', width: '100%', height: '100%' },
|
|
57
64
|
React.createElement(Box, { flexDirection: 'row', flexGrow: 1 },
|
|
58
|
-
React.createElement(Sidebar, { flowmind: flowmind, width: 30, onSkillSelect: handleSkillSelect }),
|
|
65
|
+
React.createElement(Sidebar, { flowmind: flowmind, width: 30, onSkillSelect: handleSkillSelect, focused: focusPanel === 'sidebar' }),
|
|
59
66
|
React.createElement(Box, { flexDirection: 'column', width: '70%', flexGrow: 1 },
|
|
60
|
-
React.createElement(ChatPanel, { onSubmit: handleCommand, isProcessing: isProcessing, onExit: exit }),
|
|
67
|
+
React.createElement(ChatPanel, { onSubmit: handleCommand, isProcessing: isProcessing, onExit: exit, focused: focusPanel === 'chat' }),
|
|
61
68
|
React.createElement(ResultPanel, { results: results })
|
|
62
69
|
)
|
|
63
70
|
),
|
|
@@ -3,7 +3,7 @@ const { Box, Text, useInput } = require('ink');
|
|
|
3
3
|
const TextInput = require('ink-text-input').default || require('ink-text-input');
|
|
4
4
|
const Spinner = require('ink-spinner').default || require('ink-spinner');
|
|
5
5
|
|
|
6
|
-
function ChatPanel({ onSubmit, isProcessing, onExit }) {
|
|
6
|
+
function ChatPanel({ onSubmit, isProcessing, onExit, focused }) {
|
|
7
7
|
const [input, setInput] = React.useState('');
|
|
8
8
|
const [history, setHistory] = React.useState([]);
|
|
9
9
|
const [cmdHistory, setCmdHistory] = React.useState([]);
|
|
@@ -15,9 +15,9 @@ function ChatPanel({ onSubmit, isProcessing, onExit }) {
|
|
|
15
15
|
return () => { mountedRef.current = false; };
|
|
16
16
|
}, []);
|
|
17
17
|
|
|
18
|
-
// Handle Up/Down arrow for command history
|
|
18
|
+
// Handle Up/Down arrow for command history (only when focused)
|
|
19
19
|
useInput((ch, key) => {
|
|
20
|
-
if (isProcessing) return;
|
|
20
|
+
if (!focused || isProcessing) return;
|
|
21
21
|
|
|
22
22
|
if (key.upArrow && cmdHistory.length > 0) {
|
|
23
23
|
const newIndex = historyIndex === -1
|
|
@@ -2,7 +2,7 @@ const React = require('react');
|
|
|
2
2
|
const { Box, Text, useInput } = require('ink');
|
|
3
3
|
const DragonTotem = require('./DragonTotem.jsx');
|
|
4
4
|
|
|
5
|
-
function Sidebar({ flowmind, width, onSkillSelect }) {
|
|
5
|
+
function Sidebar({ flowmind, width, onSkillSelect, focused }) {
|
|
6
6
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
7
7
|
const [skills, setSkills] = React.useState([]);
|
|
8
8
|
const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
|
|
@@ -24,6 +24,7 @@ function Sidebar({ flowmind, width, onSkillSelect }) {
|
|
|
24
24
|
}, [flowmind]);
|
|
25
25
|
|
|
26
26
|
useInput((input, key) => {
|
|
27
|
+
if (!focused) return; // Ignore input when sidebar is not focused
|
|
27
28
|
if (key.upArrow) setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
28
29
|
else if (key.downArrow) setSelectedIndex(prev => Math.min(skills.length - 1, prev + 1));
|
|
29
30
|
else if (key.return && skills[selectedIndex] && onSkillSelect) onSkillSelect(skills[selectedIndex]);
|