flowmind 1.1.0 → 1.2.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.
@@ -0,0 +1,65 @@
1
+ const React = require('react');
2
+ const { Box, Text } = require('ink');
3
+
4
+ const LEVEL_NAMES = ['Egg', 'Hatchling', 'Juvenile', 'Adult', 'Elder', 'Ascended'];
5
+
6
+ function StatsRow({ flowmind }) {
7
+ const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
8
+ const [learningStats, setLearningStats] = React.useState({ totalRecords: 0, byType: {} });
9
+ const [aiStatus, setAiStatus] = React.useState({ initialized: false, defaultProvider: 'none' });
10
+
11
+ React.useEffect(() => {
12
+ if (!flowmind) return;
13
+ const refresh = () => {
14
+ try { setHonorData(flowmind.getHonorData()); } catch (e) { /* ignore */ }
15
+ try { flowmind.getStats().then(s => setLearningStats(s)); } catch (e) { /* ignore */ }
16
+ try { setAiStatus(flowmind.getAIStatus()); } catch (e) { /* ignore */ }
17
+ };
18
+ refresh();
19
+ const interval = setInterval(refresh, 5000);
20
+ return () => clearInterval(interval);
21
+ }, [flowmind]);
22
+
23
+ const barWidth = 16;
24
+ const progress = honorData.points > 0 ? Math.min(1, honorData.points / 100) : 0;
25
+ const filled = Math.round(progress * barWidth);
26
+ const progressBar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
27
+
28
+ return (
29
+ React.createElement(Box, { flexDirection: 'row' },
30
+ React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'yellow', paddingX: 1, width: '33%' },
31
+ React.createElement(Text, { bold: true, color: 'yellow' }, 'Honor'),
32
+ React.createElement(Text, null,
33
+ React.createElement(Text, { color: 'yellow' }, LEVEL_NAMES[honorData.level] || 'Egg'),
34
+ React.createElement(Text, { color: 'gray' }, ' Lv' + honorData.level)
35
+ ),
36
+ React.createElement(Text, { color: 'green' }, progressBar),
37
+ React.createElement(Text, { color: 'gray' }, honorData.points + '/100 pts')
38
+ ),
39
+ React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'cyan', paddingX: 1, width: '33%' },
40
+ React.createElement(Text, { bold: true, color: 'cyan' }, 'Learning'),
41
+ React.createElement(Text, null,
42
+ React.createElement(Text, { color: 'white' }, '' + (learningStats.totalRecords || 0)),
43
+ React.createElement(Text, { color: 'gray' }, ' records')
44
+ ),
45
+ Object.entries(learningStats.byType || {}).map(([type, count]) =>
46
+ React.createElement(Text, { key: type, color: 'gray' }, ' ' + type + ': ' + count)
47
+ ),
48
+ React.createElement(Text, null,
49
+ React.createElement(Text, { color: 'gray' }, 'AI: '),
50
+ React.createElement(Text, { color: aiStatus.initialized ? 'green' : 'red' }, aiStatus.initialized ? 'ok' : 'off')
51
+ )
52
+ ),
53
+ React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'blue', paddingX: 1, width: '33%' },
54
+ React.createElement(Text, { bold: true, color: 'blue' }, 'Components'),
55
+ React.createElement(Text, { color: 'gray' }, 'Registry loaded'),
56
+ React.createElement(Text, null,
57
+ React.createElement(Text, { color: 'gray' }, 'Provider: '),
58
+ React.createElement(Text, { color: aiStatus.initialized ? 'green' : 'red' }, aiStatus.defaultProvider || 'none')
59
+ )
60
+ )
61
+ )
62
+ );
63
+ }
64
+
65
+ module.exports = StatsRow;
package/mcp/server.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const FlowMind = require('../core');
9
+ const eventBus = require('../core/event-bus');
9
10
 
10
11
  // MCP Server 实现
