fluxy-bot 0.5.1 → 0.5.2
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/package.json +1 -1
- package/supervisor/chat/src/components/Chat/MessageBubble.tsx +52 -1
- package/supervisor/chat/src/components/Chat/MessageList.tsx +1 -0
- package/supervisor/chat/src/hooks/useChat.ts +22 -0
- package/supervisor/chat/src/hooks/useFluxyChat.ts +17 -0
- package/supervisor/scheduler.ts +41 -24
- package/worker/prompts/fluxy-system-prompt.txt +1 -1
package/package.json
CHANGED
|
@@ -14,6 +14,11 @@ interface Props {
|
|
|
14
14
|
audioData?: string;
|
|
15
15
|
attachments?: StoredAttachment[];
|
|
16
16
|
onImageClick?: (images: string[], index: number) => void;
|
|
17
|
+
notification?: {
|
|
18
|
+
source: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
priority?: string;
|
|
21
|
+
};
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
function formatTime(iso: string): string {
|
|
@@ -25,7 +30,7 @@ function formatTime(iso: string): string {
|
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
export default function MessageBubble({ role, content, timestamp, hasAttachments, audioData, attachments, onImageClick }: Props) {
|
|
33
|
+
export default function MessageBubble({ role, content, timestamp, hasAttachments, audioData, attachments, onImageClick, notification }: Props) {
|
|
29
34
|
const isUser = role === 'user';
|
|
30
35
|
const time = timestamp ? formatTime(timestamp) : '';
|
|
31
36
|
|
|
@@ -90,6 +95,52 @@ export default function MessageBubble({ role, content, timestamp, hasAttachments
|
|
|
90
95
|
);
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
// Notification bubble (autonomous pulse/cron messages)
|
|
99
|
+
if (notification) {
|
|
100
|
+
const isHigh = notification.priority === 'high';
|
|
101
|
+
const sourceLabel = notification.source === 'pulse' ? 'PULSE' : `CRON`;
|
|
102
|
+
const sourceIcon = notification.source === 'pulse' ? '\u{1F514}' : '\u23F0';
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="flex flex-col items-start gap-0.5">
|
|
106
|
+
<div
|
|
107
|
+
className={`min-w-0 rounded-2xl px-4 py-2.5 text-sm leading-relaxed prose prose-sm prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 break-words overflow-hidden ${
|
|
108
|
+
isHigh
|
|
109
|
+
? 'bg-amber-500/15 border border-amber-500/30 text-foreground'
|
|
110
|
+
: 'bg-blue-500/10 border border-blue-500/20 text-foreground'
|
|
111
|
+
}`}
|
|
112
|
+
style={{ maxWidth: '92%', overflowWrap: 'anywhere' }}
|
|
113
|
+
>
|
|
114
|
+
<div className={`flex items-center gap-1.5 mb-1.5 text-xs font-medium ${isHigh ? 'text-amber-400' : 'text-blue-400'}`}>
|
|
115
|
+
<span>{sourceIcon}</span>
|
|
116
|
+
<span>{sourceLabel}</span>
|
|
117
|
+
{notification.title && <span className="text-muted-foreground">— {notification.title}</span>}
|
|
118
|
+
</div>
|
|
119
|
+
<ReactMarkdown
|
|
120
|
+
remarkPlugins={[remarkGfm]}
|
|
121
|
+
components={{
|
|
122
|
+
pre({ children }) { return <pre className="overflow-x-auto my-2">{children}</pre>; },
|
|
123
|
+
code({ className, children, ...props }) {
|
|
124
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
125
|
+
const code = String(children).replace(/\n$/, '');
|
|
126
|
+
if (match) {
|
|
127
|
+
return <SyntaxHighlighter style={oneDark} language={match[1]} PreTag="div" customStyle={{ margin: '0.5rem 0', borderRadius: '0.5rem', fontSize: '0.8rem', overflowX: 'auto' }}>{code}</SyntaxHighlighter>;
|
|
128
|
+
}
|
|
129
|
+
return <code className="bg-white/10 rounded px-1 py-0.5 text-[0.8rem] break-all" style={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }} {...props}>{children}</code>;
|
|
130
|
+
},
|
|
131
|
+
p({ children }) { return <p className="my-1.5">{children}</p>; },
|
|
132
|
+
ul({ children }) { return <ul className="my-1.5 pl-4 list-disc">{children}</ul>; },
|
|
133
|
+
ol({ children }) { return <ol className="my-1.5 pl-4 list-decimal">{children}</ol>; },
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{content}
|
|
137
|
+
</ReactMarkdown>
|
|
138
|
+
</div>
|
|
139
|
+
{time && <span className="text-[10px] text-muted-foreground/50 px-1">{time}</span>}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
93
144
|
return (
|
|
94
145
|
<div className="flex flex-col items-start gap-0.5">
|
|
95
146
|
<div className="min-w-0 rounded-2xl px-4 py-2.5 text-sm leading-relaxed bg-muted text-foreground prose prose-sm prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 break-words overflow-hidden" style={{ maxWidth: '92%', overflowWrap: 'anywhere' }}>
|
|
@@ -16,6 +16,11 @@ export interface ChatMessage {
|
|
|
16
16
|
hasAttachments?: boolean;
|
|
17
17
|
audioData?: string; // data URL or HTTP URL for voice messages
|
|
18
18
|
attachments?: StoredAttachment[];
|
|
19
|
+
notification?: {
|
|
20
|
+
source: string; // 'pulse' or cron id
|
|
21
|
+
title?: string;
|
|
22
|
+
priority?: string; // 'high', 'normal', etc.
|
|
23
|
+
};
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export interface ToolActivity {
|
|
@@ -166,6 +171,23 @@ export function useChat(ws: WsClient | null) {
|
|
|
166
171
|
},
|
|
167
172
|
]);
|
|
168
173
|
}),
|
|
174
|
+
// Autonomous agent notifications (pulse/cron)
|
|
175
|
+
ws.on('bot:autonomous-message', (data: { content: string; source: string; title?: string; priority?: string; timestamp: string }) => {
|
|
176
|
+
setMessages((msgs) => [
|
|
177
|
+
...msgs,
|
|
178
|
+
{
|
|
179
|
+
id: `notif-${Date.now()}`,
|
|
180
|
+
role: 'assistant',
|
|
181
|
+
content: data.content,
|
|
182
|
+
timestamp: data.timestamp,
|
|
183
|
+
notification: {
|
|
184
|
+
source: data.source,
|
|
185
|
+
title: data.title,
|
|
186
|
+
priority: data.priority,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
]);
|
|
190
|
+
}),
|
|
169
191
|
];
|
|
170
192
|
|
|
171
193
|
return () => unsubs.forEach((u) => u());
|
|
@@ -123,6 +123,23 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enable
|
|
|
123
123
|
},
|
|
124
124
|
]);
|
|
125
125
|
}),
|
|
126
|
+
// Autonomous agent notifications (pulse/cron)
|
|
127
|
+
ws.on('bot:autonomous-message', (data: { content: string; source: string; title?: string; priority?: string; timestamp: string }) => {
|
|
128
|
+
setMessages((msgs) => [
|
|
129
|
+
...msgs,
|
|
130
|
+
{
|
|
131
|
+
id: `notif-${Date.now()}`,
|
|
132
|
+
role: 'assistant',
|
|
133
|
+
content: data.content,
|
|
134
|
+
timestamp: data.timestamp,
|
|
135
|
+
notification: {
|
|
136
|
+
source: data.source,
|
|
137
|
+
title: data.title,
|
|
138
|
+
priority: data.priority,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
}),
|
|
126
143
|
// Cross-device sync: append message from another client
|
|
127
144
|
ws.on('chat:sync', (data: { conversationId: string; message: { role: string; content: string; timestamp: string } }) => {
|
|
128
145
|
if (conversationId && data.conversationId !== conversationId) return;
|
package/supervisor/scheduler.ts
CHANGED
|
@@ -121,35 +121,52 @@ function triggerAgent(prompt: string, label: string) {
|
|
|
121
121
|
log.warn(`[scheduler] Failed to create conversation for ${label}: ${err.message}`);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
let fullResponse = '';
|
|
125
|
+
|
|
124
126
|
startFluxyAgentQuery(convId, prompt, model, (type, eventData) => {
|
|
125
127
|
if (type === 'bot:response') {
|
|
126
|
-
|
|
127
|
-
const messageRegex = /<Message>([\s\S]*?)<\/Message>/g;
|
|
128
|
-
let match;
|
|
129
|
-
while ((match = messageRegex.exec(eventData.content)) !== null) {
|
|
130
|
-
const messageContent = match[1].trim();
|
|
131
|
-
log.info(`[scheduler] Agent message: ${messageContent.slice(0, 80)}...`);
|
|
132
|
-
broadcastFluxy('bot:autonomous-message', {
|
|
133
|
-
content: messageContent,
|
|
134
|
-
source: label,
|
|
135
|
-
timestamp: new Date().toISOString(),
|
|
136
|
-
});
|
|
137
|
-
// TODO: push notification for PWA
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Persist bot response to DB
|
|
141
|
-
if (dbConvId) {
|
|
142
|
-
workerApi(`/api/conversations/${dbConvId}/messages`, 'POST', {
|
|
143
|
-
role: 'assistant',
|
|
144
|
-
content: eventData.content,
|
|
145
|
-
meta: { model, source: label },
|
|
146
|
-
}).catch((err: any) => {
|
|
147
|
-
log.warn(`[scheduler] DB persist error: ${err.message}`);
|
|
148
|
-
});
|
|
149
|
-
}
|
|
128
|
+
fullResponse = eventData.content || '';
|
|
150
129
|
}
|
|
151
130
|
|
|
152
131
|
if (type === 'bot:done') {
|
|
132
|
+
// Extract <Message> blocks after agent turn completes
|
|
133
|
+
if (fullResponse) {
|
|
134
|
+
const messageRegex = /<Message(?:\s+([^>]*))?>(([\s\S]*?))<\/Message>/g;
|
|
135
|
+
let match;
|
|
136
|
+
while ((match = messageRegex.exec(fullResponse)) !== null) {
|
|
137
|
+
const attrs = match[1] || '';
|
|
138
|
+
const messageContent = match[2].trim();
|
|
139
|
+
|
|
140
|
+
// Parse optional title and priority attributes
|
|
141
|
+
const titleMatch = attrs.match(/title="([^"]*)"/);
|
|
142
|
+
const priorityMatch = attrs.match(/priority="([^"]*)"/);
|
|
143
|
+
|
|
144
|
+
log.info(`[scheduler] Agent message: ${messageContent.slice(0, 80)}...`);
|
|
145
|
+
broadcastFluxy('bot:autonomous-message', {
|
|
146
|
+
content: messageContent,
|
|
147
|
+
source: label,
|
|
148
|
+
title: titleMatch?.[1],
|
|
149
|
+
priority: priorityMatch?.[1],
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
});
|
|
152
|
+
// TODO: push notification for PWA
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Strip <Message> blocks from stored response
|
|
156
|
+
const cleanedResponse = fullResponse.replace(/<Message(?:\s+[^>]*)?>[\s\S]*?<\/Message>/g, '').trim();
|
|
157
|
+
|
|
158
|
+
// Persist cleaned bot response to DB
|
|
159
|
+
if (dbConvId && cleanedResponse) {
|
|
160
|
+
workerApi(`/api/conversations/${dbConvId}/messages`, 'POST', {
|
|
161
|
+
role: 'assistant',
|
|
162
|
+
content: cleanedResponse,
|
|
163
|
+
meta: { model, source: label },
|
|
164
|
+
}).catch((err: any) => {
|
|
165
|
+
log.warn(`[scheduler] DB persist error: ${err.message}`);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
153
170
|
log.info(`[scheduler] ${label} agent query complete`);
|
|
154
171
|
if (eventData.usedFileTools) {
|
|
155
172
|
log.info(`[scheduler] File tools used — restarting backend`);
|
|
@@ -104,7 +104,7 @@ When you receive a `<PULSE/>` tag, it means the background process triggered you
|
|
|
104
104
|
2. **Check the workspace.** Any broken routes? Stale data? Code that needs cleanup? Problems you noticed earlier but didn't fix?
|
|
105
105
|
3. **Be proactive.** Think about what would help or impress your human. Maybe research a topic they mentioned. Maybe organize something messy. Maybe prepare for something you know is coming.
|
|
106
106
|
4. **Rate importance 0–10** based on what you know about your human:
|
|
107
|
-
- **8+**: Send a message immediately using `<Message>Your markdown message here
|
|
107
|
+
- **8+**: Send a message immediately using `<Message>Your markdown message here</Message>`. You can optionally add title and priority: `<Message title="Build Error" priority="high">details here</Message>`
|
|
108
108
|
- **Below 8**: Note it in your daily memory. Discuss later when they talk to you.
|
|
109
109
|
|
|
110
110
|
**Track what you check** so you don't repeat the same things every pulse. Rotate through different tasks.
|