flowmind 1.5.3 → 1.5.6
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/CHANGELOG.md +31 -1
- package/README.md +2 -0
- package/README_CN.md +2 -0
- package/bin/flowmind.js +104 -118
- package/core/adapters/mcp-adapter.js +9 -8
- package/core/ai/providers/mimo.js +1 -1
- package/core/cli-ink.js +79 -0
- package/core/index.js +139 -1
- package/core/log-query-parser.js +324 -0
- package/core/providers/aliyun/dms-adapter.js +7 -35
- package/core/providers/aliyun/redis-adapter.js +4 -20
- package/core/providers/aliyun/sls-adapter.js +3 -10
- package/core/providers/friday/report-adapter.js +5 -25
- package/core/providers/yapi/yapi-adapter.js +6 -30
- package/core/providers/yuque/yuque-adapter.js +7 -35
- package/core/update-notifier.js +74 -0
- package/dashboard/app.jsx +69 -10
- package/dashboard/components/ActivityFeed.jsx +5 -4
- package/dashboard/components/DragonPanel.jsx +17 -1
- package/dashboard/components/McpStatusBar.jsx +19 -1
- package/dashboard/components/StatsRow.jsx +27 -2
- package/package.json +2 -1
- package/scripts/check-update.js +52 -0
- package/skills/auto-flow/index.js +451 -122
- package/skills/log-audit/index.js +146 -25
- package/skills/sls-log-audit/index.js +7 -30
- package/skills/yapi-sync-interface/index.js +146 -13
- package/skills/yuque-sync-design/index.js +132 -13
- package/tui/app.jsx +86 -9
- package/tui/components/ChatPanel.jsx +10 -7
- package/tui/components/DragonTotem.jsx +12 -1
- package/tui/components/Sidebar.jsx +19 -7
- package/tui/components/StatusBar.jsx +28 -1
- package/tui/format-result.js +60 -0
- package/tui/layout.js +60 -0
package/tui/app.jsx
CHANGED
|
@@ -1,23 +1,78 @@
|
|
|
1
1
|
const React = require('react');
|
|
2
|
-
const { Box, useApp, useInput } = require('ink');
|
|
2
|
+
const { Box, Text, useApp, useInput } = require('ink');
|
|
3
3
|
const Sidebar = require('./components/Sidebar.jsx');
|
|
4
4
|
const ChatPanel = require('./components/ChatPanel.jsx');
|
|
5
5
|
const StatusBar = require('./components/StatusBar.jsx');
|
|
6
6
|
const { formatResultText } = require('./format-result');
|
|
7
|
+
const { getTuiLayout, MIN_COLUMNS, MIN_ROWS } = require('./layout');
|
|
8
|
+
|
|
9
|
+
class TuiErrorBoundary extends React.Component {
|
|
10
|
+
constructor(props) {
|
|
11
|
+
super(props);
|
|
12
|
+
this.state = { error: null };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static getDerivedStateFromError(error) {
|
|
16
|
+
return { error };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
componentDidCatch(error) {
|
|
20
|
+
if (this.props.onError) {
|
|
21
|
+
this.props.onError(error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
render() {
|
|
26
|
+
if (this.state.error) {
|
|
27
|
+
return React.createElement(
|
|
28
|
+
Box,
|
|
29
|
+
{ flexDirection: 'column', borderStyle: 'single', borderColor: 'red', paddingX: 1, paddingY: 1 },
|
|
30
|
+
React.createElement(Text, { color: 'red', bold: true }, 'TUI render error'),
|
|
31
|
+
React.createElement(Text, { color: 'gray' }, this.state.error.message || 'Unknown render failure'),
|
|
32
|
+
React.createElement(Text, { color: 'gray' }, 'Resize the terminal or restart FlowMind.')
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return this.props.children;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
7
39
|
|
|
8
40
|
function App({ flowmind, asciiMode = false }) {
|
|
9
41
|
const MAX_MESSAGES = 60;
|
|
10
42
|
const [messages, setMessages] = React.useState([]);
|
|
11
43
|
const [isProcessing, setIsProcessing] = React.useState(false);
|
|
12
44
|
const [focusPanel, setFocusPanel] = React.useState('chat'); // 'chat' | 'sidebar'
|
|
45
|
+
const [terminalSize, setTerminalSize] = React.useState(() => ({
|
|
46
|
+
columns: Number(process.stdout?.columns) || 0,
|
|
47
|
+
rows: Number(process.stdout?.rows) || 0
|
|
48
|
+
}));
|
|
13
49
|
const mountedRef = React.useRef(true);
|
|
14
50
|
const messageIdRef = React.useRef(0);
|
|
51
|
+
const commandIdRef = React.useRef(0);
|
|
52
|
+
const activeCommandRef = React.useRef(false);
|
|
15
53
|
const { exit } = useApp();
|
|
16
54
|
|
|
17
55
|
React.useEffect(() => {
|
|
18
56
|
return () => { mountedRef.current = false; };
|
|
19
57
|
}, []);
|
|
20
58
|
|
|
59
|
+
React.useEffect(() => {
|
|
60
|
+
const updateSize = () => {
|
|
61
|
+
if (!mountedRef.current) return;
|
|
62
|
+
setTerminalSize({
|
|
63
|
+
columns: Number(process.stdout?.columns) || 0,
|
|
64
|
+
rows: Number(process.stdout?.rows) || 0
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
updateSize();
|
|
69
|
+
process.stdout?.on?.('resize', updateSize);
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
process.stdout?.removeListener?.('resize', updateSize);
|
|
73
|
+
};
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
21
76
|
const appendMessage = React.useCallback((role, text, metadata = {}) => {
|
|
22
77
|
const id = ++messageIdRef.current;
|
|
23
78
|
setMessages(prev => [...prev, { id, role, text, metadata }].slice(-MAX_MESSAGES));
|
|
@@ -32,13 +87,15 @@ function App({ flowmind, asciiMode = false }) {
|
|
|
32
87
|
});
|
|
33
88
|
|
|
34
89
|
const handleCommand = React.useCallback(async (input) => {
|
|
35
|
-
if (!mountedRef.current) return;
|
|
90
|
+
if (!mountedRef.current || activeCommandRef.current) return;
|
|
91
|
+
activeCommandRef.current = true;
|
|
92
|
+
const commandId = ++commandIdRef.current;
|
|
36
93
|
appendMessage('user', input);
|
|
37
94
|
setIsProcessing(true);
|
|
38
95
|
|
|
39
96
|
try {
|
|
40
97
|
const result = await flowmind.process(input);
|
|
41
|
-
if (!mountedRef.current) return;
|
|
98
|
+
if (!mountedRef.current || commandId !== commandIdRef.current) return;
|
|
42
99
|
|
|
43
100
|
appendMessage('flowmind', formatResultText(result), {
|
|
44
101
|
type: result.type,
|
|
@@ -47,9 +104,14 @@ function App({ flowmind, asciiMode = false }) {
|
|
|
47
104
|
scene: result.metadata?.sceneMatch?.scene?.name
|
|
48
105
|
});
|
|
49
106
|
} catch (e) {
|
|
50
|
-
if (mountedRef.current
|
|
107
|
+
if (mountedRef.current && commandId === commandIdRef.current) {
|
|
108
|
+
appendMessage('flowmind', 'Error: ' + e.message, { type: 'error' });
|
|
109
|
+
}
|
|
51
110
|
} finally {
|
|
52
|
-
if (mountedRef.current)
|
|
111
|
+
if (mountedRef.current && commandId === commandIdRef.current) {
|
|
112
|
+
setIsProcessing(false);
|
|
113
|
+
}
|
|
114
|
+
activeCommandRef.current = false;
|
|
53
115
|
}
|
|
54
116
|
}, [appendMessage, flowmind]);
|
|
55
117
|
|
|
@@ -72,20 +134,35 @@ function App({ flowmind, asciiMode = false }) {
|
|
|
72
134
|
}
|
|
73
135
|
}, [appendMessage]);
|
|
74
136
|
|
|
137
|
+
const layout = getTuiLayout(terminalSize.columns, terminalSize.rows);
|
|
138
|
+
|
|
139
|
+
if (layout.tooSmall) {
|
|
140
|
+
return (
|
|
141
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'red', paddingX: 1, paddingY: 1 },
|
|
142
|
+
React.createElement(Text, { bold: true, color: 'red' }, 'Terminal too small'),
|
|
143
|
+
React.createElement(Text, { color: 'gray' }, `Need at least ${MIN_COLUMNS}x${MIN_ROWS}. Current size: ${layout.columns}x${layout.rows}.`),
|
|
144
|
+
React.createElement(Text, { color: 'gray' }, 'Resize the terminal to keep the TUI stable.')
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
75
149
|
return (
|
|
76
|
-
|
|
150
|
+
React.createElement(TuiErrorBoundary, null,
|
|
151
|
+
React.createElement(Box, { flexDirection: 'column', width: '100%', height: '100%' },
|
|
77
152
|
React.createElement(Box, { flexDirection: 'row', flexGrow: 1 },
|
|
78
|
-
React.createElement(Sidebar, { flowmind: flowmind, width:
|
|
153
|
+
React.createElement(Sidebar, { flowmind: flowmind, width: layout.sidebarWidth, onSkillSelect: handleSkillSelect, focused: focusPanel === 'sidebar', asciiMode: asciiMode }),
|
|
79
154
|
React.createElement(ChatPanel, {
|
|
80
155
|
messages,
|
|
81
156
|
onSubmit: handleCommand,
|
|
82
157
|
isProcessing: isProcessing,
|
|
83
158
|
onExit: exit,
|
|
84
159
|
focused: focusPanel === 'chat',
|
|
85
|
-
asciiMode: asciiMode
|
|
160
|
+
asciiMode: asciiMode,
|
|
161
|
+
width: layout.chatWidth
|
|
86
162
|
})
|
|
87
163
|
),
|
|
88
|
-
React.createElement(StatusBar, { flowmind: flowmind, focusPanel: focusPanel, asciiMode: asciiMode })
|
|
164
|
+
React.createElement(StatusBar, { flowmind: flowmind, focusPanel: focusPanel, asciiMode: asciiMode, width: layout.columns })
|
|
165
|
+
)
|
|
89
166
|
)
|
|
90
167
|
);
|
|
91
168
|
}
|
|
@@ -4,11 +4,12 @@ const TextInput = require('ink-text-input').default || require('ink-text-input')
|
|
|
4
4
|
const Spinner = require('ink-spinner').default || require('ink-spinner');
|
|
5
5
|
const { getBorderStyle, isExitCommand } = require('../ui');
|
|
6
6
|
|
|
7
|
-
function ChatPanel({ messages, onSubmit, isProcessing, onExit, focused, asciiMode = false }) {
|
|
7
|
+
function ChatPanel({ messages, onSubmit, isProcessing, onExit, focused, asciiMode = false, width = 0 }) {
|
|
8
8
|
const [input, setInput] = React.useState('');
|
|
9
9
|
const [cmdHistory, setCmdHistory] = React.useState([]);
|
|
10
10
|
const [historyIndex, setHistoryIndex] = React.useState(-1);
|
|
11
11
|
const [savedInput, setSavedInput] = React.useState('');
|
|
12
|
+
const isCompact = width < 60;
|
|
12
13
|
|
|
13
14
|
// Handle Up/Down arrow for command history (only when focused)
|
|
14
15
|
useInput((ch, key) => {
|
|
@@ -36,7 +37,7 @@ function ChatPanel({ messages, onSubmit, isProcessing, onExit, focused, asciiMod
|
|
|
36
37
|
|
|
37
38
|
const handleSubmit = (value) => {
|
|
38
39
|
const normalized = value.trim();
|
|
39
|
-
if (!normalized) return;
|
|
40
|
+
if (!normalized || isProcessing) return;
|
|
40
41
|
// Add to command history (deduplicate consecutive)
|
|
41
42
|
if (cmdHistory.length === 0 || cmdHistory[cmdHistory.length - 1] !== normalized) {
|
|
42
43
|
setCmdHistory(prev => [...prev, normalized]);
|
|
@@ -51,7 +52,7 @@ function ChatPanel({ messages, onSubmit, isProcessing, onExit, focused, asciiMod
|
|
|
51
52
|
onSubmit(normalized);
|
|
52
53
|
};
|
|
53
54
|
|
|
54
|
-
const displayMessages = messages.slice(-18);
|
|
55
|
+
const displayMessages = messages.slice(-(isCompact ? 8 : 18));
|
|
55
56
|
|
|
56
57
|
const renderMeta = (message) => {
|
|
57
58
|
const metaParts = [];
|
|
@@ -89,17 +90,17 @@ function ChatPanel({ messages, onSubmit, isProcessing, onExit, focused, asciiMod
|
|
|
89
90
|
return (
|
|
90
91
|
React.createElement(Box, { flexDirection: 'column', flexGrow: 1, borderStyle: getBorderStyle(asciiMode), borderColor: focused ? 'green' : 'gray', paddingX: 1 },
|
|
91
92
|
React.createElement(Text, { bold: true, color: focused ? 'green' : 'gray' }, focused ? 'FlowMind Chat [Focused]' : 'FlowMind Chat'),
|
|
92
|
-
React.createElement(Text, { color: 'gray' }, 'Single-pane conversation. Output stays in the same view.'),
|
|
93
|
+
React.createElement(Text, { color: 'gray' }, isCompact ? 'Compact chat mode.' : 'Single-pane conversation. Output stays in the same view.'),
|
|
93
94
|
React.createElement(Box, { flexDirection: 'column', flexGrow: 1, marginTop: 1, overflow: 'hidden' },
|
|
94
95
|
displayMessages.length === 0 && React.createElement(Box, { flexDirection: 'column' },
|
|
95
96
|
React.createElement(Text, { color: 'gray' }, 'Describe what you want FlowMind to do.'),
|
|
96
|
-
React.createElement(Text, { color: 'gray' }, 'The dialog history stays in this panel, with the input fixed at the bottom.'),
|
|
97
|
+
!isCompact && React.createElement(Text, { color: 'gray' }, 'The dialog history stays in this panel, with the input fixed at the bottom.'),
|
|
97
98
|
React.createElement(Text, { color: 'gray' }, 'Type `exit` or `/exit` to quit, or press Ctrl+C.')
|
|
98
99
|
),
|
|
99
100
|
displayMessages.map(renderMessage)
|
|
100
101
|
),
|
|
101
102
|
React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: focused ? 'green' : 'gray', paddingX: 1, marginTop: 1 },
|
|
102
|
-
React.createElement(Text, { color: 'gray' }, 'Enter send Up/Down history Tab sidebar exit /exit :q Ctrl+C'),
|
|
103
|
+
React.createElement(Text, { color: 'gray' }, isCompact ? 'Enter send | Tab switch | Ctrl+C' : 'Enter send Up/Down history Tab sidebar exit /exit :q Ctrl+C'),
|
|
103
104
|
isProcessing
|
|
104
105
|
? React.createElement(Text, { color: 'yellow' },
|
|
105
106
|
React.createElement(Spinner, { type: 'dots' }),
|
|
@@ -113,7 +114,9 @@ function ChatPanel({ messages, onSubmit, isProcessing, onExit, focused, asciiMod
|
|
|
113
114
|
onSubmit: handleSubmit,
|
|
114
115
|
focus: focused,
|
|
115
116
|
showCursor: focused,
|
|
116
|
-
placeholder: focused
|
|
117
|
+
placeholder: focused
|
|
118
|
+
? (isCompact ? 'Ask FlowMind...' : 'Ask FlowMind to do something...')
|
|
119
|
+
: 'Press Tab to focus input'
|
|
117
120
|
})
|
|
118
121
|
)
|
|
119
122
|
)
|
|
@@ -2,7 +2,7 @@ const React = require('react');
|
|
|
2
2
|
const { Box, Text } = require('ink');
|
|
3
3
|
const { LEVEL_COLORS, LEVEL_NAMES, LEVEL_STATES, getDragonArt } = require('../ui');
|
|
4
4
|
|
|
5
|
-
function DragonTotem({ honorData, compact, asciiMode = false }) {
|
|
5
|
+
function DragonTotem({ honorData, compact, asciiMode = false, width = 0 }) {
|
|
6
6
|
const points = honorData?.points || 0;
|
|
7
7
|
const level = honorData?.level || 0;
|
|
8
8
|
const art = getDragonArt(level, { asciiMode, compact });
|
|
@@ -12,6 +12,17 @@ function DragonTotem({ honorData, compact, asciiMode = false }) {
|
|
|
12
12
|
const nextLevelPoints = [1, 10, 30, 60, 100];
|
|
13
13
|
const nextPoints = nextLevelPoints[level] || null;
|
|
14
14
|
const pointsToNext = nextPoints !== null ? nextPoints - points : 0;
|
|
15
|
+
const superCompact = width > 0 && width < 26;
|
|
16
|
+
|
|
17
|
+
if (superCompact) {
|
|
18
|
+
return (
|
|
19
|
+
React.createElement(Box, { flexDirection: 'column', paddingX: 1 },
|
|
20
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'Dragon'),
|
|
21
|
+
React.createElement(Text, { color: color }, 'Lv' + level + ' ' + levelName),
|
|
22
|
+
React.createElement(Text, { color: 'gray' }, points + ' pts')
|
|
23
|
+
)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
15
26
|
|
|
16
27
|
return (
|
|
17
28
|
React.createElement(Box, { flexDirection: 'column', paddingX: 1 },
|
|
@@ -7,6 +7,7 @@ function Sidebar({ flowmind, width, onSkillSelect, focused, asciiMode = false })
|
|
|
7
7
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
8
8
|
const [skills, setSkills] = React.useState([]);
|
|
9
9
|
const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
|
|
10
|
+
const isCompact = width < 30;
|
|
10
11
|
|
|
11
12
|
React.useEffect(() => {
|
|
12
13
|
if (flowmind) {
|
|
@@ -24,27 +25,38 @@ function Sidebar({ flowmind, width, onSkillSelect, focused, asciiMode = false })
|
|
|
24
25
|
}
|
|
25
26
|
}, [flowmind]);
|
|
26
27
|
|
|
28
|
+
React.useEffect(() => {
|
|
29
|
+
if (skills.length === 0) {
|
|
30
|
+
setSelectedIndex(0);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (selectedIndex >= skills.length) {
|
|
34
|
+
setSelectedIndex(skills.length - 1);
|
|
35
|
+
}
|
|
36
|
+
}, [selectedIndex, skills.length]);
|
|
37
|
+
|
|
27
38
|
useInput((input, key) => {
|
|
28
39
|
if (!focused) return; // Ignore input when sidebar is not focused
|
|
40
|
+
if (skills.length === 0) return;
|
|
29
41
|
if (key.upArrow) setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
30
42
|
else if (key.downArrow) setSelectedIndex(prev => Math.min(skills.length - 1, prev + 1));
|
|
31
43
|
else if (key.return && skills[selectedIndex] && onSkillSelect) onSkillSelect(skills[selectedIndex]);
|
|
32
44
|
});
|
|
33
45
|
|
|
34
|
-
const barWidth = Math.max(
|
|
46
|
+
const barWidth = Math.max(8, width - 4);
|
|
35
47
|
const progress = honorData.points > 0 ? Math.min(1, honorData.points / 100) : 0;
|
|
36
48
|
const progressBar = getProgressBar(barWidth, progress, asciiMode);
|
|
37
49
|
|
|
38
50
|
return (
|
|
39
51
|
React.createElement(Box, { flexDirection: 'column', width: width, borderStyle: getBorderStyle(asciiMode), borderColor: focused ? 'cyan' : 'gray', paddingX: 1 },
|
|
40
52
|
React.createElement(Text, { color: focused ? 'cyan' : 'gray' }, focused ? 'Sidebar [Focused]' : 'Sidebar'),
|
|
41
|
-
React.createElement(DragonTotem, { honorData: honorData, compact: true, asciiMode: asciiMode }),
|
|
53
|
+
!isCompact && React.createElement(DragonTotem, { honorData: honorData, compact: true, asciiMode: asciiMode, width: width }),
|
|
42
54
|
React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
43
55
|
React.createElement(Text, { bold: true, color: 'cyan' }, 'Progress'),
|
|
44
56
|
React.createElement(Text, { color: 'green' }, progressBar),
|
|
45
57
|
React.createElement(Text, { color: 'gray' }, honorData.points + '/100 pts')
|
|
46
58
|
),
|
|
47
|
-
React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
59
|
+
!isCompact && React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
48
60
|
React.createElement(Text, { bold: true, color: 'cyan' }, 'Skills (' + skills.length + ')'),
|
|
49
61
|
React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
50
62
|
skills.length === 0 && React.createElement(Text, { color: 'gray' }, 'No skills loaded'),
|
|
@@ -60,14 +72,14 @@ function Sidebar({ flowmind, width, onSkillSelect, focused, asciiMode = false })
|
|
|
60
72
|
)
|
|
61
73
|
),
|
|
62
74
|
React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
63
|
-
React.createElement(Text, { color: 'gray' }, 'Tab: switch focus'),
|
|
64
|
-
React.createElement(Text, { color: 'gray' }, 'Enter: inspect in chat')
|
|
75
|
+
React.createElement(Text, { color: 'gray' }, isCompact ? 'Tab: switch focus' : 'Tab: switch focus'),
|
|
76
|
+
!isCompact && React.createElement(Text, { color: 'gray' }, 'Enter: inspect in chat')
|
|
65
77
|
),
|
|
66
78
|
React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
67
79
|
React.createElement(Text, { bold: true, color: 'cyan' }, 'Stats'),
|
|
68
80
|
React.createElement(Text, { color: 'gray' }, 'Skills used: ' + (honorData.stats?.skillUseCount || 0)),
|
|
69
|
-
React.createElement(Text, { color: 'gray' }, 'Learnings: ' + (honorData.stats?.learningCount || 0)),
|
|
70
|
-
React.createElement(Text, { color: 'gray' }, 'New skills: ' + (honorData.stats?.newSkillCount || 0))
|
|
81
|
+
!isCompact && React.createElement(Text, { color: 'gray' }, 'Learnings: ' + (honorData.stats?.learningCount || 0)),
|
|
82
|
+
!isCompact && React.createElement(Text, { color: 'gray' }, 'New skills: ' + (honorData.stats?.newSkillCount || 0))
|
|
71
83
|
)
|
|
72
84
|
)
|
|
73
85
|
);
|
|
@@ -2,10 +2,11 @@ const React = require('react');
|
|
|
2
2
|
const { Box, Text } = require('ink');
|
|
3
3
|
const { LEVEL_NAMES, getBorderStyle, getStatusDot } = require('../ui');
|
|
4
4
|
|
|
5
|
-
function StatusBar({ flowmind, focusPanel, asciiMode = false }) {
|
|
5
|
+
function StatusBar({ flowmind, focusPanel, asciiMode = false, width = 0 }) {
|
|
6
6
|
const [aiStatus, setAiStatus] = React.useState(null);
|
|
7
7
|
const [componentStatus, setComponentStatus] = React.useState(null);
|
|
8
8
|
const [honorData, setHonorData] = React.useState(null);
|
|
9
|
+
const isCompact = width < 100;
|
|
9
10
|
|
|
10
11
|
React.useEffect(() => {
|
|
11
12
|
if (flowmind) {
|
|
@@ -24,6 +25,32 @@ function StatusBar({ flowmind, focusPanel, asciiMode = false }) {
|
|
|
24
25
|
const level = honorData?.level || 0;
|
|
25
26
|
const points = honorData?.points || 0;
|
|
26
27
|
|
|
28
|
+
if (isCompact) {
|
|
29
|
+
return (
|
|
30
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: getBorderStyle(asciiMode), borderColor: 'gray', paddingX: 1 },
|
|
31
|
+
React.createElement(Text, null,
|
|
32
|
+
React.createElement(Text, { color: 'gray' }, 'AI: '),
|
|
33
|
+
React.createElement(Text, { color: aiOk ? 'green' : 'red' }, aiName),
|
|
34
|
+
React.createElement(Text, { color: aiOk ? 'green' : 'red' }, getStatusDot(aiOk, asciiMode))
|
|
35
|
+
),
|
|
36
|
+
React.createElement(Text, null,
|
|
37
|
+
React.createElement(Text, { color: 'gray' }, 'Components: '),
|
|
38
|
+
React.createElement(Text, { color: 'white' }, activeCount + '/' + componentCount),
|
|
39
|
+
React.createElement(Text, { color: 'green' }, ' active')
|
|
40
|
+
),
|
|
41
|
+
React.createElement(Text, null,
|
|
42
|
+
React.createElement(Text, { color: 'gray' }, 'Honor: '),
|
|
43
|
+
React.createElement(Text, { color: 'yellow' }, LEVEL_NAMES[level]),
|
|
44
|
+
React.createElement(Text, { color: 'gray' }, ' (' + points + ' pts)')
|
|
45
|
+
),
|
|
46
|
+
React.createElement(Text, null,
|
|
47
|
+
React.createElement(Text, { color: 'gray' }, 'Focus: '),
|
|
48
|
+
React.createElement(Text, { color: focusPanel === 'chat' ? 'green' : 'cyan' }, focusPanel || 'chat')
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
return (
|
|
28
55
|
React.createElement(Box, { borderStyle: getBorderStyle(asciiMode), borderColor: 'gray', paddingX: 1, justifyContent: 'space-between' },
|
|
29
56
|
React.createElement(Text, null,
|
package/tui/format-result.js
CHANGED
|
@@ -176,6 +176,10 @@ function formatResultText(result) {
|
|
|
176
176
|
return payload === undefined ? '' : formatScalar(payload);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
if (getSkillName(payload) === 'log-audit') {
|
|
180
|
+
return formatLogAuditResult(payload);
|
|
181
|
+
}
|
|
182
|
+
|
|
179
183
|
const message = payload.message;
|
|
180
184
|
const details = pruneDisplayValue(payload.data);
|
|
181
185
|
const detailLines = renderValueLines(details);
|
|
@@ -195,6 +199,62 @@ function formatResultText(result) {
|
|
|
195
199
|
return truncateRenderedText(JSON.stringify(payload, null, 2));
|
|
196
200
|
}
|
|
197
201
|
|
|
202
|
+
function getSkillName(payload) {
|
|
203
|
+
return payload?.skill || payload?.data?.skill || null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function formatLogAuditResult(payload) {
|
|
207
|
+
const data = isPlainObject(payload.data) ? payload.data : {};
|
|
208
|
+
const execution = isPlainObject(data.execution) ? data.execution : {};
|
|
209
|
+
const rawSummary = isPlainObject(execution.rawSummary) ? execution.rawSummary : null;
|
|
210
|
+
const executionDisplay = rawSummary
|
|
211
|
+
? Object.fromEntries(Object.entries(execution).filter(([key]) => key !== 'rawSummary'))
|
|
212
|
+
: execution;
|
|
213
|
+
|
|
214
|
+
const sections = [];
|
|
215
|
+
if (payload.message) {
|
|
216
|
+
sections.push(payload.message);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (data.noRecords === true) {
|
|
220
|
+
sections.push('狀態:未命中日誌');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (isPlainObject(data.queryParams)) {
|
|
224
|
+
const queryLines = renderValueLines(data.queryParams);
|
|
225
|
+
if (queryLines.length > 0) {
|
|
226
|
+
sections.push(['查詢條件', ...queryLines].join('\n'));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (isPlainObject(executionDisplay) && Object.keys(executionDisplay).length > 0) {
|
|
231
|
+
const executionLines = renderValueLines(executionDisplay);
|
|
232
|
+
if (executionLines.length > 0) {
|
|
233
|
+
sections.push(['執行結果', ...executionLines].join('\n'));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (rawSummary) {
|
|
238
|
+
const rawLines = renderValueLines(rawSummary);
|
|
239
|
+
if (rawLines.length > 0) {
|
|
240
|
+
sections.push(['原始返回', ...rawLines].join('\n'));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (sections.length > 0) {
|
|
245
|
+
return sections.join('\n\n');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const details = pruneDisplayValue(payload.data);
|
|
249
|
+
const detailLines = renderValueLines(details);
|
|
250
|
+
|
|
251
|
+
if (detailLines.length > 0) {
|
|
252
|
+
return truncateRenderedText(detailLines.join('\n'));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return truncateRenderedText(JSON.stringify(payload, null, 2));
|
|
256
|
+
}
|
|
257
|
+
|
|
198
258
|
module.exports = {
|
|
199
259
|
formatResultText,
|
|
200
260
|
pruneDisplayValue,
|
package/tui/layout.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const MIN_COLUMNS = 88;
|
|
2
|
+
const MIN_ROWS = 20;
|
|
3
|
+
const DEFAULT_SIDEBAR_WIDTH = 32;
|
|
4
|
+
const MIN_SIDEBAR_WIDTH = 24;
|
|
5
|
+
const MAX_SIDEBAR_WIDTH = 40;
|
|
6
|
+
const MIN_CHAT_WIDTH = 28;
|
|
7
|
+
|
|
8
|
+
function normalizeDimension(value) {
|
|
9
|
+
const numeric = Number(value);
|
|
10
|
+
if (!Number.isFinite(numeric) || numeric < 0) {
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
return Math.floor(numeric);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getTuiLayout(columns, rows) {
|
|
17
|
+
const safeColumns = normalizeDimension(columns);
|
|
18
|
+
const safeRows = normalizeDimension(rows);
|
|
19
|
+
const tooSmall = safeColumns < MIN_COLUMNS || safeRows < MIN_ROWS;
|
|
20
|
+
|
|
21
|
+
if (tooSmall) {
|
|
22
|
+
return {
|
|
23
|
+
columns: safeColumns,
|
|
24
|
+
rows: safeRows,
|
|
25
|
+
tooSmall: true,
|
|
26
|
+
sidebarWidth: Math.min(DEFAULT_SIDEBAR_WIDTH, safeColumns),
|
|
27
|
+
chatWidth: Math.max(0, safeColumns - DEFAULT_SIDEBAR_WIDTH - 2)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sidebarWidth = clamp(
|
|
32
|
+
Math.round(safeColumns * 0.28),
|
|
33
|
+
MIN_SIDEBAR_WIDTH,
|
|
34
|
+
Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, safeColumns - MIN_CHAT_WIDTH - 2))
|
|
35
|
+
);
|
|
36
|
+
const chatWidth = Math.max(MIN_CHAT_WIDTH, safeColumns - sidebarWidth - 2);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
columns: safeColumns,
|
|
40
|
+
rows: safeRows,
|
|
41
|
+
tooSmall: false,
|
|
42
|
+
sidebarWidth,
|
|
43
|
+
chatWidth
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function clamp(value, min, max) {
|
|
48
|
+
return Math.max(min, Math.min(max, value));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
DEFAULT_SIDEBAR_WIDTH,
|
|
53
|
+
MAX_SIDEBAR_WIDTH,
|
|
54
|
+
MIN_CHAT_WIDTH,
|
|
55
|
+
MIN_COLUMNS,
|
|
56
|
+
MIN_ROWS,
|
|
57
|
+
MIN_SIDEBAR_WIDTH,
|
|
58
|
+
getTuiLayout,
|
|
59
|
+
normalizeDimension
|
|
60
|
+
};
|