sparkecoder 0.1.51 → 0.1.53
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/agent/index.d.ts +2 -2
- package/dist/{index-BYOHniN4.d.ts → index-dJv_dqUq.d.ts} +5 -5
- package/dist/index.d.ts +3 -3
- package/dist/{search-DALwmPRX.d.ts → search-DrztQ_iP.d.ts} +5 -5
- package/dist/tools/index.d.ts +2 -2
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/web/.next/server/app/(main)/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/api/config/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/api/health/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/embed/[id]/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/embed/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/web/.next/server/app/index.rsc +4 -4
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_40c1da73._.js → 2374f_06a5ea48._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_ec658806._.js → 2374f_2572135b._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_c8f8a326._.js → 2374f_273991ba._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_13da7b2a._.js → 2374f_460c0d78._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_1de2f628._.js → 2374f_5ee28d4c._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_9f2fae06._.js → 2374f_65b86b54._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_f3c65774._.js → 2374f_7b421f78._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_41d07cce._.js → 2374f_88cbeb7b._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_2bfcd3a2._.js → 2374f_8d018190._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_e5142825._.js → 2374f_a7457131._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_dd21a4e9._.js → 2374f_c9e3cd7b._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_88aa1671._.js → 2374f_de035f79._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_ac79ddf4._.js → 2374f_df3c414e._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_6d8e5e94._.js → 2374f_ec997752._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__a87e1c27._.js +3 -3
- package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__43405ad6._.js → [root-of-the-server]__c94aac98._.js} +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/{web_273500a6._.js → web_9c9f0e3b._.js} +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/{web_329773d1._.js → web_b85931da._.js} +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/{web_8305f089._.js → web_d08270f7._.js} +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_components_sessions-sidebar_tsx_92510070._.js +1 -1
- package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/{c25d80326ea739f3.js → 1685d98ff8751ba3.js} +3 -3
- package/web/.next/standalone/web/.next/static/chunks/3f295b6960943c38.js +1 -0
- package/web/.next/standalone/web/.next/static/chunks/7228b2394d1fb347.css +1 -0
- package/web/.next/standalone/web/.next/static/chunks/a2b4737b190d1b54.js +5 -0
- package/web/.next/standalone/web/.next/static/{static/chunks/b7d722dab2a7ecc0.js → chunks/e97212fcc8221479.js} +2 -2
- package/web/.next/standalone/web/.next/static/chunks/f6e47c8a9766ce91.js +7 -0
- package/web/.next/standalone/web/.next/static/static/chunks/{c25d80326ea739f3.js → 1685d98ff8751ba3.js} +3 -3
- package/web/.next/standalone/web/.next/static/static/chunks/3f295b6960943c38.js +1 -0
- package/web/.next/standalone/web/.next/static/static/chunks/7228b2394d1fb347.css +1 -0
- package/web/.next/standalone/web/.next/static/static/chunks/a2b4737b190d1b54.js +5 -0
- package/web/.next/{static/chunks/b7d722dab2a7ecc0.js → standalone/web/.next/static/static/chunks/e97212fcc8221479.js} +2 -2
- package/web/.next/standalone/web/.next/static/static/chunks/f6e47c8a9766ce91.js +7 -0
- package/web/.next/standalone/web/src/components/ai-elements/search-tool.tsx +26 -3
- package/web/.next/standalone/web/src/components/ai-elements/subagent-modal.tsx +13 -1
- package/web/.next/standalone/web/src/components/chat-interface.tsx +98 -62
- package/web/.next/standalone/web/src/components/sessions-sidebar.tsx +23 -1
- package/web/.next/standalone/web/src/hooks/use-notification-sound.ts +78 -0
- package/web/.next/static/chunks/{c25d80326ea739f3.js → 1685d98ff8751ba3.js} +3 -3
- package/web/.next/static/chunks/3f295b6960943c38.js +1 -0
- package/web/.next/static/chunks/7228b2394d1fb347.css +1 -0
- package/web/.next/static/chunks/a2b4737b190d1b54.js +5 -0
- package/web/.next/{standalone/web/.next/static/chunks/b7d722dab2a7ecc0.js → static/chunks/e97212fcc8221479.js} +2 -2
- package/web/.next/static/chunks/f6e47c8a9766ce91.js +7 -0
- package/web/.next/standalone/web/.next/static/chunks/96ec96279ada0efe.js +0 -1
- package/web/.next/standalone/web/.next/static/chunks/cc6d43f798bbe415.js +0 -5
- package/web/.next/standalone/web/.next/static/chunks/de300ff10b63be85.css +0 -1
- package/web/.next/standalone/web/.next/static/chunks/de71e63276f488bb.js +0 -7
- package/web/.next/standalone/web/.next/static/static/chunks/96ec96279ada0efe.js +0 -1
- package/web/.next/standalone/web/.next/static/static/chunks/cc6d43f798bbe415.js +0 -5
- package/web/.next/standalone/web/.next/static/static/chunks/de300ff10b63be85.css +0 -1
- package/web/.next/standalone/web/.next/static/static/chunks/de71e63276f488bb.js +0 -7
- package/web/.next/static/chunks/96ec96279ada0efe.js +0 -1
- package/web/.next/static/chunks/cc6d43f798bbe415.js +0 -5
- package/web/.next/static/chunks/de300ff10b63be85.css +0 -1
- package/web/.next/static/chunks/de71e63276f488bb.js +0 -7
- /package/web/.next/standalone/web/.next/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_ssgManifest.js +0 -0
- /package/web/.next/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_buildManifest.js +0 -0
- /package/web/.next/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_ssgManifest.js +0 -0
|
@@ -116,6 +116,7 @@ import {
|
|
|
116
116
|
import { TodoPanel } from '@/components/ai-elements/todo-panel';
|
|
117
117
|
import { getConfig, type AppConfig } from '@/lib/config';
|
|
118
118
|
import { mutateSessions } from '@/hooks/use-sessions';
|
|
119
|
+
import { useNotificationSound } from '@/hooks/use-notification-sound';
|
|
119
120
|
import {
|
|
120
121
|
Select,
|
|
121
122
|
SelectContent,
|
|
@@ -368,6 +369,10 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
|
|
|
368
369
|
const [pendingSelectedElements, setPendingSelectedElements] = useState<string | null>(null);
|
|
369
370
|
const [isRunning, setIsRunning] = useState(false);
|
|
370
371
|
const [messageQueue, setMessageQueue] = useState<QueuedMessage[]>([]);
|
|
372
|
+
// Ref mirror of messageQueue so SSE event handlers can read the latest queue
|
|
373
|
+
// without stale closures. Updated on every render.
|
|
374
|
+
const messageQueueRef = useRef<QueuedMessage[]>([]);
|
|
375
|
+
messageQueueRef.current = messageQueue;
|
|
371
376
|
const [editingQueueId, setEditingQueueId] = useState<string | null>(null);
|
|
372
377
|
const [editingQueueText, setEditingQueueText] = useState('');
|
|
373
378
|
const [queueExpanded, setQueueExpanded] = useState(true);
|
|
@@ -405,6 +410,13 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
|
|
|
405
410
|
const [sessionSettingsOpen, setSessionSettingsOpen] = useState(false);
|
|
406
411
|
const nameInputRef = useRef<HTMLInputElement>(null);
|
|
407
412
|
|
|
413
|
+
// Track current session ID via ref so async callbacks can check for staleness
|
|
414
|
+
const sessionIdRef = useRef(session.id);
|
|
415
|
+
sessionIdRef.current = session.id;
|
|
416
|
+
|
|
417
|
+
// Notification sound
|
|
418
|
+
const { playDing } = useNotificationSound();
|
|
419
|
+
|
|
408
420
|
// Version check state
|
|
409
421
|
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null);
|
|
410
422
|
const [updateBannerDismissed, setUpdateBannerDismissed] = useState(false);
|
|
@@ -1172,16 +1184,19 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
|
|
|
1172
1184
|
setCurrentToolCalls([...toolCallsRef.current]);
|
|
1173
1185
|
|
|
1174
1186
|
// Add to chatItems (or update if already there from API load)
|
|
1187
|
+
// Spread completedTool first to preserve extra fields like exploreSteps,
|
|
1188
|
+
// then override with the final output and status
|
|
1175
1189
|
const completedToolItem: ChatItem = {
|
|
1176
1190
|
id: `tool-${event.toolCallId}`,
|
|
1177
1191
|
type: 'tool-call',
|
|
1178
1192
|
toolCall: {
|
|
1193
|
+
...(completedTool || {}),
|
|
1179
1194
|
toolCallId: event.toolCallId,
|
|
1180
1195
|
toolName: completedTool?.toolName || 'unknown',
|
|
1181
1196
|
input: completedTool?.input || {},
|
|
1182
1197
|
output: unwrappedOutput,
|
|
1183
1198
|
status: finalStatus,
|
|
1184
|
-
},
|
|
1199
|
+
} as ToolCallInfo,
|
|
1185
1200
|
};
|
|
1186
1201
|
|
|
1187
1202
|
setChatItems((prev) => {
|
|
@@ -1224,6 +1239,47 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
|
|
|
1224
1239
|
break;
|
|
1225
1240
|
|
|
1226
1241
|
case 'finish':
|
|
1242
|
+
// Flush any remaining streaming text into chatItems BEFORE clearing
|
|
1243
|
+
if (currentTextRef.current.trim()) {
|
|
1244
|
+
const remainingTextItem: ChatItem = {
|
|
1245
|
+
id: `text-finish-${Date.now()}`,
|
|
1246
|
+
type: 'assistant-text',
|
|
1247
|
+
content: currentTextRef.current,
|
|
1248
|
+
};
|
|
1249
|
+
setChatItems((prev) => [...prev, remainingTextItem]);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Flush any remaining reasoning
|
|
1253
|
+
if (currentReasoningRef.current.trim()) {
|
|
1254
|
+
const remainingReasoningItem: ChatItem = {
|
|
1255
|
+
id: `reasoning-finish-${Date.now()}`,
|
|
1256
|
+
type: 'reasoning',
|
|
1257
|
+
content: currentReasoningRef.current,
|
|
1258
|
+
};
|
|
1259
|
+
setChatItems((prev) => [...prev, remainingReasoningItem]);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Move any remaining streaming tool calls into chatItems so they stay visible
|
|
1263
|
+
// (e.g. tool calls whose output arrived but weren't yet committed)
|
|
1264
|
+
if (toolCallsRef.current.length > 0) {
|
|
1265
|
+
const remainingTools: ChatItem[] = toolCallsRef.current.map((tc) => ({
|
|
1266
|
+
id: `tool-${tc.toolCallId}`,
|
|
1267
|
+
type: 'tool-call' as const,
|
|
1268
|
+
toolCall: {
|
|
1269
|
+
...tc,
|
|
1270
|
+
status: tc.status === 'streaming' || tc.status === 'running' ? 'completed' as const : tc.status,
|
|
1271
|
+
},
|
|
1272
|
+
}));
|
|
1273
|
+
setChatItems((prev) => {
|
|
1274
|
+
// Only add tools that aren't already in chatItems
|
|
1275
|
+
const existingToolIds = new Set(
|
|
1276
|
+
prev.filter(i => i.type === 'tool-call').map(i => i.toolCall?.toolCallId)
|
|
1277
|
+
);
|
|
1278
|
+
const newTools = remainingTools.filter(t => !existingToolIds.has(t.toolCall?.toolCallId));
|
|
1279
|
+
return newTools.length > 0 ? [...prev, ...newTools] : prev;
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1227
1283
|
// Reset streaming state
|
|
1228
1284
|
setCurrentText('');
|
|
1229
1285
|
setCurrentReasoning('');
|
|
@@ -1239,21 +1295,34 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
|
|
|
1239
1295
|
// Don't reset lastKnownStreamIdRef here - it helps prevent reconnecting to same stream
|
|
1240
1296
|
setIsRunning(false);
|
|
1241
1297
|
setIsWatching(false);
|
|
1298
|
+
playDing();
|
|
1242
1299
|
|
|
1243
|
-
//
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1300
|
+
// Only refresh checkpoints (for revert buttons) - NOT chatItems.
|
|
1301
|
+
{
|
|
1302
|
+
const finishSessionId = session.id;
|
|
1303
|
+
getSessionCheckpoints(finishSessionId)
|
|
1304
|
+
.then((checkpointsData) => {
|
|
1305
|
+
if (sessionIdRef.current !== finishSessionId) return;
|
|
1306
|
+
setCheckpoints(checkpointsData.checkpoints || []);
|
|
1307
|
+
})
|
|
1308
|
+
.catch(() => {});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Send next queued message directly from the finish event.
|
|
1312
|
+
// This is the correct place to do it: one event → one send.
|
|
1313
|
+
// No useEffect, no timers, no race conditions.
|
|
1314
|
+
{
|
|
1315
|
+
const queue = messageQueueRef.current;
|
|
1316
|
+
if (queue.length > 0) {
|
|
1317
|
+
const next = queue[0];
|
|
1318
|
+
setMessageQueue((prev) => prev.slice(1));
|
|
1319
|
+
// Small delay so React can commit the state reset above first,
|
|
1320
|
+
// then start the new stream in a fresh render cycle
|
|
1321
|
+
setTimeout(() => {
|
|
1322
|
+
executeSubmitRef.current(next.text, next.attachments, next.selectedElements);
|
|
1323
|
+
}, 50);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1257
1326
|
break;
|
|
1258
1327
|
|
|
1259
1328
|
case 'abort':
|
|
@@ -1794,46 +1863,12 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
|
|
|
1794
1863
|
setIsWatching(false);
|
|
1795
1864
|
};
|
|
1796
1865
|
|
|
1797
|
-
//
|
|
1798
|
-
// latest version (avoids stale closure capturing old handleSSEEvent, etc.)
|
|
1866
|
+
// Ref to executeSubmit so SSE handlers and sendNow can always call the latest version
|
|
1799
1867
|
const executeSubmitRef = useRef(executeSubmit);
|
|
1800
1868
|
executeSubmitRef.current = executeSubmit;
|
|
1801
|
-
|
|
1802
|
-
//
|
|
1803
|
-
|
|
1804
|
-
const skipAutoSendRef = useRef(false); // Set by sendNow to prevent double-send
|
|
1805
|
-
useEffect(() => {
|
|
1806
|
-
const wasRunning = prevIsRunningRef.current;
|
|
1807
|
-
prevIsRunningRef.current = isRunning;
|
|
1808
|
-
|
|
1809
|
-
// Auto-send whenever we're idle and there are queued messages.
|
|
1810
|
-
// This covers both:
|
|
1811
|
-
// 1. Agent just finished (wasRunning=true → isRunning=false) with a queue
|
|
1812
|
-
// 2. Queue got a new item while already idle (e.g., agent finished BEFORE
|
|
1813
|
-
// the user queued a message, so the running→stopped transition was missed)
|
|
1814
|
-
// Safe because executeSubmit sets isRunning=true, preventing re-entry
|
|
1815
|
-
// until the next agent response finishes.
|
|
1816
|
-
const shouldAutoSend = !isRunning && messageQueue.length > 0;
|
|
1817
|
-
|
|
1818
|
-
if (shouldAutoSend) {
|
|
1819
|
-
// If sendNow triggered the stop, skip auto-send (it handles its own send)
|
|
1820
|
-
if (skipAutoSendRef.current) {
|
|
1821
|
-
skipAutoSendRef.current = false;
|
|
1822
|
-
return;
|
|
1823
|
-
}
|
|
1824
|
-
// Small delay to let UI settle
|
|
1825
|
-
const timer = setTimeout(() => {
|
|
1826
|
-
setMessageQueue((prev) => {
|
|
1827
|
-
if (prev.length === 0) return prev;
|
|
1828
|
-
const [next, ...rest] = prev;
|
|
1829
|
-
// Use ref to always call the latest executeSubmit (not a stale closure)
|
|
1830
|
-
executeSubmitRef.current(next.text, next.attachments, next.selectedElements);
|
|
1831
|
-
return rest;
|
|
1832
|
-
});
|
|
1833
|
-
}, 300);
|
|
1834
|
-
return () => clearTimeout(timer);
|
|
1835
|
-
}
|
|
1836
|
-
}, [isRunning, messageQueue.length]);
|
|
1869
|
+
|
|
1870
|
+
// Note: Queue auto-send is handled directly in the 'finish' SSE event handler,
|
|
1871
|
+
// not via useEffect. This avoids timers, stale closures, and double-fires.
|
|
1837
1872
|
|
|
1838
1873
|
// Queue management helpers
|
|
1839
1874
|
const removeFromQueue = (id: string) => {
|
|
@@ -1870,11 +1905,9 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
|
|
|
1870
1905
|
// Remove from queue
|
|
1871
1906
|
setMessageQueue((prev) => prev.filter((m) => m.id !== id));
|
|
1872
1907
|
|
|
1873
|
-
// Stop current if running
|
|
1908
|
+
// Stop current if running, then send after it settles
|
|
1874
1909
|
if (isRunning) {
|
|
1875
|
-
skipAutoSendRef.current = true; // Prevent auto-send from firing
|
|
1876
1910
|
await handleStop();
|
|
1877
|
-
// Wait for stop to settle, then send using ref for fresh closure
|
|
1878
1911
|
setTimeout(() => {
|
|
1879
1912
|
executeSubmitRef.current(item.text, item.attachments, item.selectedElements);
|
|
1880
1913
|
}, 500);
|
|
@@ -2616,14 +2649,17 @@ export function ChatInterface({ session, isEmbed = false }: ChatInterfaceProps)
|
|
|
2616
2649
|
);
|
|
2617
2650
|
if (isPendingApproval) return false;
|
|
2618
2651
|
|
|
2619
|
-
// Don't show if
|
|
2652
|
+
// Don't show if this exact tool call is currently in the streaming section
|
|
2620
2653
|
const isStreaming = currentToolCalls.some(
|
|
2621
|
-
(tc) => tc.toolCallId === item.toolCall?.toolCallId
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2654
|
+
(tc) => tc.toolCallId === item.toolCall?.toolCallId
|
|
2655
|
+
);
|
|
2656
|
+
// Also hide if there's a placeholder that will be replaced by this tool
|
|
2657
|
+
// (placeholder toolCallIds start with the toolName + "_pending_")
|
|
2658
|
+
const hasPlaceholder = currentToolCalls.some(
|
|
2659
|
+
(tc) => tc.toolCallId.startsWith(`${item.toolCall?.toolName}_pending_`) &&
|
|
2660
|
+
(tc.status === 'streaming' || tc.status === 'running')
|
|
2625
2661
|
);
|
|
2626
|
-
if (isStreaming) return false;
|
|
2662
|
+
if (isStreaming || hasPlaceholder) return false;
|
|
2627
2663
|
}
|
|
2628
2664
|
return true;
|
|
2629
2665
|
})
|
|
@@ -4,7 +4,7 @@ import { useState, useEffect, useRef } from 'react';
|
|
|
4
4
|
import { useRouter, usePathname } from 'next/navigation';
|
|
5
5
|
import Link from 'next/link';
|
|
6
6
|
import Image from 'next/image';
|
|
7
|
-
import { Plus, MessageSquare, Trash2, Loader2, Settings, PanelLeftClose, PanelLeft, Key, Check, X, Eye, EyeOff } from 'lucide-react';
|
|
7
|
+
import { Plus, MessageSquare, Trash2, Loader2, Settings, PanelLeftClose, PanelLeft, Key, Check, X, Eye, EyeOff, Volume2 } from 'lucide-react';
|
|
8
8
|
import {
|
|
9
9
|
Sidebar,
|
|
10
10
|
SidebarContent,
|
|
@@ -45,6 +45,7 @@ import { createSession, deleteSession, getApiKeys, setApiKey, type ApiKeyStatus
|
|
|
45
45
|
import { getConfig, type AppConfig } from '@/lib/config';
|
|
46
46
|
import { cn } from '@/lib/utils';
|
|
47
47
|
import { useSessions, mutateSessions } from '@/hooks/use-sessions';
|
|
48
|
+
import { useNotificationSound } from '@/hooks/use-notification-sound';
|
|
48
49
|
|
|
49
50
|
// Format relative time like "2m", "1h", "3d"
|
|
50
51
|
function formatRelativeTime(dateString: string): string {
|
|
@@ -87,6 +88,9 @@ export function SessionsSidebar() {
|
|
|
87
88
|
const [showApiKey, setShowApiKey] = useState(false);
|
|
88
89
|
const [savingApiKey, setSavingApiKey] = useState(false);
|
|
89
90
|
|
|
91
|
+
// Notification sound setting
|
|
92
|
+
const { enabled: soundEnabled, setEnabled: setSoundEnabled } = useNotificationSound();
|
|
93
|
+
|
|
90
94
|
// Detect if we're in embed mode
|
|
91
95
|
const isEmbedMode = pathname.startsWith('/embed/');
|
|
92
96
|
|
|
@@ -828,6 +832,24 @@ export function SessionsSidebar() {
|
|
|
828
832
|
</div>
|
|
829
833
|
</div>
|
|
830
834
|
|
|
835
|
+
{/* Notification Sound */}
|
|
836
|
+
<div className="space-y-1.5">
|
|
837
|
+
<Label className="flex items-center gap-2 text-sm font-medium">
|
|
838
|
+
<Volume2 className="size-4" />
|
|
839
|
+
Notification Sound
|
|
840
|
+
</Label>
|
|
841
|
+
<div className="flex items-center justify-between rounded-lg border p-2.5 bg-muted/30">
|
|
842
|
+
<div className="space-y-0">
|
|
843
|
+
<span className="text-sm font-medium">Play sound when done</span>
|
|
844
|
+
<p className="text-[11px] text-muted-foreground">Ding when the assistant finishes responding</p>
|
|
845
|
+
</div>
|
|
846
|
+
<Switch
|
|
847
|
+
checked={soundEnabled}
|
|
848
|
+
onCheckedChange={setSoundEnabled}
|
|
849
|
+
/>
|
|
850
|
+
</div>
|
|
851
|
+
</div>
|
|
852
|
+
|
|
831
853
|
<p className="text-xs text-muted-foreground">
|
|
832
854
|
These settings apply to new sessions. You can also change the model per-session in the chat header.
|
|
833
855
|
</p>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
const STORAGE_KEY = 'sparkecoder_notification_sound';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Synthesize a pleasant two-tone "ding" using the Web Audio API.
|
|
9
|
+
* No audio file needed – it generates the tones on the fly.
|
|
10
|
+
*/
|
|
11
|
+
function playDingSound() {
|
|
12
|
+
try {
|
|
13
|
+
const ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
|
|
14
|
+
|
|
15
|
+
// --- first tone (higher, shorter) ---
|
|
16
|
+
const osc1 = ctx.createOscillator();
|
|
17
|
+
const gain1 = ctx.createGain();
|
|
18
|
+
osc1.type = 'sine';
|
|
19
|
+
osc1.frequency.setValueAtTime(880, ctx.currentTime); // A5
|
|
20
|
+
gain1.gain.setValueAtTime(0.3, ctx.currentTime);
|
|
21
|
+
gain1.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
|
|
22
|
+
osc1.connect(gain1);
|
|
23
|
+
gain1.connect(ctx.destination);
|
|
24
|
+
osc1.start(ctx.currentTime);
|
|
25
|
+
osc1.stop(ctx.currentTime + 0.3);
|
|
26
|
+
|
|
27
|
+
// --- second tone (slightly higher, slightly delayed) ---
|
|
28
|
+
const osc2 = ctx.createOscillator();
|
|
29
|
+
const gain2 = ctx.createGain();
|
|
30
|
+
osc2.type = 'sine';
|
|
31
|
+
osc2.frequency.setValueAtTime(1174.66, ctx.currentTime + 0.15); // D6
|
|
32
|
+
gain2.gain.setValueAtTime(0.0001, ctx.currentTime);
|
|
33
|
+
gain2.gain.setValueAtTime(0.25, ctx.currentTime + 0.15);
|
|
34
|
+
gain2.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5);
|
|
35
|
+
osc2.connect(gain2);
|
|
36
|
+
gain2.connect(ctx.destination);
|
|
37
|
+
osc2.start(ctx.currentTime + 0.15);
|
|
38
|
+
osc2.stop(ctx.currentTime + 0.5);
|
|
39
|
+
|
|
40
|
+
// Clean up context after the sound finishes
|
|
41
|
+
setTimeout(() => ctx.close(), 600);
|
|
42
|
+
} catch {
|
|
43
|
+
// Silently ignore – e.g. if AudioContext isn't available
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hook to manage the notification sound setting and playback.
|
|
49
|
+
*
|
|
50
|
+
* - `enabled` / `setEnabled` – toggle stored in localStorage
|
|
51
|
+
* - `playDing()` – plays the ding only when enabled
|
|
52
|
+
*/
|
|
53
|
+
export function useNotificationSound() {
|
|
54
|
+
const [enabled, setEnabledState] = useState(() => {
|
|
55
|
+
if (typeof window === 'undefined') return true;
|
|
56
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
57
|
+
return stored === null ? true : stored === 'true'; // default ON
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Keep a ref so callbacks that capture stale closures still see the latest value
|
|
61
|
+
const enabledRef = useRef(enabled);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
enabledRef.current = enabled;
|
|
64
|
+
}, [enabled]);
|
|
65
|
+
|
|
66
|
+
const setEnabled = useCallback((value: boolean) => {
|
|
67
|
+
setEnabledState(value);
|
|
68
|
+
localStorage.setItem(STORAGE_KEY, String(value));
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const playDing = useCallback(() => {
|
|
72
|
+
if (enabledRef.current) {
|
|
73
|
+
playDingSound();
|
|
74
|
+
}
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
return { enabled, setEnabled, playDing } as const;
|
|
78
|
+
}
|