fluxy-bot 0.5.28 → 0.5.30
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/dist-fluxy/assets/{fluxy-D3zajcpJ.js → fluxy-DbZm7HVX.js} +15 -15
- package/dist-fluxy/fluxy.html +1 -1
- package/package.json +1 -1
- package/supervisor/chat/src/hooks/useFluxyChat.ts +29 -2
- package/supervisor/index.ts +27 -1
- package/supervisor/scheduler.ts +17 -1
- package/vite.config.ts +15 -0
- package/worker/prompts/fluxy-system-prompt.txt +33 -0
- package/workspace/client/src/App.tsx +4 -7
package/dist-fluxy/fluxy.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content" />
|
|
6
6
|
<title>Fluxy Chat</title>
|
|
7
|
-
<script type="module" crossorigin src="/fluxy/assets/fluxy-
|
|
7
|
+
<script type="module" crossorigin src="/fluxy/assets/fluxy-DbZm7HVX.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/fluxy/assets/globals-B1nERNzb.js">
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/fluxy/assets/globals-BHyeC8Bo.css">
|
|
10
10
|
</head>
|
package/package.json
CHANGED
|
@@ -15,6 +15,12 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enable
|
|
|
15
15
|
const [streamBuffer, setStreamBuffer] = useState('');
|
|
16
16
|
const [tools, setTools] = useState<ToolActivity[]>([]);
|
|
17
17
|
const loaded = useRef(false);
|
|
18
|
+
const conversationIdRef = useRef<string | null>(null);
|
|
19
|
+
const streamingRef = useRef(false);
|
|
20
|
+
|
|
21
|
+
// Keep refs in sync with state
|
|
22
|
+
useEffect(() => { conversationIdRef.current = conversationId; }, [conversationId]);
|
|
23
|
+
useEffect(() => { streamingRef.current = streaming; }, [streaming]);
|
|
18
24
|
|
|
19
25
|
// Load current conversation from DB
|
|
20
26
|
const loadFromDb = useCallback(async () => {
|
|
@@ -76,6 +82,19 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enable
|
|
|
76
82
|
}
|
|
77
83
|
}, [enabled, triggerReload, loadFromDb]);
|
|
78
84
|
|
|
85
|
+
// Periodic DB sync after reconnect while streaming — catches the final bot:response
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!enabled || !triggerReload || triggerReload === 0 || !streaming) return;
|
|
88
|
+
const interval = setInterval(() => {
|
|
89
|
+
if (!streamingRef.current) {
|
|
90
|
+
clearInterval(interval);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
loadFromDb();
|
|
94
|
+
}, 3000);
|
|
95
|
+
return () => clearInterval(interval);
|
|
96
|
+
}, [enabled, triggerReload, streaming, loadFromDb]);
|
|
97
|
+
|
|
79
98
|
useEffect(() => {
|
|
80
99
|
if (!ws) return;
|
|
81
100
|
|
|
@@ -125,7 +144,7 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enable
|
|
|
125
144
|
}),
|
|
126
145
|
// Cross-device sync: append message from another client
|
|
127
146
|
ws.on('chat:sync', (data: { conversationId: string; message: { role: string; content: string; timestamp: string } }) => {
|
|
128
|
-
if (
|
|
147
|
+
if (conversationIdRef.current && data.conversationId !== conversationIdRef.current) return;
|
|
129
148
|
setMessages((msgs) => [
|
|
130
149
|
...msgs,
|
|
131
150
|
{
|
|
@@ -140,6 +159,14 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enable
|
|
|
140
159
|
ws.on('chat:conversation-created', (data: { conversationId: string }) => {
|
|
141
160
|
setConversationId(data.conversationId);
|
|
142
161
|
}),
|
|
162
|
+
// Server sends current streaming state on (re)connect
|
|
163
|
+
ws.on('chat:state', (data: { streaming: boolean; conversationId?: string; buffer?: string }) => {
|
|
164
|
+
if (data.streaming) {
|
|
165
|
+
setStreaming(true);
|
|
166
|
+
if (data.conversationId) setConversationId(data.conversationId);
|
|
167
|
+
if (data.buffer) setStreamBuffer(data.buffer);
|
|
168
|
+
}
|
|
169
|
+
}),
|
|
143
170
|
// Context cleared (from any client)
|
|
144
171
|
ws.on('chat:cleared', () => {
|
|
145
172
|
setMessages([]);
|
|
@@ -152,7 +179,7 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enable
|
|
|
152
179
|
];
|
|
153
180
|
|
|
154
181
|
return () => unsubs.forEach((u) => u());
|
|
155
|
-
}, [ws
|
|
182
|
+
}, [ws]);
|
|
156
183
|
|
|
157
184
|
const sendMessage = useCallback(
|
|
158
185
|
(content: string, attachments?: Attachment[], audioData?: string) => {
|
package/supervisor/index.ts
CHANGED
|
@@ -120,6 +120,10 @@ export async function startSupervisor() {
|
|
|
120
120
|
// Track active DB conversation per WS client
|
|
121
121
|
const clientConvs = new Map<WebSocket, string>();
|
|
122
122
|
|
|
123
|
+
// Track current stream state so reconnecting clients get caught up
|
|
124
|
+
let currentStreamConvId: string | null = null;
|
|
125
|
+
let currentStreamBuffer = '';
|
|
126
|
+
|
|
123
127
|
/** Call worker API endpoints */
|
|
124
128
|
async function workerApi(path: string, method = 'GET', body?: any) {
|
|
125
129
|
const opts: RequestInit = { method, headers: { 'Content-Type': 'application/json' } };
|
|
@@ -352,6 +356,18 @@ export async function startSupervisor() {
|
|
|
352
356
|
let convId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
353
357
|
conversations.set(ws, []);
|
|
354
358
|
|
|
359
|
+
// Send current streaming state so reconnecting clients can catch up
|
|
360
|
+
if (agentQueryActive && currentStreamConvId) {
|
|
361
|
+
ws.send(JSON.stringify({
|
|
362
|
+
type: 'chat:state',
|
|
363
|
+
data: {
|
|
364
|
+
streaming: true,
|
|
365
|
+
conversationId: currentStreamConvId,
|
|
366
|
+
buffer: currentStreamBuffer,
|
|
367
|
+
},
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
|
|
355
371
|
ws.on('message', (raw) => {
|
|
356
372
|
const rawStr = raw.toString();
|
|
357
373
|
|
|
@@ -498,10 +514,19 @@ export async function startSupervisor() {
|
|
|
498
514
|
|
|
499
515
|
// Start agent query
|
|
500
516
|
agentQueryActive = true;
|
|
517
|
+
currentStreamConvId = convId;
|
|
518
|
+
currentStreamBuffer = '';
|
|
501
519
|
startFluxyAgentQuery(convId, content, freshConfig.ai.model, (type, eventData) => {
|
|
520
|
+
// Track stream buffer for reconnecting clients
|
|
521
|
+
if (type === 'bot:token' && eventData.token) {
|
|
522
|
+
currentStreamBuffer += eventData.token;
|
|
523
|
+
}
|
|
524
|
+
|
|
502
525
|
// Intercept bot:done — Vite HMR handles file changes automatically
|
|
503
526
|
if (type === 'bot:done') {
|
|
504
527
|
agentQueryActive = false;
|
|
528
|
+
currentStreamConvId = null;
|
|
529
|
+
currentStreamBuffer = '';
|
|
505
530
|
// Restart if agent used file tools OR file watcher detected changes during the turn
|
|
506
531
|
if (eventData.usedFileTools || pendingBackendRestart) {
|
|
507
532
|
console.log('[supervisor] Agent turn ended — restarting backend');
|
|
@@ -512,8 +537,9 @@ export async function startSupervisor() {
|
|
|
512
537
|
return; // don't forward bot:done to client
|
|
513
538
|
}
|
|
514
539
|
|
|
515
|
-
// Save assistant response to DB
|
|
540
|
+
// Save assistant response to DB + clear stream state
|
|
516
541
|
if (type === 'bot:response') {
|
|
542
|
+
currentStreamBuffer = '';
|
|
517
543
|
(async () => {
|
|
518
544
|
try {
|
|
519
545
|
await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
|
package/supervisor/scheduler.ts
CHANGED
|
@@ -253,8 +253,21 @@ function tick() {
|
|
|
253
253
|
log.info(`[scheduler] Removing fired one-shot cron: ${cron.id}`);
|
|
254
254
|
const fresh = readCronsConfig().filter((c) => c.id !== cron.id);
|
|
255
255
|
writeCronsConfig(fresh);
|
|
256
|
+
// Also remove the task file if it exists
|
|
257
|
+
try {
|
|
258
|
+
fs.unlinkSync(path.join(WORKSPACE_DIR, 'tasks', `${cron.id}.md`));
|
|
259
|
+
} catch {}
|
|
256
260
|
} : undefined;
|
|
257
|
-
|
|
261
|
+
// Inject task file content if tasks/{id}.md exists
|
|
262
|
+
let cronPrompt = `<CRON>${cron.id}</CRON>`;
|
|
263
|
+
try {
|
|
264
|
+
const taskFile = path.join(WORKSPACE_DIR, 'tasks', `${cron.id}.md`);
|
|
265
|
+
const taskContent = fs.readFileSync(taskFile, 'utf-8').trim();
|
|
266
|
+
if (taskContent) {
|
|
267
|
+
cronPrompt += `\n<CRON_TASK_DETAIL>\n${taskContent}\n</CRON_TASK_DETAIL>`;
|
|
268
|
+
}
|
|
269
|
+
} catch {}
|
|
270
|
+
triggerAgent(cronPrompt, cron.id, onComplete);
|
|
258
271
|
}
|
|
259
272
|
}
|
|
260
273
|
}
|
|
@@ -265,6 +278,9 @@ function tick() {
|
|
|
265
278
|
if (c.oneShot && cronIsExpired(c.schedule)) {
|
|
266
279
|
log.info(`[scheduler] cronIsExpired=true for "${c.id}" schedule="${c.schedule}"`);
|
|
267
280
|
log.info(`[scheduler] Removing expired one-shot cron: ${c.id}`);
|
|
281
|
+
try {
|
|
282
|
+
fs.unlinkSync(path.join(WORKSPACE_DIR, 'tasks', `${c.id}.md`));
|
|
283
|
+
} catch {}
|
|
268
284
|
return false;
|
|
269
285
|
}
|
|
270
286
|
return true;
|
package/vite.config.ts
CHANGED
|
@@ -24,6 +24,21 @@ export default defineConfig({
|
|
|
24
24
|
warmup: {
|
|
25
25
|
clientFiles: ['./src/main.tsx'],
|
|
26
26
|
},
|
|
27
|
+
watch: {
|
|
28
|
+
ignored: [
|
|
29
|
+
'**/app.db*',
|
|
30
|
+
'**/.backend.log',
|
|
31
|
+
'**/files/**',
|
|
32
|
+
'**/.env',
|
|
33
|
+
'**/backend/**',
|
|
34
|
+
'**/*.db',
|
|
35
|
+
'**/*.db-journal',
|
|
36
|
+
'**/*.db-wal',
|
|
37
|
+
'**/*.db-shm',
|
|
38
|
+
'**/*.sqlite',
|
|
39
|
+
'**/*.log',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
27
42
|
},
|
|
28
43
|
optimizeDeps: {
|
|
29
44
|
include: [
|
|
@@ -148,8 +148,41 @@ Just edit `CRONS.json` with the Write or Edit tool when asked. Each cron needs:
|
|
|
148
148
|
|
|
149
149
|
When you receive a `<CRON>cron-id</CRON>` message, look up that ID in your CRONS.json (provided in your context above) to find the task description. Execute the task, save results to the appropriate files, finish your turn.
|
|
150
150
|
|
|
151
|
+
If a `<CRON_TASK_DETAIL>` block follows the CRON tag, it contains the full instructions from `tasks/{id}.md` — use those as your execution plan instead of just the short `task` summary.
|
|
152
|
+
|
|
151
153
|
Notify your human only if importance is 7+ — otherwise log results silently.
|
|
152
154
|
|
|
155
|
+
## Task Files — `tasks/`
|
|
156
|
+
|
|
157
|
+
Complex cron tasks can have detailed instruction files in `tasks/{cron-id}.md`. The supervisor automatically injects the file content into your context when the cron fires.
|
|
158
|
+
|
|
159
|
+
**How it works:**
|
|
160
|
+
- The `task` field in CRONS.json stays short (1-2 sentences) — a summary for listing and logging
|
|
161
|
+
- The `.md` file has the full instructions: steps, resources, output format, error handling
|
|
162
|
+
- When the cron fires, you receive both `<CRON>id</CRON>` and `<CRON_TASK_DETAIL>` with the file contents
|
|
163
|
+
|
|
164
|
+
**When to create a task file:**
|
|
165
|
+
- The cron requires multiple steps or tool usage (browser, API calls, file manipulation)
|
|
166
|
+
- The cron needs specific file paths, URLs, credentials, or configuration
|
|
167
|
+
- A single `task` string can't capture the full instructions
|
|
168
|
+
|
|
169
|
+
**When NOT to create a task file:**
|
|
170
|
+
- Simple reminders or one-step tasks that fit in 1-2 sentences
|
|
171
|
+
|
|
172
|
+
**Format:** `tasks/{cron-id}.md` — the filename must match the cron `id`. Include: what to do, resources needed, numbered steps, where to save output, and error handling.
|
|
173
|
+
|
|
174
|
+
**Referencing from CRONS.json:**
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"id": "bip-hot-posts",
|
|
178
|
+
"schedule": "0 */8 * * *",
|
|
179
|
+
"task": "Scrape top 5 BIP hot posts. See tasks/bip-hot-posts.md for full instructions.",
|
|
180
|
+
"enabled": true
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Lifecycle:** You create the `.md` file when you create the cron. For oneShot crons, the supervisor auto-deletes both the JSON entry and the task file after execution.
|
|
185
|
+
|
|
153
186
|
---
|
|
154
187
|
|
|
155
188
|
# Coding Excellence
|
|
@@ -58,13 +58,10 @@ export default function App() {
|
|
|
58
58
|
} else if (e.data?.type === 'fluxy:onboard-complete') {
|
|
59
59
|
setShowOnboard(false);
|
|
60
60
|
} else if (e.data?.type === 'fluxy:hmr-update') {
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
sessionStorage.setItem('fluxy_widget_open', '1');
|
|
66
|
-
}
|
|
67
|
-
setTimeout(() => location.reload(), 800);
|
|
61
|
+
// Vite HMR handles hot updates natively — no manual reload needed.
|
|
62
|
+
// Manual location.reload() here was causing unnecessary full-page refreshes
|
|
63
|
+
// that killed the chat iframe's WebSocket connection.
|
|
64
|
+
console.log('[dashboard] File changed — Vite HMR will handle it');
|
|
68
65
|
}
|
|
69
66
|
};
|
|
70
67
|
window.addEventListener('message', handler);
|