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.
Files changed (149) hide show
  1. package/dist/agent/index.d.ts +2 -2
  2. package/dist/{index-BYOHniN4.d.ts → index-dJv_dqUq.d.ts} +5 -5
  3. package/dist/index.d.ts +3 -3
  4. package/dist/{search-DALwmPRX.d.ts → search-DrztQ_iP.d.ts} +5 -5
  5. package/dist/tools/index.d.ts +2 -2
  6. package/package.json +1 -1
  7. package/web/.next/BUILD_ID +1 -1
  8. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  9. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  10. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  11. package/web/.next/standalone/web/.next/server/app/(main)/page.js.nft.json +1 -1
  12. package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
  13. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js.nft.json +1 -1
  14. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
  15. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  16. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  17. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  18. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  19. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  20. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  21. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  22. package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  24. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +2 -2
  25. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  26. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  28. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  31. package/web/.next/standalone/web/.next/server/app/api/config/route.js.nft.json +1 -1
  32. package/web/.next/standalone/web/.next/server/app/api/health/route.js.nft.json +1 -1
  33. package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +1 -1
  34. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  35. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +2 -2
  36. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +2 -2
  37. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  38. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +2 -2
  39. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +2 -2
  40. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  44. package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +1 -1
  45. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  46. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +2 -2
  47. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +2 -2
  48. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +2 -2
  50. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +2 -2
  51. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  56. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +2 -2
  57. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +2 -2
  58. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +2 -2
  60. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +2 -2
  61. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  62. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  65. package/web/.next/standalone/web/.next/server/app/docs.rsc +2 -2
  66. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +2 -2
  67. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +2 -2
  69. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +2 -2
  70. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/embed/[id]/page.js.nft.json +1 -1
  73. package/web/.next/standalone/web/.next/server/app/embed/[id]/page_client-reference-manifest.js +1 -1
  74. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  75. package/web/.next/standalone/web/.next/server/app/index.rsc +4 -4
  76. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
  77. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +2 -2
  78. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
  79. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  80. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
  81. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  82. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_40c1da73._.js → 2374f_06a5ea48._.js} +1 -1
  83. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_ec658806._.js → 2374f_2572135b._.js} +1 -1
  84. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_c8f8a326._.js → 2374f_273991ba._.js} +1 -1
  85. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_13da7b2a._.js → 2374f_460c0d78._.js} +1 -1
  86. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_1de2f628._.js → 2374f_5ee28d4c._.js} +1 -1
  87. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_9f2fae06._.js → 2374f_65b86b54._.js} +1 -1
  88. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_f3c65774._.js → 2374f_7b421f78._.js} +1 -1
  89. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_41d07cce._.js → 2374f_88cbeb7b._.js} +1 -1
  90. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_2bfcd3a2._.js → 2374f_8d018190._.js} +1 -1
  91. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_e5142825._.js → 2374f_a7457131._.js} +1 -1
  92. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_dd21a4e9._.js → 2374f_c9e3cd7b._.js} +1 -1
  93. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_88aa1671._.js → 2374f_de035f79._.js} +1 -1
  94. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_ac79ddf4._.js → 2374f_df3c414e._.js} +1 -1
  95. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_6d8e5e94._.js → 2374f_ec997752._.js} +1 -1
  96. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__a87e1c27._.js +3 -3
  97. package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__43405ad6._.js → [root-of-the-server]__c94aac98._.js} +2 -2
  98. package/web/.next/standalone/web/.next/server/chunks/ssr/{web_273500a6._.js → web_9c9f0e3b._.js} +2 -2
  99. package/web/.next/standalone/web/.next/server/chunks/ssr/{web_329773d1._.js → web_b85931da._.js} +2 -2
  100. package/web/.next/standalone/web/.next/server/chunks/ssr/{web_8305f089._.js → web_d08270f7._.js} +2 -2
  101. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_components_sessions-sidebar_tsx_92510070._.js +1 -1
  102. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  103. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  104. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  105. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  106. package/web/.next/standalone/web/.next/static/chunks/{c25d80326ea739f3.js → 1685d98ff8751ba3.js} +3 -3
  107. package/web/.next/standalone/web/.next/static/chunks/3f295b6960943c38.js +1 -0
  108. package/web/.next/standalone/web/.next/static/chunks/7228b2394d1fb347.css +1 -0
  109. package/web/.next/standalone/web/.next/static/chunks/a2b4737b190d1b54.js +5 -0
  110. package/web/.next/standalone/web/.next/static/{static/chunks/b7d722dab2a7ecc0.js → chunks/e97212fcc8221479.js} +2 -2
  111. package/web/.next/standalone/web/.next/static/chunks/f6e47c8a9766ce91.js +7 -0
  112. package/web/.next/standalone/web/.next/static/static/chunks/{c25d80326ea739f3.js → 1685d98ff8751ba3.js} +3 -3
  113. package/web/.next/standalone/web/.next/static/static/chunks/3f295b6960943c38.js +1 -0
  114. package/web/.next/standalone/web/.next/static/static/chunks/7228b2394d1fb347.css +1 -0
  115. package/web/.next/standalone/web/.next/static/static/chunks/a2b4737b190d1b54.js +5 -0
  116. package/web/.next/{static/chunks/b7d722dab2a7ecc0.js → standalone/web/.next/static/static/chunks/e97212fcc8221479.js} +2 -2
  117. package/web/.next/standalone/web/.next/static/static/chunks/f6e47c8a9766ce91.js +7 -0
  118. package/web/.next/standalone/web/src/components/ai-elements/search-tool.tsx +26 -3
  119. package/web/.next/standalone/web/src/components/ai-elements/subagent-modal.tsx +13 -1
  120. package/web/.next/standalone/web/src/components/chat-interface.tsx +98 -62
  121. package/web/.next/standalone/web/src/components/sessions-sidebar.tsx +23 -1
  122. package/web/.next/standalone/web/src/hooks/use-notification-sound.ts +78 -0
  123. package/web/.next/static/chunks/{c25d80326ea739f3.js → 1685d98ff8751ba3.js} +3 -3
  124. package/web/.next/static/chunks/3f295b6960943c38.js +1 -0
  125. package/web/.next/static/chunks/7228b2394d1fb347.css +1 -0
  126. package/web/.next/static/chunks/a2b4737b190d1b54.js +5 -0
  127. package/web/.next/{standalone/web/.next/static/chunks/b7d722dab2a7ecc0.js → static/chunks/e97212fcc8221479.js} +2 -2
  128. package/web/.next/static/chunks/f6e47c8a9766ce91.js +7 -0
  129. package/web/.next/standalone/web/.next/static/chunks/96ec96279ada0efe.js +0 -1
  130. package/web/.next/standalone/web/.next/static/chunks/cc6d43f798bbe415.js +0 -5
  131. package/web/.next/standalone/web/.next/static/chunks/de300ff10b63be85.css +0 -1
  132. package/web/.next/standalone/web/.next/static/chunks/de71e63276f488bb.js +0 -7
  133. package/web/.next/standalone/web/.next/static/static/chunks/96ec96279ada0efe.js +0 -1
  134. package/web/.next/standalone/web/.next/static/static/chunks/cc6d43f798bbe415.js +0 -5
  135. package/web/.next/standalone/web/.next/static/static/chunks/de300ff10b63be85.css +0 -1
  136. package/web/.next/standalone/web/.next/static/static/chunks/de71e63276f488bb.js +0 -7
  137. package/web/.next/static/chunks/96ec96279ada0efe.js +0 -1
  138. package/web/.next/static/chunks/cc6d43f798bbe415.js +0 -5
  139. package/web/.next/static/chunks/de300ff10b63be85.css +0 -1
  140. package/web/.next/static/chunks/de71e63276f488bb.js +0 -7
  141. /package/web/.next/standalone/web/.next/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_buildManifest.js +0 -0
  142. /package/web/.next/standalone/web/.next/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_clientMiddlewareManifest.json +0 -0
  143. /package/web/.next/standalone/web/.next/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_ssgManifest.js +0 -0
  144. /package/web/.next/standalone/web/.next/static/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_buildManifest.js +0 -0
  145. /package/web/.next/standalone/web/.next/static/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_clientMiddlewareManifest.json +0 -0
  146. /package/web/.next/standalone/web/.next/static/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_ssgManifest.js +0 -0
  147. /package/web/.next/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_buildManifest.js +0 -0
  148. /package/web/.next/static/{PL5DsSghvjeqtWcOBQ9Qh → OpS0l4dUAiyM_f_IQW4mJ}/_clientMiddlewareManifest.json +0 -0
  149. /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
- // Reload messages and checkpoints after stream finishes
1244
- // This ensures messages have proper messageSequence for the revert button
1245
- // and avoids duplicate key issues from streaming vs API-loaded items
1246
- Promise.all([
1247
- getSessionMessages(session.id),
1248
- getSessionCheckpoints(session.id).catch(() => ({ checkpoints: [] })),
1249
- ]).then(([apiMessages, checkpointsData]) => {
1250
- const sorted = [...apiMessages].sort((a, b) =>
1251
- new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
1252
- );
1253
- const converted = convertApiMessages(sorted);
1254
- setChatItems(converted);
1255
- setCheckpoints(checkpointsData.checkpoints || []);
1256
- }).catch(() => {}); // Silently ignore errors
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
- // Keep a ref to executeSubmit so the auto-send effect always calls the
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
- // Auto-send next queued message when assistant finishes
1803
- const prevIsRunningRef = useRef(isRunning);
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 it's currently in the streaming tool calls
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
- // Also check for placeholder matches (same tool name, streaming status)
2623
- (tc.toolName === item.toolCall?.toolName &&
2624
- (tc.status === 'streaming' || tc.status === 'running'))
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
+ }