11
12
  class FlowMindMCPServer {
@@ -122,106 +123,120 @@ class FlowMindMCPServer {
122
123
  async callTool(name, args) {
123
124
  await this.init();
124
125
 
126
+ const callStart = Date.now();
127
+ let result;
128
+
125
129
  try {
126
130
  // 核心工具
127
131
  if (name === 'flowmind_process') {
128
- const result = await this.flowmind.process(args.input, args.context || {});
129
- return {
132
+ const data = await this.flowmind.process(args.input, args.context || {});
133
+ result = {
130
134
  content: [{
131
135
  type: 'text',
132
- text: JSON.stringify(result, null, 2)
136
+ text: JSON.stringify(data, null, 2)
133
137
  }]
134
138
  };
135
- }
136
-
137
- if (name === 'flowmind_list_skills') {
139
+ } else if (name === 'flowmind_list_skills') {
138
140
  const skills = this.flowmind.skills.list();
139
- return {
141
+ result = {
140
142
  content: [{
141
143
  type: 'text',
142
144
  text: JSON.stringify({ skills }, null, 2)
143
145
  }]
144
146
  };
145
- }
146
-
147
- if (name === 'flowmind_get_skill') {
147
+ } else if (name === 'flowmind_get_skill') {
148
148
  const skill = this.flowmind.skills.get(args.name);
149
149
  if (!skill) {
150
- return {
150
+ result = {
151
151
  content: [{
152
152
  type: 'text',
153
153
  text: JSON.stringify({ error: `Skill not found: ${args.name}` })
154
154
  }],
155
155
  isError: true
156
156
  };
157
+ } else {
158
+ result = {
159
+ content: [{
160
+ type: 'text',
161
+ text: JSON.stringify({
162
+ name: skill.name,
163
+ description: skill.definition?.description,
164
+ category: skill.definition?.category,
165
+ triggers: skill.definition?.triggers,
166
+ componentDependencies: skill.definition?.componentDependencies
167
+ }, null, 2)
168
+ }]
169
+ };
157
170
  }
158
- return {
159
- content: [{
160
- type: 'text',
161
- text: JSON.stringify({
162
- name: skill.name,
163
- description: skill.definition?.description,
164
- category: skill.definition?.category,
165
- triggers: skill.definition?.triggers,
166
- componentDependencies: skill.definition?.componentDependencies
167
- }, null, 2)
168
- }]
169
- };
170
- }
171
-
172
- if (name === 'flowmind_ai_status') {
171
+ } else if (name === 'flowmind_ai_status') {
173
172
  const status = this.flowmind.getAIStatus();
174
- return {
173
+ result = {
175
174
  content: [{
176
175
  type: 'text',
177
176
  text: JSON.stringify(status, null, 2)
178
177
  }]
179
178
  };
180
- }
181
-
182
- if (name === 'flowmind_learning_stats') {
179
+ } else if (name === 'flowmind_learning_stats') {
183
180
  const stats = await this.flowmind.getStats();
184
- return {
181
+ result = {
185
182
  content: [{
186
183
  type: 'text',
187
184
  text: JSON.stringify(stats, null, 2)
188
185
  }]
189
186
  };
190
- }
191
-
192
- // 技能工具
193
- if (name.startsWith('flowmind_skill_')) {
187
+ } else if (name.startsWith('flowmind_skill_')) {
188
+ // 技能工具
194
189
  const skillName = name.replace('flowmind_skill_', '');
195
190
  const skill = this.flowmind.skills.get(skillName);
196
191
 
197
192
  if (!skill) {
198
- return {
193
+ result = {
199
194
  content: [{
200
195
  type: 'text',
201
196
  text: JSON.stringify({ error: `Skill not found: ${skillName}` })
202
197
  }],
203
198
  isError: true
204
199
  };
200
+ } else {
201
+ const data = await this.flowmind.executeWithLearning(skill, args.input, args.context || {});
202
+ result = {
203
+ content: [{
204
+ type: 'text',
205
+ text: JSON.stringify(data, null, 2)
206
+ }]
207
+ };
205
208
  }
206
-
207
- const result = await this.flowmind.executeWithLearning(skill, args.input, args.context || {});
208
- return {
209
+ } else {
210
+ result = {
209
211
  content: [{
210
212
  type: 'text',
211
- text: JSON.stringify(result, null, 2)
212
- }]
213
+ text: JSON.stringify({ error: `Unknown tool: ${name}` })
214
+ }],
215
+ isError: true
213
216
  };
214
217
  }
215
218
 
216
- return {
217
- content: [{
218
- type: 'text',
219
- text: JSON.stringify({ error: `Unknown tool: ${name}` })
220
- }],
221
- isError: true
222
- };
219
+ // Emit success event
220
+ eventBus.emit('mcp:tool_called', {
221
+ tool: name,
222
+ args,
223
+ duration: Date.now() - callStart,
224
+ success: !result.isError,
225
+ timestamp: new Date().toISOString()
226
+ });
227
+
228
+ return result;
223
229
 
224
230
  } catch (error) {
231
+ eventBus.emit('mcp:tool_called', {
232
+ tool: name,
233
+ args,
234
+ duration: Date.now() - callStart,
235
+ success: false,
236
+ error: error.message,
237
+ timestamp: new Date().toISOString()
238
+ });
239
+
225
240
  return {
226
241
  content: [{
227
242
  type: 'text',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowmind",
3
- "version": "1.1.0",
3
+ "version": "1.2.3",
4
4
  "description": "The AI Agent That Learns How You Work - Stop repeating yourself, FlowMind learns your workflows and applies them automatically.",
5
5
  "main": "core/index.js",
6
6
  "bin": {
@@ -43,6 +43,8 @@
43
43
  "core/",
44
44
  "mcp/",
45
45
  "skills/",
46
+ "tui/",
47
+ "dashboard/",
46
48
  "scripts/",
47
49
  "templates/",
48
50
  "config/",
@@ -56,16 +58,20 @@
56
58
  "node": ">=18.0.0"
57
59
  },
58
60
  "dependencies": {
59
- "commander": "^11.0.0",
60
61
  "chalk": "^4.1.2",
61
- "ora": "^5.4.1",
62
- "inquirer": "^8.2.6",
62
+ "commander": "^11.0.0",
63
63
  "fs-extra": "^11.1.1",
64
+ "ink": "^3.2.0",
65
+ "ink-spinner": "^4.0.3",
66
+ "ink-text-input": "^4.0.3",
67
+ "inquirer": "^8.2.6",
68
+ "ora": "^5.4.1",
69
+ "react": "^18.3.1",
64
70
  "uuid": "^9.0.0"
65
71
  },
66
72
  "devDependencies": {
67
- "jest": "^29.7.0",
68
73
  "eslint": "^8.50.0",
74
+ "jest": "^29.7.0",
69
75
  "nodemon": "^3.0.1",
70
76
  "webpack": "^5.88.0",
71
77
  "webpack-cli": "^5.1.4"
package/tui/app.jsx ADDED
@@ -0,0 +1,69 @@
1
+ const React = require('react');
2
+ const { Box, Text, useApp, useInput } = require('ink');
3
+ const Sidebar = require('./components/Sidebar.jsx');
4
+ const ChatPanel = require('./components/ChatPanel.jsx');
5
+ const ResultPanel = require('./components/ResultPanel.jsx');
6
+ const StatusBar = require('./components/StatusBar.jsx');
7
+
8
+ function App({ flowmind }) {
9
+ const [results, setResults] = React.useState([]);
10
+ const [isProcessing, setIsProcessing] = React.useState(false);
11
+ const mountedRef = React.useRef(true);
12
+ const { exit } = useApp();
13
+
14
+ React.useEffect(() => {
15
+ return () => { mountedRef.current = false; };
16
+ }, []);
17
+
18
+ useInput((input, key) => {
19
+ if (key.ctrl && input === 'c') exit();
20
+ });
21
+
22
+ const handleCommand = React.useCallback(async (input, addResponse) => {
23
+ setIsProcessing(true);
24
+ try {
25
+ const result = await flowmind.process(input);
26
+ if (!mountedRef.current) return;
27
+ if (result.type === 'result') {
28
+ const text = typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2);
29
+ addResponse(text.substring(0, 200) + (text.length > 200 ? '...' : ''));
30
+ } else if (result.type === 'learning') {
31
+ addResponse(result.message || 'Learning recorded');
32
+ } else if (result.type === 'error') {
33
+ addResponse('Error: ' + result.message);
34
+ }
35
+ setResults(prev => [...prev, result]);
36
+ } catch (e) {
37
+ if (mountedRef.current) addResponse('Error: ' + e.message);
38
+ } finally {
39
+ if (mountedRef.current) setIsProcessing(false);
40
+ }
41
+ }, [flowmind]);
42
+
43
+ const handleSkillSelect = React.useCallback((skill) => {
44
+ try {
45
+ setResults(prev => [...prev, {
46
+ type: 'result',
47
+ data: { name: skill.name, description: skill.definition?.description || 'No description', category: skill.category || 'general', path: skill.path },
48
+ metadata: { skill: skill.name }
49
+ }]);
50
+ } catch (e) {
51
+ // ignore skill select errors
52
+ }
53
+ }, []);
54
+
55
+ return (
56
+ React.createElement(Box, { flexDirection: 'column', width: '100%', height: '100%' },
57
+ React.createElement(Box, { flexDirection: 'row', flexGrow: 1 },
58
+ React.createElement(Sidebar, { flowmind: flowmind, width: 30, onSkillSelect: handleSkillSelect }),
59
+ React.createElement(Box, { flexDirection: 'column', width: '70%', flexGrow: 1 },
60
+ React.createElement(ChatPanel, { onSubmit: handleCommand, isProcessing: isProcessing, onExit: exit }),
61
+ React.createElement(ResultPanel, { results: results })
62
+ )
63
+ ),
64
+ React.createElement(StatusBar, { flowmind: flowmind })
65
+ )
66
+ );
67
+ }
68
+
69
+ module.exports = App;
@@ -0,0 +1,72 @@
1
+ const React = require('react');
2
+ const { Box, Text } = require('ink');
3
+ const TextInput = require('ink-text-input').default || require('ink-text-input');
4
+ const Spinner = require('ink-spinner').default || require('ink-spinner');
5
+
6
+ function ChatPanel({ onSubmit, isProcessing, onExit }) {
7
+ const [input, setInput] = React.useState('');
8
+ const [history, setHistory] = React.useState([]);
9
+ const mountedRef = React.useRef(true);
10
+
11
+ React.useEffect(() => {
12
+ return () => { mountedRef.current = false; };
13
+ }, []);
14
+
15
+ const handleSubmit = (value) => {
16
+ if (!value.trim()) return;
17
+ setHistory(prev => [...prev, { role: 'user', text: value }]);
18
+ setInput('');
19
+ if (value.toLowerCase() === 'exit' || value.toLowerCase() === 'quit') {
20
+ if (onExit) onExit();
21
+ return;
22
+ }
23
+ try {
24
+ onSubmit(value, (response) => {
25
+ if (mountedRef.current) {
26
+ setHistory(prev => [...prev, { role: 'flowmind', text: response }]);
27
+ }
28
+ });
29
+ } catch (e) {
30
+ if (mountedRef.current) {
31
+ setHistory(prev => [...prev, { role: 'flowmind', text: 'Error: ' + e.message }]);
32
+ }
33
+ }
34
+ };
35
+
36
+ const displayHistory = history.slice(-20);
37
+
38
+ return (
39
+ React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'green', paddingX: 1 },
40
+ React.createElement(Text, { bold: true, color: 'green' }, 'Command Input'),
41
+ React.createElement(Box, { flexDirection: 'column', marginTop: 1, minHeight: 6 },
42
+ displayHistory.length === 0 && React.createElement(Text, { color: 'gray' }, 'Type a command to get started. Type "exit" to quit.'),
43
+ displayHistory.map((msg, i) =>
44
+ React.createElement(Box, { key: i, flexDirection: 'column' },
45
+ msg.role === 'user'
46
+ ? React.createElement(Text, null,
47
+ React.createElement(Text, { color: 'green', bold: true }, '> '),
48
+ React.createElement(Text, { color: 'white' }, msg.text)
49
+ )
50
+ : React.createElement(Text, null,
51
+ React.createElement(Text, { color: 'cyan', bold: true }, '< '),
52
+ React.createElement(Text, { color: 'cyan' }, msg.text)
53
+ )
54
+ )
55
+ )
56
+ ),
57
+ React.createElement(Box, { marginTop: 1 },
58
+ isProcessing
59
+ ? React.createElement(Text, { color: 'yellow' },
60
+ React.createElement(Spinner, { type: 'dots' }),
61
+ ' Processing...'
62
+ )
63
+ : React.createElement(Box, null,
64
+ React.createElement(Text, { color: 'green', bold: true }, '> '),
65
+ React.createElement(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit })
66
+ )
67
+ )
68
+ )
69
+ );
70
+ }
71
+
72
+ module.exports = ChatPanel;
@@ -0,0 +1,108 @@
1
+ const React = require('react');
2
+ const { Box, Text } = require('ink');
3
+
4
+ const DRAGON_ARTS = {
5
+ 0: [
6
+ ' ╭─────╮ ',
7
+ ' ╱ ╭─╮ ╲ ',
8
+ ' │ │ │ │ ',
9
+ ' │ │ ◎ │ │ ',
10
+ ' │ ╰─╯ │ ',
11
+ ' ╲ ╱ ',
12
+ ' ╰─────╯ ',
13
+ ],
14
+ 1: [
15
+ ' ╭──╮ ',
16
+ ' ╭────╯ ╰───╮ ',
17
+ ' ╱ ◎ ╰─╯ ╲ ',
18
+ ' ╱ ▽ ╲ ',
19
+ ' ╲ ╱╲ ╱╲ ╱ ',
20
+ ' ╲╱╱ ╲╱╱ ╲╱╲╱ ',
21
+ ],
22
+ 2: [
23
+ ' ╭─╮ ╭─╮ ',
24
+ ' ╭────╯ ╰──╯ ╰───╮ ',
25
+ ' ╱ ◎ ╰──╯ ╲ ',
26
+ ' ╱ ╭────────╮ ╲ ',
27
+ ' ╲ ╱ ╱╱╱╱╱╱╱╱ ╲ ╱ ',
28
+ ' ╲───╯ ╱╱╱╱╱╱╱╱╱╱ ╰──╱ ',
29
+ ' ╰─╯ ╰─╯ ',
30
+ ],
31
+ 3: [
32
+ ' ╭───╮ ╭───╮ ',
33
+ ' ╭───╯ ╰──╯ ╰───╮ ',
34
+ ' ╱ ◎ ╰───╯ ╲ ',
35
+ '│ ╭──────────╮ │ ',
36
+ '│ ╱ ╱╱╱╱╱╱╱╱╱╱ ╲ │ ',
37
+ ' ╲──╯ ╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',
38
+ ' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╱ ',
39
+ ' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',
40
+ ' ╰───╯ ╰───╯ ',
41
+ ],
42
+ 4: [
43
+ ' ╭───╮ ╭───╮ ',
44
+ '╭───╯ ╰──────╯ ╰───╮ ',
45
+ '│ ◎ ╰───╯ │ ',
46
+ '│ ╭────────────╮ │ ',
47
+ '│ ╱ ╱╱╱╱╱╱╱╱╱╱╱╱ ╲ │ ',
48
+ ' ╲───╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰──╯ ',
49
+ ' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╲ ',
50
+ ' ╲─╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰─╲ ',
51
+ ' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',
52
+ ' ╰───╯ ╰───╯ ',
53
+ ],
54
+ 5: [
55
+ ' ★ ╭───╮ ╭───╮ ★ ',
56
+ '╭─╯ ╰──╯ ╰──╯ ╰─╮ ',
57
+ '│ ◎ ╰───╯ │ ',
58
+ '│ ╭──────────────╮ │ ',
59
+ '│ ╱ ★╱╱╱╱╱╱╱╱╱╱★╱╱ ╲ │ ',
60
+ ' ╲────╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',
61
+ ' ╲ ╱╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱ ╲ ',
62
+ ' ╲──╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰──╲ ',
63
+ ' ╲─╯╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱╰──╲ ',
64
+ ' ★ ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ★ ',
65
+ ' ╰───╯ ╰───╯ ',
66
+ ],
67
+ };
68
+
69
+ const LEVEL_NAMES = ['Egg', 'Hatchling', 'Juvenile', 'Adult', 'Elder', 'Ascended'];
70
+ const LEVEL_STATES = ['dormant', 'awakening', 'growing', 'soaring', 'wise', 'transcendent'];
71
+ const LEVEL_COLORS = ['gray', 'cyan', 'cyan', 'cyanBright', 'cyanBright', 'cyanBright'];
72
+
73
+ function DragonTotem({ honorData, compact }) {
74
+ const points = honorData?.points || 0;
75
+ const level = honorData?.level || 0;
76
+ const art = DRAGON_ARTS[level] || DRAGON_ARTS[0];
77
+ const color = LEVEL_COLORS[level] || 'gray';
78
+ const levelName = LEVEL_NAMES[level] || 'Unknown';
79
+ const state = LEVEL_STATES[level] || 'unknown';
80
+ const nextLevelPoints = [1, 10, 30, 60, 100];
81
+ const nextPoints = nextLevelPoints[level] || null;
82
+ const pointsToNext = nextPoints !== null ? nextPoints - points : 0;
83
+ const lines = compact ? art.slice(0, 4) : art;
84
+
85
+ return (
86
+ React.createElement(Box, { flexDirection: 'column', paddingX: 1 },
87
+ React.createElement(Text, { bold: true, color: 'cyan' }, 'Dragon Totem'),
88
+ React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
89
+ lines.map((line, i) => React.createElement(Text, { key: i, color: color }, line))
90
+ ),
91
+ React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
92
+ React.createElement(Text, null,
93
+ React.createElement(Text, { color: 'yellow' }, 'Lv' + level),
94
+ React.createElement(Text, { color: 'white' }, ' ' + levelName),
95
+ React.createElement(Text, { color: 'gray' }, ' (' + state + ')')
96
+ ),
97
+ React.createElement(Text, null,
98
+ React.createElement(Text, { color: 'yellow' }, '' + points),
99
+ React.createElement(Text, { color: 'gray' }, ' pts')
100
+ ),
101
+ pointsToNext > 0 && React.createElement(Text, { color: 'gray' }, pointsToNext + ' pts to ' + LEVEL_NAMES[level + 1]),
102
+ pointsToNext <= 0 && level >= 5 && React.createElement(Text, { color: 'yellow' }, 'Max level!')
103
+ )
104
+ )
105
+ );
106
+ }
107
+
108
+ module.exports = DragonTotem;
@@ -0,0 +1,33 @@
1
+ const React = require('react');
2
+ const { Box, Text } = require('ink');
3
+
4
+ function ResultPanel({ results }) {
5
+ const displayResults = results.slice(-30);
6
+
7
+ return (
8
+ React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'yellow', paddingX: 1, flexGrow: 1 },
9
+ React.createElement(Text, { bold: true, color: 'yellow' }, 'Results'),
10
+ React.createElement(Box, { flexDirection: 'column', marginTop: 1, overflow: 'hidden' },
11
+ displayResults.length === 0 && React.createElement(Text, { color: 'gray' }, 'No results yet. Execute a command to see output here.'),
12
+ displayResults.map((result, i) =>
13
+ React.createElement(Box, { key: i, flexDirection: 'column', marginBottom: 1 },
14
+ result.type === 'result' && React.createElement(React.Fragment, null,
15
+ result.metadata?.skill && React.createElement(Text, null,
16
+ React.createElement(Text, { color: 'gray' }, 'Skill: '),
17
+ React.createElement(Text, { color: 'green' }, result.metadata.skill),
18
+ result.metadata?.duration && React.createElement(Text, { color: 'gray' }, ' (' + result.metadata.duration + 'ms)')
19
+ ),
20
+ React.createElement(Text, { color: 'white' },
21
+ typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2)
22
+ )
23
+ ),
24
+ result.type === 'learning' && React.createElement(Text, { color: 'cyan' }, result.message),
25
+ result.type === 'error' && React.createElement(Text, { color: 'red' }, 'Error: ' + result.message)
26
+ )
27
+ )
28
+ )
29
+ )
30
+ );
31
+ }
32
+
33
+ module.exports = ResultPanel;
@@ -0,0 +1,70 @@
1
+ const React = require('react');
2
+ const { Box, Text, useInput } = require('ink');
3
+ const DragonTotem = require('./DragonTotem.jsx');
4
+
5
+ function Sidebar({ flowmind, width, onSkillSelect }) {
6
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
7
+ const [skills, setSkills] = React.useState([]);
8
+ const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
9
+
10
+ React.useEffect(() => {
11
+ if (flowmind) {
12
+ try {
13
+ const list = flowmind.skills.list() || [];
14
+ setSkills(list);
15
+ } catch (e) {
16
+ setSkills([]);
17
+ }
18
+ try {
19
+ setHonorData(flowmind.getHonorData());
20
+ } catch (e) {
21
+ // keep default honorData
22
+ }
23
+ }
24
+ }, [flowmind]);
25
+
26
+ useInput((input, key) => {
27
+ if (key.upArrow) setSelectedIndex(prev => Math.max(0, prev - 1));
28
+ else if (key.downArrow) setSelectedIndex(prev => Math.min(skills.length - 1, prev + 1));
29
+ else if (key.return && skills[selectedIndex] && onSkillSelect) onSkillSelect(skills[selectedIndex]);
30
+ });
31
+
32
+ const barWidth = Math.max(10, width - 4);
33
+ const progress = honorData.points > 0 ? Math.min(1, honorData.points / 100) : 0;
34
+ const filled = Math.round(progress * barWidth);
35
+ const progressBar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
36
+
37
+ return (
38
+ React.createElement(Box, { flexDirection: 'column', width: width, borderStyle: 'single', borderColor: 'cyan', paddingX: 1 },
39
+ React.createElement(DragonTotem, { honorData: honorData, compact: true }),
40
+ React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
41
+ React.createElement(Text, { bold: true, color: 'cyan' }, 'Progress'),
42
+ React.createElement(Text, { color: 'green' }, progressBar),
43
+ React.createElement(Text, { color: 'gray' }, honorData.points + '/100 pts')
44
+ ),
45
+ React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
46
+ React.createElement(Text, { bold: true, color: 'cyan' }, 'Skills (' + skills.length + ')'),
47
+ React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
48
+ skills.length === 0 && React.createElement(Text, { color: 'gray' }, 'No skills loaded'),
49
+ skills.map((skill, i) => {
50
+ const isSelected = i === selectedIndex;
51
+ const category = skill.category || 'general';
52
+ const prefix = isSelected ? '\u25B6 ' : ' ';
53
+ return React.createElement(Text, { key: skill.name },
54
+ React.createElement(Text, { color: isSelected ? 'green' : 'white' }, prefix + skill.name),
55
+ React.createElement(Text, { color: 'gray' }, ' [' + category + ']')
56
+ );
57
+ })
58
+ )
59
+ ),
60
+ React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
61
+ React.createElement(Text, { bold: true, color: 'cyan' }, 'Stats'),
62
+ React.createElement(Text, { color: 'gray' }, 'Skills used: ' + (honorData.stats?.skillUseCount || 0)),
63
+ React.createElement(Text, { color: 'gray' }, 'Learnings: ' + (honorData.stats?.learningCount || 0)),
64
+ React.createElement(Text, { color: 'gray' }, 'New skills: ' + (honorData.stats?.newSkillCount || 0))
65
+ )
66
+ )
67
+ );
68
+ }
69
+
70
+ module.exports = Sidebar;