@polderlabs/bizar 4.2.3 → 4.3.0
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/bizar-dash/src/server/api.mjs +2 -0
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +426 -0
- package/bizar-dash/src/server/serve-info.mjs +124 -1
- package/bizar-dash/src/server/server.mjs +1 -0
- package/bizar-dash/src/web/components/chat/SessionList.tsx +15 -5
- package/bizar-dash/src/web/components/chat/useChat.ts +228 -42
- package/bizar-dash/src/web/lib/types.ts +1 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +7 -5
- package/bizar-dash/src/web/views/Chat.tsx +7 -5
- package/bizar-dash/src/web/views/Config.tsx +2 -0
- package/bizar-dash/tests/dash-cli-load.test.mjs +44 -0
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +273 -0
- package/cli/bin.mjs +13 -13
- package/config/opencode.json +13 -13
- package/package.json +4 -4
- package/packages/sdk/tests/fixtures/fetch-mock.ts +2 -2
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// src/components/chat/useChat.ts — shared chat state: messages, sessions, send, polling.
|
|
2
|
+
// Supports two parallel message streams: bizar (GET /api/chat) and opencode (GET /api/opencode-sessions/:id/messages + SSE).
|
|
2
3
|
|
|
3
4
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
5
|
import type { ChatMessage, ChatResponse, ChatSession, Settings, Snapshot } from '../../lib/types';
|
|
5
6
|
import { api } from '../../lib/api';
|
|
6
7
|
|
|
7
8
|
export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?: string | null) {
|
|
8
|
-
|
|
9
|
+
// ── Bizar stream ────────────────────────────────────────────────────────────
|
|
10
|
+
const [bizarMessages, setBizarMessages] = useState<ChatMessage[]>([]);
|
|
9
11
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
|
10
12
|
const [opencodeSessions, setOpencodeSessions] = useState<ChatSession[]>([]);
|
|
11
13
|
const [sessionId, setSessionId] = useState('');
|
|
@@ -13,21 +15,28 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
13
15
|
const [sending, setSending] = useState(false);
|
|
14
16
|
const [pinned, setPinned] = useState<Set<number>>(new Set());
|
|
15
17
|
|
|
18
|
+
// ── Opencode stream ─────────────────────────────────────────────────────────
|
|
19
|
+
const [opencodeMessages, setOpencodeMessages] = useState<ChatMessage[]>([]);
|
|
20
|
+
const [activeSource, setActiveSource] = useState<'bizar' | 'opencode' | null>(null);
|
|
21
|
+
const [activeOpencodeSessionId, setActiveOpencodeSessionId] = useState<string | null>(null);
|
|
22
|
+
const opencodeEsRef = useRef<EventSource | null>(null);
|
|
23
|
+
|
|
16
24
|
const listRef = useRef<HTMLDivElement>(null);
|
|
17
25
|
const toastRef = useRef<{ error: (msg: string) => void; success: (msg: string) => void; info: (msg: string) => void } | null>(null);
|
|
18
26
|
|
|
19
|
-
// Auto-scroll on new messages
|
|
27
|
+
// Auto-scroll on new messages (both streams)
|
|
20
28
|
useEffect(() => {
|
|
21
29
|
if (listRef.current) {
|
|
22
30
|
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
23
31
|
}
|
|
24
|
-
}, [
|
|
32
|
+
}, [bizarMessages, opencodeMessages]);
|
|
25
33
|
|
|
34
|
+
// ── Load bizar chat ─────────────────────────────────────────────────────────
|
|
26
35
|
const loadChat = useCallback(async (sid?: string) => {
|
|
27
36
|
try {
|
|
28
37
|
const url = sid ? `/chat?session=${encodeURIComponent(sid)}` : '/chat?limit=200';
|
|
29
38
|
const data = await api.get<ChatResponse>(url);
|
|
30
|
-
|
|
39
|
+
setBizarMessages(data.messages || []);
|
|
31
40
|
setSessions(data.sessions || []);
|
|
32
41
|
} catch (err) {
|
|
33
42
|
toastRef.current?.error(`Chat load failed: ${(err as Error).message}`);
|
|
@@ -40,7 +49,7 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
40
49
|
setLoading(true);
|
|
41
50
|
try {
|
|
42
51
|
const data = await api.get<{ sessionId?: string; messages: ChatMessage[] }>(`/tasks/${encodeURIComponent(taskId)}/chat`);
|
|
43
|
-
|
|
52
|
+
setBizarMessages(data.messages || []);
|
|
44
53
|
setSessionId(data.sessionId || taskId);
|
|
45
54
|
} catch (err) {
|
|
46
55
|
toastRef.current?.error(`Task chat load failed: ${(err as Error).message}`);
|
|
@@ -70,6 +79,120 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
70
79
|
} catch { /* best-effort */ }
|
|
71
80
|
}, []);
|
|
72
81
|
|
|
82
|
+
// ── Opencode session: load messages + open SSE ───────────────────────────────
|
|
83
|
+
const loadOpencodeSession = useCallback(async (sessionId: string) => {
|
|
84
|
+
// Close any existing opencode SSE
|
|
85
|
+
opencodeEsRef.current?.close();
|
|
86
|
+
opencodeEsRef.current = null;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const data = await api.get<{ messages: ChatMessage[] }>(`/opencode-sessions/${encodeURIComponent(sessionId)}/messages`);
|
|
90
|
+
setOpencodeMessages(data.messages || []);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
toastRef.current?.error(`Opencode session load failed: ${(err as Error).message}`);
|
|
93
|
+
setOpencodeMessages([]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setActiveOpencodeSessionId(sessionId);
|
|
97
|
+
setActiveSource('opencode');
|
|
98
|
+
|
|
99
|
+
// Open SSE — EventSource can't set headers; token goes in query string via urlWithToken
|
|
100
|
+
const tok = api.getToken();
|
|
101
|
+
const baseUrl = tok
|
|
102
|
+
? `/api/opencode-sessions/${encodeURIComponent(sessionId)}/stream?token=${encodeURIComponent(tok)}`
|
|
103
|
+
: `/api/opencode-sessions/${encodeURIComponent(sessionId)}/stream`;
|
|
104
|
+
const es = new EventSource(baseUrl);
|
|
105
|
+
opencodeEsRef.current = es;
|
|
106
|
+
|
|
107
|
+
es.onmessage = (e: MessageEvent) => {
|
|
108
|
+
// Each message is one SSE frame: "event: <type>\ndata: <json>\n\n"
|
|
109
|
+
// Split on newlines to isolate "event: …" and "data: …" lines
|
|
110
|
+
const raw = e.data;
|
|
111
|
+
if (!raw || raw.startsWith(':')) return; // skip heartbeat
|
|
112
|
+
|
|
113
|
+
let eventType = '';
|
|
114
|
+
let eventData: string | null = null;
|
|
115
|
+
|
|
116
|
+
const lines = raw.split('\n');
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
if (line.startsWith('event:')) {
|
|
119
|
+
eventType = line.slice(5).trim();
|
|
120
|
+
} else if (line.startsWith('data:')) {
|
|
121
|
+
eventData = line.slice(5).trim();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!eventType || eventData === null) return;
|
|
126
|
+
|
|
127
|
+
// Build a ChatMessage from the SSE event
|
|
128
|
+
let role: ChatMessage['role'] = 'assistant';
|
|
129
|
+
if (eventType.startsWith('message.user.')) role = 'user';
|
|
130
|
+
|
|
131
|
+
let content: string;
|
|
132
|
+
if (eventType === 'message.part.updated' && eventData !== null) {
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(eventData);
|
|
135
|
+
content = parsed.data?.part?.text ?? JSON.stringify(parsed.data ?? parsed);
|
|
136
|
+
} catch {
|
|
137
|
+
content = eventData;
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(eventData);
|
|
142
|
+
content = parsed.data ?? JSON.stringify(parsed);
|
|
143
|
+
} catch {
|
|
144
|
+
content = eventData;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Dedup: skip if we already have a message with this id (from SSE echo)
|
|
149
|
+
const msgId = (() => {
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(eventData!);
|
|
152
|
+
return parsed.messageID || parsed.id || null;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
157
|
+
|
|
158
|
+
const newMsg: ChatMessage = {
|
|
159
|
+
id: msgId ?? undefined,
|
|
160
|
+
role,
|
|
161
|
+
content,
|
|
162
|
+
ts: new Date().toISOString(),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
setOpencodeMessages((cur) => {
|
|
166
|
+
if (msgId && cur.some((m) => m.id === msgId)) return cur;
|
|
167
|
+
return [...cur, newMsg];
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
es.onerror = () => {
|
|
172
|
+
// SSE error — silently close; don't spam the user
|
|
173
|
+
try { es.close(); } catch { /* noop */ }
|
|
174
|
+
opencodeEsRef.current = null;
|
|
175
|
+
};
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
// ── Close opencode session ──────────────────────────────────────────────────
|
|
179
|
+
const closeOpencodeSession = useCallback(() => {
|
|
180
|
+
opencodeEsRef.current?.close();
|
|
181
|
+
opencodeEsRef.current = null;
|
|
182
|
+
setOpencodeMessages([]);
|
|
183
|
+
setActiveOpencodeSessionId(null);
|
|
184
|
+
setActiveSource('bizar');
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
// ── Select a bizar session ──────────────────────────────────────────────────
|
|
188
|
+
const selectBizarSession = useCallback((sid: string) => {
|
|
189
|
+
closeOpencodeSession();
|
|
190
|
+
setSessionId(sid);
|
|
191
|
+
setActiveSource('bizar');
|
|
192
|
+
loadChat(sid);
|
|
193
|
+
}, [closeOpencodeSession, loadChat]);
|
|
194
|
+
|
|
195
|
+
// ── Create session ──────────────────────────────────────────────────────────
|
|
73
196
|
const onCreateSession = useCallback(async () => {
|
|
74
197
|
if (!snapshot.activeProject) {
|
|
75
198
|
toastRef.current?.error('Pick a project in Overview to scope chat sessions.');
|
|
@@ -78,7 +201,7 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
78
201
|
try {
|
|
79
202
|
const created = await api.post<ChatSession>('/chat/sessions', {});
|
|
80
203
|
setSessionId(created.id);
|
|
81
|
-
|
|
204
|
+
setBizarMessages([]);
|
|
82
205
|
setPinned(new Set());
|
|
83
206
|
await loadChat(created.id);
|
|
84
207
|
refreshSessions().catch(() => {});
|
|
@@ -87,36 +210,66 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
87
210
|
}
|
|
88
211
|
}, [snapshot.activeProject, loadChat, refreshSessions]);
|
|
89
212
|
|
|
213
|
+
// ── Send message ────────────────────────────────────────────────────────────
|
|
90
214
|
const onSend = useCallback(async (message: string, agent: string, model: string, attachments?: string[]) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
215
|
+
if (activeSource === 'opencode' && activeOpencodeSessionId) {
|
|
216
|
+
// Optimistic add
|
|
217
|
+
const optimisticId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
218
|
+
const optimistic: ChatMessage = {
|
|
219
|
+
id: optimisticId,
|
|
220
|
+
role: 'user',
|
|
221
|
+
content: message,
|
|
222
|
+
agent,
|
|
223
|
+
ts: new Date().toISOString(),
|
|
224
|
+
};
|
|
225
|
+
setOpencodeMessages((cur) => [...cur, optimistic]);
|
|
226
|
+
setSending(true);
|
|
227
|
+
try {
|
|
228
|
+
const result = await api.post<{ ok: boolean; messageId: string }>(
|
|
229
|
+
`/opencode-sessions/${encodeURIComponent(activeOpencodeSessionId)}/send`,
|
|
230
|
+
{ message, agent },
|
|
231
|
+
);
|
|
232
|
+
// SSE echo will dedup on messageId === optimisticId or server-provided id
|
|
233
|
+
void result;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
// Remove optimistic on failure
|
|
236
|
+
setOpencodeMessages((cur) => cur.filter((m) => m.id !== optimisticId));
|
|
237
|
+
toastRef.current?.error(`Send failed: ${(err as Error).message}`);
|
|
238
|
+
} finally {
|
|
239
|
+
setSending(false);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
// Default: send to bizar chat
|
|
243
|
+
const optimistic: ChatMessage = {
|
|
244
|
+
role: 'user',
|
|
245
|
+
content: message,
|
|
246
|
+
agent,
|
|
247
|
+
ts: new Date().toISOString(),
|
|
248
|
+
};
|
|
249
|
+
setBizarMessages((cur) => [...cur, optimistic]);
|
|
250
|
+
setSending(true);
|
|
251
|
+
try {
|
|
252
|
+
const response = await api.post<{ messages?: ChatMessage[] }>('/chat', { message, agent, model, attachments: attachments || [] });
|
|
253
|
+
if (response?.messages) {
|
|
254
|
+
for (const msg of response.messages) {
|
|
255
|
+
if (msg.role === 'assistant') {
|
|
256
|
+
setBizarMessages((cur) => {
|
|
257
|
+
const exists = cur.some((m) => m.ts === msg.ts);
|
|
258
|
+
return exists ? cur : [...cur, msg];
|
|
259
|
+
});
|
|
260
|
+
}
|
|
108
261
|
}
|
|
262
|
+
} else {
|
|
263
|
+
toastRef.current?.info('Still processing… the response will appear shortly.');
|
|
109
264
|
}
|
|
110
|
-
}
|
|
111
|
-
|
|
265
|
+
} catch (err) {
|
|
266
|
+
setBizarMessages((cur) => cur.filter((m) => m !== optimistic));
|
|
267
|
+
toastRef.current?.error(`Send failed: ${(err as Error).message}`);
|
|
268
|
+
} finally {
|
|
269
|
+
setSending(false);
|
|
112
270
|
}
|
|
113
|
-
} catch (err) {
|
|
114
|
-
setMessages((cur) => cur.filter((m) => m !== optimistic));
|
|
115
|
-
toastRef.current?.error(`Send failed: ${(err as Error).message}`);
|
|
116
|
-
} finally {
|
|
117
|
-
setSending(false);
|
|
118
271
|
}
|
|
119
|
-
}, []);
|
|
272
|
+
}, [activeSource, activeOpencodeSessionId]);
|
|
120
273
|
|
|
121
274
|
const onRegenerate = useCallback(async (messageId: string) => {
|
|
122
275
|
if (!sessionId) {
|
|
@@ -132,8 +285,12 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
132
285
|
}, [sessionId, loadChat]);
|
|
133
286
|
|
|
134
287
|
const deleteMessage = useCallback((idx: number) => {
|
|
135
|
-
|
|
136
|
-
|
|
288
|
+
if (activeSource === 'opencode') {
|
|
289
|
+
setOpencodeMessages((cur) => cur.filter((_, i) => i !== idx));
|
|
290
|
+
} else {
|
|
291
|
+
setBizarMessages((cur) => cur.filter((_, i) => i !== idx));
|
|
292
|
+
}
|
|
293
|
+
}, [activeSource]);
|
|
137
294
|
|
|
138
295
|
const togglePin = useCallback((idx: number) => {
|
|
139
296
|
setPinned((cur) => {
|
|
@@ -152,7 +309,15 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
152
309
|
);
|
|
153
310
|
}, []);
|
|
154
311
|
|
|
155
|
-
//
|
|
312
|
+
// ── Cleanup SSE on unmount ──────────────────────────────────────────────────
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
return () => {
|
|
315
|
+
opencodeEsRef.current?.close();
|
|
316
|
+
opencodeEsRef.current = null;
|
|
317
|
+
};
|
|
318
|
+
}, []);
|
|
319
|
+
|
|
320
|
+
// ── Initial load ────────────────────────────────────────────────────────────
|
|
156
321
|
useEffect(() => {
|
|
157
322
|
if (initialTaskId) {
|
|
158
323
|
loadTaskChat(initialTaskId);
|
|
@@ -163,7 +328,7 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
163
328
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
164
329
|
}, []);
|
|
165
330
|
|
|
166
|
-
// Listen for bizar:setChatTask events
|
|
331
|
+
// ── Listen for bizar:setChatTask events ────────────────────────────────────
|
|
167
332
|
useEffect(() => {
|
|
168
333
|
const handler = (e: Event) => {
|
|
169
334
|
const taskId = (e as CustomEvent<{ taskId: string }>).detail?.taskId;
|
|
@@ -173,7 +338,7 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
173
338
|
return () => window.removeEventListener('bizar:setChatTask', handler);
|
|
174
339
|
}, [loadTaskChat]);
|
|
175
340
|
|
|
176
|
-
// WebSocket for live updates
|
|
341
|
+
// ── WebSocket for live bizar updates ────────────────────────────────────────
|
|
177
342
|
useEffect(() => {
|
|
178
343
|
let closed = false;
|
|
179
344
|
import('../../lib/ws').then(({ Ws }) => {
|
|
@@ -182,7 +347,7 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
182
347
|
ws.on((msg) => {
|
|
183
348
|
if (msg.type !== 'chat:message') return;
|
|
184
349
|
const next = msg.message;
|
|
185
|
-
|
|
350
|
+
setBizarMessages((cur) => {
|
|
186
351
|
const exists = cur.some((m) => m.ts === next.ts);
|
|
187
352
|
return exists ? cur : [...cur, next];
|
|
188
353
|
});
|
|
@@ -192,8 +357,18 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
192
357
|
}, []);
|
|
193
358
|
|
|
194
359
|
return {
|
|
195
|
-
// State
|
|
196
|
-
|
|
360
|
+
// ── State ────────────────────────────────────────────────────────────────
|
|
361
|
+
/**
|
|
362
|
+
* Messages from the active source.
|
|
363
|
+
* Use `activeSource === 'opencode' ? opencodeMessages : bizarMessages`.
|
|
364
|
+
* Kept as `messages` alias for backward compatibility with components
|
|
365
|
+
* that read `chat.messages` directly.
|
|
366
|
+
*/
|
|
367
|
+
messages: activeSource === 'opencode' ? opencodeMessages : bizarMessages,
|
|
368
|
+
/** Messages from the bizar chat stream. */
|
|
369
|
+
bizarMessages,
|
|
370
|
+
/** Messages from the opencode session stream. */
|
|
371
|
+
opencodeMessages,
|
|
197
372
|
sessions,
|
|
198
373
|
opencodeSessions,
|
|
199
374
|
sessionId,
|
|
@@ -202,13 +377,24 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
202
377
|
sending,
|
|
203
378
|
pinned,
|
|
204
379
|
listRef,
|
|
205
|
-
//
|
|
380
|
+
// ── Opencode state ──────────────────────────────────────────────────────
|
|
381
|
+
/** Which source is currently displayed: 'bizar', 'opencode', or null. */
|
|
382
|
+
activeSource,
|
|
383
|
+
/** The opencode session ID currently selected, or null. */
|
|
384
|
+
activeOpencodeSessionId,
|
|
385
|
+
// ── Setters for toast integration ───────────────────────────────────────
|
|
206
386
|
setToast: (toast: typeof toastRef.current) => { toastRef.current = toast; },
|
|
207
|
-
// Actions
|
|
387
|
+
// ── Actions ─────────────────────────────────────────────────────────────
|
|
208
388
|
loadChat,
|
|
209
389
|
loadTaskChat,
|
|
210
390
|
refreshSessions,
|
|
211
391
|
refreshOpencodeSessions,
|
|
392
|
+
/** Load an opencode session's messages and open its SSE stream. */
|
|
393
|
+
loadOpencodeSession,
|
|
394
|
+
/** Close the opencode SSE and fall back to bizar. */
|
|
395
|
+
closeOpencodeSession,
|
|
396
|
+
/** Select a bizar session (closes any opencode SSE first). */
|
|
397
|
+
selectBizarSession,
|
|
212
398
|
onCreateSession,
|
|
213
399
|
onSend,
|
|
214
400
|
onRegenerate,
|
|
@@ -216,4 +402,4 @@ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?:
|
|
|
216
402
|
togglePin,
|
|
217
403
|
copyMessage,
|
|
218
404
|
};
|
|
219
|
-
}
|
|
405
|
+
}
|
|
@@ -121,19 +121,21 @@ export function MobileChat({ snapshot, settings, setActiveTab, initialTaskId, on
|
|
|
121
121
|
sessions={chat.sessions}
|
|
122
122
|
opencodeSessions={chat.opencodeSessions}
|
|
123
123
|
activeSessionId={chat.sessionId}
|
|
124
|
+
activeOpencodeSessionId={chat.activeOpencodeSessionId}
|
|
124
125
|
activeProject={snapshot.activeProject}
|
|
125
126
|
onCreateSession={handleCreateSession}
|
|
126
|
-
onSelectSession={chat.
|
|
127
|
+
onSelectSession={chat.selectBizarSession}
|
|
128
|
+
onSelectOpencodeSession={(s) => chat.loadOpencodeSession(s.id)}
|
|
127
129
|
creating={creating}
|
|
128
130
|
/>
|
|
129
131
|
</aside>
|
|
130
132
|
|
|
131
133
|
<main className="chat-main">
|
|
132
134
|
<ChatThread
|
|
133
|
-
messages={chat.
|
|
135
|
+
messages={chat.activeSource === 'opencode' ? chat.opencodeMessages : chat.bizarMessages}
|
|
134
136
|
loading={chat.loading}
|
|
135
137
|
activeProject={snapshot.activeProject}
|
|
136
|
-
sessionId={chat.sessionId}
|
|
138
|
+
sessionId={chat.activeSource === 'opencode' ? (chat.activeOpencodeSessionId ?? chat.sessionId) : chat.sessionId}
|
|
137
139
|
pinned={chat.pinned}
|
|
138
140
|
onPickSuggestion={(t) => setText(t)}
|
|
139
141
|
onCopy={(m) => chat.copyMessage(m as Parameters<typeof chat.copyMessage>[0])}
|
|
@@ -164,8 +166,8 @@ export function MobileChat({ snapshot, settings, setActiveTab, initialTaskId, on
|
|
|
164
166
|
|
|
165
167
|
<aside className={`chat-info ${!infoOpen ? 'chat-info-hidden' : ''}`}>
|
|
166
168
|
<InfoPanel
|
|
167
|
-
sessionId={chat.sessionId}
|
|
168
|
-
messages={chat.
|
|
169
|
+
sessionId={chat.activeSource === 'opencode' ? (chat.activeOpencodeSessionId ?? chat.sessionId) : chat.sessionId}
|
|
170
|
+
messages={chat.activeSource === 'opencode' ? chat.opencodeMessages : chat.bizarMessages}
|
|
169
171
|
pinned={chat.pinned}
|
|
170
172
|
agent={agent}
|
|
171
173
|
model={model}
|
|
@@ -131,19 +131,21 @@ export function Chat({ snapshot, settings, setActiveTab, initialTaskId, onClearT
|
|
|
131
131
|
sessions={chat.sessions}
|
|
132
132
|
opencodeSessions={chat.opencodeSessions}
|
|
133
133
|
activeSessionId={chat.sessionId}
|
|
134
|
+
activeOpencodeSessionId={chat.activeOpencodeSessionId}
|
|
134
135
|
activeProject={snapshot.activeProject}
|
|
135
136
|
onCreateSession={handleCreateSession}
|
|
136
|
-
onSelectSession={chat.
|
|
137
|
+
onSelectSession={chat.selectBizarSession}
|
|
138
|
+
onSelectOpencodeSession={(s) => chat.loadOpencodeSession(s.id)}
|
|
137
139
|
creating={creating}
|
|
138
140
|
/>
|
|
139
141
|
</aside>
|
|
140
142
|
|
|
141
143
|
<main className="chat-main">
|
|
142
144
|
<ChatThread
|
|
143
|
-
messages={chat.
|
|
145
|
+
messages={chat.activeSource === 'opencode' ? chat.opencodeMessages : chat.bizarMessages}
|
|
144
146
|
loading={chat.loading}
|
|
145
147
|
activeProject={snapshot.activeProject}
|
|
146
|
-
sessionId={chat.sessionId}
|
|
148
|
+
sessionId={chat.activeSource === 'opencode' ? (chat.activeOpencodeSessionId ?? chat.sessionId) : chat.sessionId}
|
|
147
149
|
pinned={chat.pinned}
|
|
148
150
|
onPickSuggestion={(t) => setText(t)}
|
|
149
151
|
onCopy={(m) => chat.copyMessage(m as Parameters<typeof chat.copyMessage>[0])}
|
|
@@ -174,8 +176,8 @@ export function Chat({ snapshot, settings, setActiveTab, initialTaskId, onClearT
|
|
|
174
176
|
|
|
175
177
|
<aside className="chat-info">
|
|
176
178
|
<InfoPanel
|
|
177
|
-
sessionId={chat.sessionId}
|
|
178
|
-
messages={chat.
|
|
179
|
+
sessionId={chat.activeSource === 'opencode' ? (chat.activeOpencodeSessionId ?? chat.sessionId) : chat.sessionId}
|
|
180
|
+
messages={chat.activeSource === 'opencode' ? chat.opencodeMessages : chat.bizarMessages}
|
|
179
181
|
pinned={chat.pinned}
|
|
180
182
|
agent={agent}
|
|
181
183
|
model={model}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/dash-cli-load.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Regression test for the "is not iterable" bug in loadDashCli (v4.2.4).
|
|
5
|
+
* The async IIFE was being spread into an array literal before resolving,
|
|
6
|
+
* producing: TypeError: (intermediate value) is not iterable.
|
|
7
|
+
*
|
|
8
|
+
* Run with:
|
|
9
|
+
* node --test tests/dash-cli-load.test.mjs
|
|
10
|
+
*/
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const CLI = path.resolve(__dirname, '../../cli/bin.mjs');
|
|
19
|
+
|
|
20
|
+
test('bizar dash status does not throw "is not iterable" (regression for v4.2.4)', () => {
|
|
21
|
+
const result = spawnSync('node', [CLI, 'dash', 'status'], {
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
timeout: 15_000,
|
|
24
|
+
});
|
|
25
|
+
const combined = (result.stdout || '') + (result.stderr || '');
|
|
26
|
+
assert.ok(
|
|
27
|
+
!combined.includes('is not iterable'),
|
|
28
|
+
`Expected no "is not iterable" error, got: ${combined.slice(0, 300)}`,
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('bizar dash status exits cleanly (0) or with a non-iterable error', () => {
|
|
33
|
+
const result = spawnSync('node', [CLI, 'dash', 'status'], {
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
timeout: 15_000,
|
|
36
|
+
});
|
|
37
|
+
// Exit code 0 = dashboard status resolved successfully.
|
|
38
|
+
// Non-zero = dashboard not running (acceptable), but must not be the
|
|
39
|
+
// "is not iterable" TypeError from the broken spread operator.
|
|
40
|
+
const acceptableExit = result.status === 0 || result.status === 1;
|
|
41
|
+
const noIterableError = !((result.stdout || '') + (result.stderr || '')).includes('is not iterable');
|
|
42
|
+
assert.ok(acceptableExit && noIterableError,
|
|
43
|
+
`Unexpected result: exit=${result.status}, output=${((result.stdout || '') + (result.stderr || '')).slice(0, 200)}`);
|
|
44
|
+
});
|