@alpaca-editor/core 1.0.4169 → 1.0.4172

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 (88) hide show
  1. package/dist/components/ui/context-menu.js +47 -3
  2. package/dist/components/ui/context-menu.js.map +1 -1
  3. package/dist/config/config.d.ts +1 -2
  4. package/dist/config/config.js +47 -7
  5. package/dist/config/config.js.map +1 -1
  6. package/dist/config/types.d.ts +1 -1
  7. package/dist/editor/ItemInfo.js +8 -2
  8. package/dist/editor/ItemInfo.js.map +1 -1
  9. package/dist/editor/PictureEditor.js +2 -4
  10. package/dist/editor/PictureEditor.js.map +1 -1
  11. package/dist/editor/ai/AgentCostDisplay.js +4 -4
  12. package/dist/editor/ai/AgentCostDisplay.js.map +1 -1
  13. package/dist/editor/ai/AgentTerminal.d.ts +2 -1
  14. package/dist/editor/ai/AgentTerminal.js +960 -529
  15. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  16. package/dist/editor/ai/Agents.js +56 -14
  17. package/dist/editor/ai/Agents.js.map +1 -1
  18. package/dist/editor/ai/ToolCallDisplay.js +4 -2
  19. package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
  20. package/dist/editor/ai/useAgentStatus.js +14 -9
  21. package/dist/editor/ai/useAgentStatus.js.map +1 -1
  22. package/dist/editor/client/EditorShell.js +46 -21
  23. package/dist/editor/client/EditorShell.js.map +1 -1
  24. package/dist/editor/client/hooks/useSocketMessageHandler.js +12 -0
  25. package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
  26. package/dist/editor/commands/itemCommands.d.ts +1 -0
  27. package/dist/editor/commands/itemCommands.js +52 -1
  28. package/dist/editor/commands/itemCommands.js.map +1 -1
  29. package/dist/editor/control-center/WebSocketMessages.js +4 -1
  30. package/dist/editor/control-center/WebSocketMessages.js.map +1 -1
  31. package/dist/editor/control-center/setup-steps/AiSetupStep/tools/GenerateToolsSection.js +4 -0
  32. package/dist/editor/control-center/setup-steps/AiSetupStep/tools/GenerateToolsSection.js.map +1 -1
  33. package/dist/editor/field-types/AttachmentEditor.d.ts +7 -2
  34. package/dist/editor/field-types/AttachmentEditor.js +74 -3
  35. package/dist/editor/field-types/AttachmentEditor.js.map +1 -1
  36. package/dist/editor/fieldTypes.d.ts +4 -0
  37. package/dist/editor/menubar/toolbar-sections/UtilityControls.js +1 -1
  38. package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
  39. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +14 -4
  40. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
  41. package/dist/editor/page-viewer/PageViewerFrame.js +17 -7
  42. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  43. package/dist/editor/services/agentService.d.ts +1 -1
  44. package/dist/editor/services/agentService.js +11 -2
  45. package/dist/editor/services/agentService.js.map +1 -1
  46. package/dist/editor/services/editService.d.ts +1 -0
  47. package/dist/editor/services/editService.js +6 -0
  48. package/dist/editor/services/editService.js.map +1 -1
  49. package/dist/editor/ui/SimpleIconButton.js +1 -1
  50. package/dist/editor/ui/SimpleIconButton.js.map +1 -1
  51. package/dist/editor/ui/TemplateSelectorDialog.d.ts +8 -0
  52. package/dist/editor/ui/TemplateSelectorDialog.js +61 -0
  53. package/dist/editor/ui/TemplateSelectorDialog.js.map +1 -0
  54. package/dist/index.d.ts +2 -2
  55. package/dist/index.js +2 -2
  56. package/dist/index.js.map +1 -1
  57. package/dist/revision.d.ts +2 -2
  58. package/dist/revision.js +2 -2
  59. package/dist/styles.css +13 -0
  60. package/dist/types.d.ts +7 -1
  61. package/package.json +1 -1
  62. package/src/components/ui/context-menu.tsx +58 -3
  63. package/src/config/config.tsx +51 -7
  64. package/src/config/types.ts +8 -2
  65. package/src/editor/ItemInfo.tsx +15 -1
  66. package/src/editor/PictureEditor.tsx +21 -23
  67. package/src/editor/ai/AgentCostDisplay.tsx +28 -6
  68. package/src/editor/ai/AgentTerminal.tsx +628 -155
  69. package/src/editor/ai/Agents.tsx +62 -12
  70. package/src/editor/ai/ToolCallDisplay.tsx +2 -5
  71. package/src/editor/ai/useAgentStatus.ts +18 -9
  72. package/src/editor/client/EditorShell.tsx +68 -32
  73. package/src/editor/client/hooks/useSocketMessageHandler.ts +28 -13
  74. package/src/editor/commands/itemCommands.tsx +76 -0
  75. package/src/editor/control-center/WebSocketMessages.tsx +4 -1
  76. package/src/editor/control-center/setup-steps/AiSetupStep/tools/GenerateToolsSection.tsx +5 -0
  77. package/src/editor/field-types/AttachmentEditor.tsx +148 -3
  78. package/src/editor/fieldTypes.ts +4 -0
  79. package/src/editor/menubar/toolbar-sections/UtilityControls.tsx +1 -1
  80. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +12 -4
  81. package/src/editor/page-viewer/PageViewerFrame.tsx +15 -6
  82. package/src/editor/services/agentService.ts +11 -1
  83. package/src/editor/services/editService.ts +15 -0
  84. package/src/editor/ui/SimpleIconButton.tsx +1 -0
  85. package/src/editor/ui/TemplateSelectorDialog.tsx +129 -0
  86. package/src/index.ts +3 -3
  87. package/src/revision.ts +2 -2
  88. package/src/types.ts +9 -1
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import React, { useEffect, useState, useRef, useCallback, useLayoutEffect, useMemo, } from "react";
3
3
  import { Send, AlertCircle, Loader2, User, Wand2, Square, Mic, MicOff, ChevronDown, ChevronUp, ListTodo, } from "lucide-react";
4
4
  import { DancingDots } from "./DancingDots";
5
- import { getAgent, startAgent, connectToAgentStream, updateAgentContext, updateAgentSettings, updateAgentCostLimit, cancelAgent, } from "../services/agentService";
5
+ import { getAgent, startAgent, updateAgentContext, updateAgentSettings, updateAgentCostLimit, cancelAgent, } from "../services/agentService";
6
6
  import { useEditContext, useFieldsEditContext } from "../client/editContext";
7
7
  import { Textarea } from "../../components/ui/textarea";
8
8
  import { Button } from "../../components/ui/button";
@@ -265,9 +265,6 @@ const TodoListPanel = ({ messages, agentMetadata, }) => {
265
265
  }))
266
266
  .filter((item) => item.text);
267
267
  }
268
- else {
269
- console.log("📋 No todo list found in metadata. AgentMetadata:", agentMetadata);
270
- }
271
268
  }
272
269
  catch (e) {
273
270
  console.error("📋 Error extracting todos from metadata:", e);
@@ -368,6 +365,43 @@ const groupConsecutiveMessages = (agentMessages) => {
368
365
  }
369
366
  return groups;
370
367
  };
368
+ // Merge messages from DB and local state with ID-based deduplication
369
+ const mergeMessagesById = (dbMessages, localMessages) => {
370
+ const messageMap = new Map();
371
+ // Normalize ID key (lowercase) to avoid duplicates caused by casing differences
372
+ const keyOf = (id) => (id ? id.toLowerCase() : "");
373
+ // First, add all DB messages (source of truth for completed messages)
374
+ dbMessages.forEach((msg) => {
375
+ if (msg.id)
376
+ messageMap.set(keyOf(msg.id), msg);
377
+ });
378
+ // Then merge local messages (preserve streaming state, prefer longer content)
379
+ localMessages.forEach((localMsg) => {
380
+ if (!localMsg.id)
381
+ return;
382
+ const key = keyOf(localMsg.id);
383
+ const existingMsg = messageMap.get(key);
384
+ if (!existingMsg) {
385
+ // New message only in local state (e.g., streaming)
386
+ messageMap.set(key, localMsg);
387
+ }
388
+ else if (!localMsg.isCompleted && localMsg.messageType === "streaming") {
389
+ // Keep streaming version if more recent/longer
390
+ if (localMsg.content.length > existingMsg.content.length) {
391
+ messageMap.set(key, localMsg);
392
+ }
393
+ }
394
+ // Otherwise, keep the DB version (completed messages from DB are authoritative)
395
+ });
396
+ // Sort by messageIndex or createdDate to maintain order
397
+ return Array.from(messageMap.values()).sort((a, b) => {
398
+ if (a.messageIndex !== b.messageIndex) {
399
+ return a.messageIndex - b.messageIndex;
400
+ }
401
+ return (new Date(a.createdDate || 0).getTime() -
402
+ new Date(b.createdDate || 0).getTime());
403
+ });
404
+ };
371
405
  // Calculate total token usage and cost data from agent messages
372
406
  const calculateTotalTokens = (messages) => {
373
407
  const totals = messages.reduce((acc, message) => {
@@ -431,7 +465,7 @@ const convertAgentMessagesToAiFormat = (agentMessages) => {
431
465
  // interface AgentTerminalProps {
432
466
  // agentStub: Agent;
433
467
  // }
434
- export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
468
+ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive = true, }) {
435
469
  const editContext = useEditContext();
436
470
  const fieldsContext = useFieldsEditContext();
437
471
  const [agent, setAgent] = useState(undefined);
@@ -442,6 +476,11 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
442
476
  const [isConnecting, setIsConnecting] = useState(false);
443
477
  const [isSubmitting, setIsSubmitting] = useState(false);
444
478
  const [agentMetadata, setAgentMetadata] = useState(null);
479
+ // Generate a stable clientSessionId per component instance for stream deduplication
480
+ const clientSessionIdRef = useRef(null);
481
+ if (!clientSessionIdRef.current) {
482
+ clientSessionIdRef.current = crypto.randomUUID();
483
+ }
445
484
  // Voice input state
446
485
  const [isVoiceSupported, setIsVoiceSupported] = useState(false);
447
486
  const [isListening, setIsListening] = useState(false);
@@ -558,6 +597,10 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
558
597
  const textareaRef = useRef(null);
559
598
  const messagesContainerRef = useRef(null);
560
599
  const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
600
+ // WebSocket subscription state for agent streaming
601
+ const seenMessageIdsRef = useRef(new Set());
602
+ const lastSeqRef = useRef(0);
603
+ const subscribedAgentIdRef = useRef(null);
561
604
  // Cache mode/model changes made while the agent is still "new" (not yet persisted)
562
605
  const pendingSettingsRef = useRef(null);
563
606
  // Auto-scroll to bottom when new messages arrive
@@ -765,13 +808,14 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
765
808
  }
766
809
  // Clear waiting state when first content chunk arrives
767
810
  setIsWaitingForResponse(false);
811
+ isWaitingRef.current = false;
768
812
  // Any content chunk is an incremental update -> reset idle timer
769
813
  resetDotsTimer();
770
814
  // Extract cost/token data from content chunk if present
771
815
  const data = message.data;
772
816
  if (data &&
773
817
  (data.totalCost !== undefined || data.totalTokens !== undefined)) {
774
- setLiveTotals({
818
+ const nextTotals = {
775
819
  input: Number(data.totalInputTokens) || 0,
776
820
  output: Number(data.totalOutputTokens) || 0,
777
821
  cached: Number(data.totalCachedTokens) || 0,
@@ -782,7 +826,15 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
782
826
  cacheWriteCost: Number(data.totalCacheWriteTokenCost) || 0,
783
827
  totalCost: Number(data.totalCost) || 0,
784
828
  currency: data.currency || "USD",
785
- });
829
+ };
830
+ const anyNonZero = (nextTotals.totalCost || 0) > 0 ||
831
+ (nextTotals.input || 0) > 0 ||
832
+ (nextTotals.output || 0) > 0 ||
833
+ (nextTotals.cached || 0) > 0 ||
834
+ (nextTotals.cacheWrite || 0) > 0;
835
+ if (anyNonZero) {
836
+ setLiveTotals(nextTotals);
837
+ }
786
838
  }
787
839
  // Always call setMessages and handle all logic in the callback with latest messages
788
840
  setMessages((prev) => {
@@ -929,7 +981,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
929
981
  const data = message.data;
930
982
  if (data &&
931
983
  (data.totalCost !== undefined || data.totalTokens !== undefined)) {
932
- setLiveTotals({
984
+ const nextTotals = {
933
985
  input: Number(data.totalInputTokens) || 0,
934
986
  output: Number(data.totalOutputTokens) || 0,
935
987
  cached: Number(data.totalCachedTokens) || 0,
@@ -940,7 +992,15 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
940
992
  cacheWriteCost: Number(data.totalCacheWriteTokenCost) || 0,
941
993
  totalCost: Number(data.totalCost) || 0,
942
994
  currency: data.currency || "USD",
943
- });
995
+ };
996
+ const anyNonZero = (nextTotals.totalCost || 0) > 0 ||
997
+ (nextTotals.input || 0) > 0 ||
998
+ (nextTotals.output || 0) > 0 ||
999
+ (nextTotals.cached || 0) > 0 ||
1000
+ (nextTotals.cacheWrite || 0) > 0;
1001
+ if (anyNonZero) {
1002
+ setLiveTotals(nextTotals);
1003
+ }
944
1004
  }
945
1005
  // Update tool result directly in the messages array
946
1006
  if (!resultMessageId) {
@@ -1013,436 +1073,492 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
1013
1073
  // Tool result activity; reset idle timer
1014
1074
  resetDotsTimer();
1015
1075
  }, [resetDotsTimer]);
1016
- // Connect to agent stream for real-time updates
1017
- const connectToStream = useCallback(async (agentData) => {
1076
+ // DEPRECATED: SSE-based streaming has been replaced by WebSocket
1077
+ // All streaming functionality is now handled by handleAgentWebSocketMessage
1078
+ // This function is kept commented for reference but is no longer used
1079
+ /*
1080
+ const connectToStream = useCallback(
1081
+ async (agentData?: AgentDetails) => {
1018
1082
  const currentAgent = agentData || agent;
1019
- if (!currentAgent)
1020
- return;
1083
+ if (!currentAgent) return;
1084
+
1021
1085
  // Cancel any existing connection
1022
1086
  if (abortControllerRef.current) {
1023
- abortControllerRef.current.abort();
1087
+ abortControllerRef.current.abort();
1024
1088
  }
1089
+
1025
1090
  const abortController = new AbortController();
1026
1091
  abortControllerRef.current = abortController;
1092
+
1027
1093
  try {
1028
- setIsConnecting(true);
1029
- // Reduced: minimal logging
1030
- // Expose agent id globally for approval actions
1031
- window.currentAgentId = currentAgent.id;
1032
- // Expose id for approval actions
1033
- // Connecting to agent stream
1034
- await connectToAgentStream(currentAgent.id, (message) => {
1035
- switch (message.type) {
1036
- case "contentChunk":
1037
- handleContentChunk(message, currentAgent);
1038
- break;
1039
- case "toolCall":
1040
- handleToolCall(message, currentAgent);
1041
- break;
1042
- case "toolResult":
1043
- handleToolResult(message, currentAgent);
1044
- break;
1045
- case "statusUpdate":
1094
+ setIsConnecting(true);
1095
+
1096
+ // Reduced: minimal logging
1097
+
1098
+ // Expose agent id globally for approval actions
1099
+ (window as any).currentAgentId = currentAgent.id;
1100
+ // Expose id for approval actions
1101
+
1102
+ // Connecting to agent stream
1103
+ await connectToAgentStream_DEPRECATED(
1104
+ currentAgent.id,
1105
+ (message: AgentStreamMessage) => {
1106
+ switch (message.type) {
1107
+ case "contentChunk":
1108
+ handleContentChunk(message, currentAgent);
1109
+ break;
1110
+
1111
+ case "toolCall":
1112
+ handleToolCall(message, currentAgent);
1113
+ break;
1114
+
1115
+ case "toolResult":
1116
+ handleToolResult(message, currentAgent);
1117
+ break;
1118
+
1119
+ case "statusUpdate":
1120
+ try {
1121
+ // Check both 'kind' and 'state' for backward compatibility
1122
+ const kind =
1123
+ (message as any)?.data?.kind ||
1124
+ (message as any)?.data?.state;
1125
+ if (kind === "streamOpen") {
1126
+ setIsConnecting(false);
1127
+ // Don't clear waiting state here - let it be cleared when first content arrives
1128
+ break;
1129
+ }
1130
+ // Live token usage totals update from backend
1131
+ if (kind === "tokenUsage") {
1132
+ const totals = (message as any)?.data?.totals;
1133
+ if (totals) {
1134
+ const totalCost = Number(totals.totalCost) || 0;
1135
+ const nextTotals = {
1136
+ input: Number(totals.totalInputTokens) || 0,
1137
+ output: Number(totals.totalOutputTokens) || 0,
1138
+ cached: Number(totals.totalCachedInputTokens) || 0,
1139
+ cacheWrite: Number(totals.totalCacheWriteTokens) || 0,
1140
+ inputCost: Number(totals.totalInputTokenCost) || 0,
1141
+ outputCost: Number(totals.totalOutputTokenCost) || 0,
1142
+ cachedCost:
1143
+ Number(totals.totalCachedInputTokenCost) || 0,
1144
+ cacheWriteCost:
1145
+ Number(totals.totalCacheWriteTokenCost) || 0,
1146
+ totalCost: totalCost,
1147
+ currency: totals.currency,
1148
+ };
1149
+ const anyNonZero =
1150
+ (nextTotals.totalCost || 0) > 0 ||
1151
+ (nextTotals.input || 0) > 0 ||
1152
+ (nextTotals.output || 0) > 0 ||
1153
+ (nextTotals.cached || 0) > 0 ||
1154
+ (nextTotals.cacheWrite || 0) > 0;
1155
+ if (anyNonZero) {
1156
+ setLiveTotals(nextTotals);
1157
+ }
1158
+
1159
+ // Check if cost limit exceeded
1160
+ if (agent?.costLimit && totalCost > agent.costLimit) {
1161
+ setCostLimitExceeded({
1162
+ totalCost: totalCost,
1163
+ costLimit: agent.costLimit,
1164
+ initialCostLimit: agent.costLimit,
1165
+ });
1166
+ setIsWaitingForResponse(false);
1167
+ shouldCreateNewMessage.current = false;
1168
+ }
1169
+
1170
+ // Force a re-render to update cost display immediately
1171
+ setMessages((prev) => [...prev]);
1172
+ }
1173
+ break;
1174
+ }
1175
+ if (kind === "toolApprovalsRequired") {
1176
+ const data = (message as any).data || {};
1177
+ const msgId: string | undefined = data.messageId;
1178
+ const ids: string[] = data.toolCallIds || [];
1179
+ // Pause stream until approval
1180
+
1181
+ // Annotate tool calls with a temporary pending marker so UI can reflect paused state on reload
1182
+ if (msgId && Array.isArray(ids) && ids.length > 0) {
1183
+ setMessages((prev) => {
1184
+ const updated = prev.map((m) => {
1185
+ if (m.id !== msgId) return m;
1186
+ const existingToolCalls = m.toolCalls || [];
1187
+ const updatedToolCalls = existingToolCalls.map(
1188
+ (tc) => {
1189
+ if (!ids.includes(tc.toolCallId)) return tc;
1190
+ const fn = tc.functionName || "";
1191
+ return {
1192
+ ...tc,
1193
+ functionName: fn.includes("(pending approval)")
1194
+ ? fn
1195
+ : fn + " (pending approval)",
1196
+ };
1197
+ },
1198
+ );
1199
+ return { ...m, toolCalls: updatedToolCalls };
1200
+ });
1201
+ messagesRef.current = updated;
1202
+ return updated;
1203
+ });
1204
+ }
1205
+
1206
+ // Keep the stream open; just clear waiting flags so UI reflects pause state
1207
+ try {
1208
+ setIsConnecting(false);
1209
+ setIsWaitingForResponse(false);
1210
+ } catch {}
1211
+ break;
1212
+ }
1213
+ if (kind === "contextWindow") {
1214
+ const data = (message as any).data || {};
1215
+ // Store last context window status in a ref so we can render it below
1216
+ (window as any).__agentContextWindowStatus = {
1217
+ model: data.model,
1218
+ normalizedModel: data.normalizedModel,
1219
+ contextWindowTokens: data.contextWindowTokens,
1220
+ maxCompletionTokens: data.maxCompletionTokens,
1221
+ estimatedInputTokens: data.estimatedInputTokens,
1222
+ messageCount: data.messageCount,
1223
+ contextUsedPercent: data.contextUsedPercent,
1224
+ };
1225
+ // Force a re-render by toggling state (cheap no-op)
1226
+ setMessages((prev) => [...prev]);
1227
+ } else if (kind === "contextChanged") {
1228
+ const data = (message as any).data || {};
1229
+ const nextContext = data.context || {};
1230
+ // Merge incoming context into local metadata
1231
+ setAgentMetadata((prev) => {
1232
+ const current = (prev || {}) as AgentMetadata;
1233
+ // Exclude top-level context to avoid duplicate keys when spreading
1234
+ const currentWithoutContext = { ...current };
1235
+ delete (currentWithoutContext as any).context;
1236
+ const next: AgentMetadata = {
1237
+ ...currentWithoutContext,
1238
+ additionalData: {
1239
+ ...(current.additionalData || {}),
1240
+ context: nextContext,
1241
+ },
1242
+ } as AgentMetadata;
1243
+ return next;
1244
+ });
1245
+ // Also reflect in agent metadata string for consistency
1246
+ setAgent((prevAgent) => {
1247
+ if (!prevAgent) return prevAgent;
1046
1248
  try {
1047
- // Check both 'kind' and 'state' for backward compatibility
1048
- const kind = message?.data?.kind ||
1049
- message?.data?.state;
1050
- if (kind === "streamOpen") {
1051
- setIsConnecting(false);
1052
- // Don't clear waiting state here - let it be cleared when first content arrives
1053
- break;
1054
- }
1055
- // Live token usage totals update from backend
1056
- if (kind === "tokenUsage") {
1057
- const totals = message?.data?.totals;
1058
- if (totals) {
1059
- const totalCost = Number(totals.totalCost) || 0;
1060
- setLiveTotals({
1061
- input: Number(totals.totalInputTokens) || 0,
1062
- output: Number(totals.totalOutputTokens) || 0,
1063
- cached: Number(totals.totalCachedInputTokens) || 0,
1064
- cacheWrite: Number(totals.totalCacheWriteTokens) || 0,
1065
- inputCost: Number(totals.totalInputTokenCost) || 0,
1066
- outputCost: Number(totals.totalOutputTokenCost) || 0,
1067
- cachedCost: Number(totals.totalCachedInputTokenCost) || 0,
1068
- cacheWriteCost: Number(totals.totalCacheWriteTokenCost) || 0,
1069
- totalCost: totalCost,
1070
- currency: totals.currency,
1071
- });
1072
- // Check if cost limit exceeded
1073
- if (agent?.costLimit && totalCost > agent.costLimit) {
1074
- setCostLimitExceeded({
1075
- totalCost: totalCost,
1076
- costLimit: agent.costLimit,
1077
- initialCostLimit: agent.costLimit,
1078
- });
1079
- setIsWaitingForResponse(false);
1080
- shouldCreateNewMessage.current = false;
1081
- }
1082
- // Force a re-render to update cost display immediately
1083
- setMessages((prev) => [...prev]);
1084
- }
1085
- break;
1086
- }
1087
- if (kind === "toolApprovalsRequired") {
1088
- const data = message.data || {};
1089
- const msgId = data.messageId;
1090
- const ids = data.toolCallIds || [];
1091
- // Pause stream until approval
1092
- // Annotate tool calls with a temporary pending marker so UI can reflect paused state on reload
1093
- if (msgId && Array.isArray(ids) && ids.length > 0) {
1094
- setMessages((prev) => {
1095
- const updated = prev.map((m) => {
1096
- if (m.id !== msgId)
1097
- return m;
1098
- const existingToolCalls = m.toolCalls || [];
1099
- const updatedToolCalls = existingToolCalls.map((tc) => {
1100
- if (!ids.includes(tc.toolCallId))
1101
- return tc;
1102
- const fn = tc.functionName || "";
1103
- return {
1104
- ...tc,
1105
- functionName: fn.includes("(pending approval)")
1106
- ? fn
1107
- : fn + " (pending approval)",
1108
- };
1109
- });
1110
- return { ...m, toolCalls: updatedToolCalls };
1111
- });
1112
- messagesRef.current = updated;
1113
- return updated;
1114
- });
1115
- }
1116
- // Keep the stream open; just clear waiting flags so UI reflects pause state
1117
- try {
1118
- setIsConnecting(false);
1119
- setIsWaitingForResponse(false);
1120
- }
1121
- catch { }
1122
- break;
1249
+ const currentMeta: AgentMetadata | null = (() => {
1250
+ try {
1251
+ return prevAgent.agentContext
1252
+ ? (JSON.parse(
1253
+ prevAgent.agentContext,
1254
+ ) as AgentMetadata)
1255
+ : null;
1256
+ } catch {
1257
+ return null;
1123
1258
  }
1124
- if (kind === "contextWindow") {
1125
- const data = message.data || {};
1126
- // Store last context window status in a ref so we can render it below
1127
- window.__agentContextWindowStatus = {
1128
- model: data.model,
1129
- normalizedModel: data.normalizedModel,
1130
- contextWindowTokens: data.contextWindowTokens,
1131
- maxCompletionTokens: data.maxCompletionTokens,
1132
- estimatedInputTokens: data.estimatedInputTokens,
1133
- messageCount: data.messageCount,
1134
- contextUsedPercent: data.contextUsedPercent,
1259
+ })();
1260
+ const nextMeta: AgentMetadata = {
1261
+ ...(currentMeta || ({} as AgentMetadata)),
1262
+ ...nextContext,
1263
+ } as AgentMetadata;
1264
+ return {
1265
+ ...prevAgent,
1266
+ agentContext: JSON.stringify(nextMeta),
1267
+ };
1268
+ } catch {
1269
+ return prevAgent;
1270
+ }
1271
+ });
1272
+ } else if (kind === "cost_limit_reached") {
1273
+ // Cost limit has been reached - show banner and stop waiting
1274
+ // NOTE: Stream stays connected so user can extend limit and continue immediately
1275
+ console.log(
1276
+ "[AgentTerminal] Cost limit reached notification received",
1277
+ message,
1278
+ );
1279
+ const data = (message as any).data || {};
1280
+ const totalCost = Number(data.totalCost) || 0;
1281
+ // Use costLimit from the notification data, or fall back to agent.costLimit
1282
+ const costLimit =
1283
+ Number(data.costLimit) || agent?.costLimit || 0;
1284
+
1285
+ console.log(
1286
+ "[AgentTerminal] Setting cost limit exceeded state:",
1287
+ {
1288
+ totalCost,
1289
+ costLimit,
1290
+ agentCostLimit: agent?.costLimit,
1291
+ dataCostLimit: data.costLimit,
1292
+ dataValues: data,
1293
+ },
1294
+ );
1295
+
1296
+ // Set the state with values from the notification
1297
+ setCostLimitExceeded({
1298
+ totalCost: totalCost,
1299
+ costLimit: costLimit,
1300
+ initialCostLimit: costLimit,
1301
+ });
1302
+
1303
+ // Clear waiting states but keep stream connected
1304
+ setIsWaitingForResponse(false);
1305
+ setIsConnecting(false);
1306
+ shouldCreateNewMessage.current = false;
1307
+ break;
1308
+ } else if (
1309
+ kind === "toolApprovalGranted" ||
1310
+ kind === "toolApprovalRejected"
1311
+ ) {
1312
+ const data = (message as any).data || {};
1313
+ const toolCallId: string | undefined = data.toolCallId;
1314
+ const msgId: string | undefined = data.messageId;
1315
+ // Processing tool approval
1316
+ if (toolCallId && msgId) {
1317
+ setMessages((prev) => {
1318
+ const updated = prev.map((m) => {
1319
+ if (m.id !== msgId) return m;
1320
+ const existingToolCalls = m.toolCalls || [];
1321
+ const updatedToolCalls = existingToolCalls.map(
1322
+ (tc) => {
1323
+ if (tc.toolCallId !== toolCallId) return tc;
1324
+ const suffix =
1325
+ kind === "toolApprovalGranted"
1326
+ ? " (approved)"
1327
+ : " (rejected)";
1328
+ // Remove "(pending approval)" suffix before adding new suffix
1329
+ const baseFunctionName = (tc.functionName || "")
1330
+ .replace(" (pending approval)", "")
1331
+ .trim();
1332
+ const newFunctionName = baseFunctionName + suffix;
1333
+ // Update function name with approval suffix
1334
+ return {
1335
+ ...tc,
1336
+ functionName: newFunctionName,
1135
1337
  };
1136
- // Force a re-render by toggling state (cheap no-op)
1137
- setMessages((prev) => [...prev]);
1338
+ },
1339
+ );
1340
+ return { ...m, toolCalls: updatedToolCalls };
1341
+ });
1342
+ messagesRef.current = updated;
1343
+ return updated;
1344
+ });
1345
+ }
1346
+ break;
1347
+ }
1348
+ } catch {}
1349
+ break;
1350
+
1351
+ case "completed":
1352
+ const completedMessageId = message.data?.messageId;
1353
+
1354
+ // If the completed event carries full messages, merge them into state
1355
+ try {
1356
+ const completionMessages = (message as any)?.data
1357
+ ?.messages as any[] | undefined;
1358
+ if (
1359
+ completionMessages &&
1360
+ Array.isArray(completionMessages) &&
1361
+ completionMessages.length > 0
1362
+ ) {
1363
+ setMessages((prev) => {
1364
+ // Mark all completion messages as completed
1365
+ const dbMessages = completionMessages.map((msg) => ({
1366
+ ...msg,
1367
+ isCompleted: true,
1368
+ messageType: "completed" as const,
1369
+ })) as AgentChatMessage[];
1370
+
1371
+ // Use ID-based merge to prevent duplicates
1372
+ const merged = mergeMessagesById(dbMessages, prev);
1373
+ messagesRef.current = merged;
1374
+ return merged;
1375
+ });
1376
+ }
1377
+ } catch (e) {
1378
+ console.warn("⚠️ Failed to merge completion messages:", e);
1379
+ }
1380
+
1381
+ // Mark the specific message as completed by messageId
1382
+ if (completedMessageId) {
1383
+ setMessages((prev) => {
1384
+ const updated = prev.map((msg) => {
1385
+ if (msg.id === completedMessageId) {
1386
+ const updatedMessage = {
1387
+ ...msg,
1388
+ isCompleted: true,
1389
+ messageType: "completed" as const,
1390
+ };
1391
+
1392
+ // Update token usage data if provided in the completed event
1393
+ if (message.data) {
1394
+ const data = message.data;
1395
+ if (data.numInputTokens !== undefined) {
1396
+ updatedMessage.inputTokens = data.numInputTokens;
1138
1397
  }
1139
- else if (kind === "contextChanged") {
1140
- const data = message.data || {};
1141
- const nextContext = data.context || {};
1142
- // Merge incoming context into local metadata
1143
- setAgentMetadata((prev) => {
1144
- const current = (prev || {});
1145
- // Exclude top-level context to avoid duplicate keys when spreading
1146
- const currentWithoutContext = { ...current };
1147
- delete currentWithoutContext.context;
1148
- const next = {
1149
- ...currentWithoutContext,
1150
- additionalData: {
1151
- ...(current.additionalData || {}),
1152
- context: nextContext,
1153
- },
1154
- };
1155
- return next;
1156
- });
1157
- // Also reflect in agent metadata string for consistency
1158
- setAgent((prevAgent) => {
1159
- if (!prevAgent)
1160
- return prevAgent;
1161
- try {
1162
- const currentMeta = (() => {
1163
- try {
1164
- return prevAgent.agentContext
1165
- ? JSON.parse(prevAgent.agentContext)
1166
- : null;
1167
- }
1168
- catch {
1169
- return null;
1170
- }
1171
- })();
1172
- const nextMeta = {
1173
- ...(currentMeta || {}),
1174
- ...nextContext,
1175
- };
1176
- return {
1177
- ...prevAgent,
1178
- agentContext: JSON.stringify(nextMeta),
1179
- };
1180
- }
1181
- catch {
1182
- return prevAgent;
1183
- }
1184
- });
1398
+ if (data.numOutputTokens !== undefined) {
1399
+ updatedMessage.outputTokens = data.numOutputTokens;
1185
1400
  }
1186
- else if (kind === "cost_limit_reached") {
1187
- // Cost limit has been reached - show banner and stop waiting
1188
- // NOTE: Stream stays connected so user can extend limit and continue immediately
1189
- console.log("[AgentTerminal] Cost limit reached notification received", message);
1190
- const data = message.data || {};
1191
- const totalCost = Number(data.totalCost) || 0;
1192
- // Use costLimit from the notification data, or fall back to agent.costLimit
1193
- const costLimit = Number(data.costLimit) || agent?.costLimit || 0;
1194
- console.log("[AgentTerminal] Setting cost limit exceeded state:", {
1195
- totalCost,
1196
- costLimit,
1197
- agentCostLimit: agent?.costLimit,
1198
- dataCostLimit: data.costLimit,
1199
- dataValues: data,
1200
- });
1201
- // Set the state with values from the notification
1202
- setCostLimitExceeded({
1203
- totalCost: totalCost,
1204
- costLimit: costLimit,
1205
- initialCostLimit: costLimit,
1206
- });
1207
- // Clear waiting states but keep stream connected
1208
- setIsWaitingForResponse(false);
1209
- setIsConnecting(false);
1210
- shouldCreateNewMessage.current = false;
1211
- break;
1401
+ if (data.numCachedTokens !== undefined) {
1402
+ updatedMessage.cachedInputTokens =
1403
+ data.numCachedTokens;
1212
1404
  }
1213
- else if (kind === "toolApprovalGranted" ||
1214
- kind === "toolApprovalRejected") {
1215
- const data = message.data || {};
1216
- const toolCallId = data.toolCallId;
1217
- const msgId = data.messageId;
1218
- // Processing tool approval
1219
- if (toolCallId && msgId) {
1220
- setMessages((prev) => {
1221
- const updated = prev.map((m) => {
1222
- if (m.id !== msgId)
1223
- return m;
1224
- const existingToolCalls = m.toolCalls || [];
1225
- const updatedToolCalls = existingToolCalls.map((tc) => {
1226
- if (tc.toolCallId !== toolCallId)
1227
- return tc;
1228
- const suffix = kind === "toolApprovalGranted"
1229
- ? " (approved)"
1230
- : " (rejected)";
1231
- // Remove "(pending approval)" suffix before adding new suffix
1232
- const baseFunctionName = (tc.functionName || "")
1233
- .replace(" (pending approval)", "")
1234
- .trim();
1235
- const newFunctionName = baseFunctionName + suffix;
1236
- // Update function name with approval suffix
1237
- return {
1238
- ...tc,
1239
- functionName: newFunctionName,
1240
- };
1241
- });
1242
- return { ...m, toolCalls: updatedToolCalls };
1243
- });
1244
- messagesRef.current = updated;
1245
- return updated;
1246
- });
1247
- }
1248
- break;
1405
+ // Update total tokens used
1406
+ updatedMessage.tokensUsed =
1407
+ (updatedMessage.inputTokens || 0) +
1408
+ (updatedMessage.outputTokens || 0);
1409
+
1410
+ // Update cost data if provided in the completed event
1411
+ if (data.inputTokenCost !== undefined) {
1412
+ updatedMessage.inputTokenCost = data.inputTokenCost;
1249
1413
  }
1250
- }
1251
- catch { }
1252
- break;
1253
- case "completed":
1254
- const completedMessageId = message.data?.messageId;
1255
- // If the completed event carries full messages, merge them into state
1256
- try {
1257
- const completionMessages = message?.data
1258
- ?.messages;
1259
- if (completionMessages &&
1260
- Array.isArray(completionMessages) &&
1261
- completionMessages.length > 0) {
1262
- setMessages((prev) => {
1263
- const existingById = new Map();
1264
- prev.forEach((m, idx) => existingById.set(m.id, idx));
1265
- const merged = [...prev];
1266
- for (const incoming of completionMessages) {
1267
- const incomingId = incoming?.id;
1268
- if (!incomingId)
1269
- continue;
1270
- const idx = existingById.get(incomingId);
1271
- const finalized = {
1272
- ...incoming,
1273
- isCompleted: true,
1274
- messageType: "completed",
1275
- };
1276
- if (typeof idx === "number") {
1277
- merged[idx] = finalized;
1278
- }
1279
- else {
1280
- merged.push(finalized);
1281
- }
1282
- }
1283
- messagesRef.current = merged;
1284
- return merged;
1285
- });
1414
+ if (data.outputTokenCost !== undefined) {
1415
+ updatedMessage.outputTokenCost =
1416
+ data.outputTokenCost;
1286
1417
  }
1287
- }
1288
- catch (e) {
1289
- console.warn("⚠️ Failed to merge completion messages:", e);
1290
- }
1291
- // Mark the specific message as completed by messageId
1292
- if (completedMessageId) {
1293
- setMessages((prev) => {
1294
- const updated = prev.map((msg) => {
1295
- if (msg.id === completedMessageId) {
1296
- const updatedMessage = {
1297
- ...msg,
1298
- isCompleted: true,
1299
- messageType: "completed",
1300
- };
1301
- // Update token usage data if provided in the completed event
1302
- if (message.data) {
1303
- const data = message.data;
1304
- if (data.numInputTokens !== undefined) {
1305
- updatedMessage.inputTokens = data.numInputTokens;
1306
- }
1307
- if (data.numOutputTokens !== undefined) {
1308
- updatedMessage.outputTokens = data.numOutputTokens;
1309
- }
1310
- if (data.numCachedTokens !== undefined) {
1311
- updatedMessage.cachedInputTokens =
1312
- data.numCachedTokens;
1313
- }
1314
- // Update total tokens used
1315
- updatedMessage.tokensUsed =
1316
- (updatedMessage.inputTokens || 0) +
1317
- (updatedMessage.outputTokens || 0);
1318
- // Update cost data if provided in the completed event
1319
- if (data.inputTokenCost !== undefined) {
1320
- updatedMessage.inputTokenCost = data.inputTokenCost;
1321
- }
1322
- if (data.outputTokenCost !== undefined) {
1323
- updatedMessage.outputTokenCost =
1324
- data.outputTokenCost;
1325
- }
1326
- if (data.cachedInputTokenCost !== undefined ||
1327
- data.cachedTokenCost !== undefined) {
1328
- updatedMessage.cachedInputTokenCost =
1329
- data.cachedInputTokenCost ?? data.cachedTokenCost;
1330
- }
1331
- if (data.totalCost !== undefined) {
1332
- updatedMessage.totalCost = data.totalCost;
1333
- }
1334
- // Handle content that might only be sent in the completed event
1335
- if (data.deltaContent && data.deltaContent.trim()) {
1336
- if (!data.isIncremental) {
1337
- // Non-incremental: replace the entire content
1338
- updatedMessage.content = data.deltaContent;
1339
- }
1340
- else {
1341
- // Incremental: append to existing content
1342
- updatedMessage.content =
1343
- (updatedMessage.content || "") +
1344
- data.deltaContent;
1345
- }
1346
- }
1347
- }
1348
- return updatedMessage;
1349
- }
1350
- return msg;
1351
- });
1352
- messagesRef.current = updated;
1353
- return updated;
1354
- });
1355
- }
1356
- else {
1357
- // Fallback: Mark any streaming messages as completed (old behavior)
1358
- console.warn("⚠️ No messageId in completed event, falling back to marking all streaming messages as completed");
1359
- setMessages((prev) => {
1360
- const updated = prev.map((msg) => !msg.isCompleted && msg.messageType === "streaming"
1361
- ? {
1362
- ...msg,
1363
- isCompleted: true,
1364
- messageType: "completed",
1365
- }
1366
- : msg);
1367
- messagesRef.current = updated;
1368
- return updated;
1369
- });
1370
- }
1371
- // Ensure waiting state is cleared when stream completes
1372
- setIsWaitingForResponse(false);
1373
- shouldCreateNewMessage.current = false;
1374
- // Streaming finished; update indicator
1375
- resetDotsTimer();
1376
- break;
1377
- case "contextUpdate":
1378
- // Update agent context when backend sends context update
1379
- try {
1380
- console.log("📥 Received contextUpdate message:", message);
1381
- const updatedContext = message.data;
1382
- if (updatedContext) {
1383
- console.log("📝 Updating agent metadata with:", updatedContext);
1384
- // Check if there's a todo list in the context
1385
- if (updatedContext.additionalData?.todoList) {
1386
- console.log("✅ Todo list found in context update:", updatedContext.additionalData.todoList);
1387
- }
1388
- // Update local metadata state
1389
- setAgentMetadata((prev) => ({
1390
- ...prev,
1391
- ...updatedContext,
1392
- }));
1393
- // Update agent state with new context
1394
- setAgent((prevAgent) => {
1395
- if (!prevAgent)
1396
- return prevAgent;
1397
- return {
1398
- ...prevAgent,
1399
- agentContext: JSON.stringify(updatedContext),
1400
- };
1401
- });
1402
- console.log("✅ Context updated from backend successfully");
1418
+ if (
1419
+ data.cachedInputTokenCost !== undefined ||
1420
+ data.cachedTokenCost !== undefined
1421
+ ) {
1422
+ updatedMessage.cachedInputTokenCost =
1423
+ data.cachedInputTokenCost ?? data.cachedTokenCost;
1403
1424
  }
1404
- else {
1405
- console.warn("⚠️ Context update received but updatedContext is null/undefined");
1425
+ if (data.totalCost !== undefined) {
1426
+ updatedMessage.totalCost = data.totalCost;
1406
1427
  }
1428
+
1429
+ // Handle content that might only be sent in the completed event
1430
+ if (data.deltaContent && data.deltaContent.trim()) {
1431
+ if (!data.isIncremental) {
1432
+ // Non-incremental: replace the entire content
1433
+ updatedMessage.content = data.deltaContent;
1434
+ } else {
1435
+ // Incremental: append to existing content
1436
+ updatedMessage.content =
1437
+ (updatedMessage.content || "") +
1438
+ data.deltaContent;
1439
+ }
1440
+ }
1441
+ }
1442
+
1443
+ return updatedMessage;
1407
1444
  }
1408
- catch (err) {
1409
- console.error("❌ Error handling context update:", err);
1410
- }
1411
- break;
1412
- case "error":
1413
- console.error("❌ Stream error:", message.error);
1414
- setError(message.error || "Stream error occurred");
1415
- setIsWaitingForResponse(false);
1416
- shouldCreateNewMessage.current = false;
1417
- // Error ends streaming; update indicator
1418
- resetDotsTimer();
1419
- break;
1420
- default:
1421
- console.warn("❓ Unhandled message type:", {
1422
- type: message.type,
1423
- typeOf: typeof message.type,
1424
- length: message.type?.length,
1425
- charCodes: message.type
1426
- ?.split("")
1427
- .map((c) => c.charCodeAt(0)),
1428
- message: message,
1429
- });
1430
- break;
1431
- }
1432
- }, abortController.signal);
1433
- }
1434
- catch (err) {
1435
- if (!abortController.signal.aborted) {
1436
- console.error("Stream connection failed:", err);
1437
- setError("Failed to connect to agent stream");
1438
- }
1439
- }
1440
- finally {
1441
- setIsConnecting(false);
1442
- // Guard: clear waiting state if connection finished without content
1443
- setIsWaitingForResponse(false);
1444
- }
1445
- }, [agent?.id, handleContentChunk, handleToolCall, handleToolResult]);
1445
+ return msg;
1446
+ });
1447
+ messagesRef.current = updated;
1448
+ return updated;
1449
+ });
1450
+ } else {
1451
+ // Fallback: Mark any streaming messages as completed (old behavior)
1452
+ console.warn(
1453
+ "⚠️ No messageId in completed event, falling back to marking all streaming messages as completed",
1454
+ );
1455
+ setMessages((prev) => {
1456
+ const updated = prev.map((msg) =>
1457
+ !msg.isCompleted && msg.messageType === "streaming"
1458
+ ? {
1459
+ ...msg,
1460
+ isCompleted: true,
1461
+ messageType: "completed",
1462
+ }
1463
+ : msg,
1464
+ );
1465
+ messagesRef.current = updated;
1466
+ return updated;
1467
+ });
1468
+ }
1469
+ // Ensure waiting state is cleared when stream completes
1470
+ setIsWaitingForResponse(false);
1471
+ isWaitingRef.current = false;
1472
+ shouldCreateNewMessage.current = false;
1473
+ // Streaming finished; update indicator
1474
+ resetDotsTimer();
1475
+ break;
1476
+
1477
+ case "contextUpdate":
1478
+ // Update agent context when backend sends context update
1479
+ try {
1480
+ console.log("📥 Received contextUpdate message:", message);
1481
+ const updatedContext = message.data as AgentMetadata;
1482
+ if (updatedContext) {
1483
+ console.log(
1484
+ "📝 Updating agent metadata with:",
1485
+ updatedContext,
1486
+ );
1487
+
1488
+ // Check if there's a todo list in the context
1489
+ if (updatedContext.additionalData?.todoList) {
1490
+ console.log(
1491
+ "✅ Todo list found in context update:",
1492
+ updatedContext.additionalData.todoList,
1493
+ );
1494
+ }
1495
+
1496
+ // Update local metadata state
1497
+ setAgentMetadata((prev) => ({
1498
+ ...prev,
1499
+ ...updatedContext,
1500
+ }));
1501
+
1502
+ // Update agent state with new context
1503
+ setAgent((prevAgent) => {
1504
+ if (!prevAgent) return prevAgent;
1505
+ return {
1506
+ ...prevAgent,
1507
+ agentContext: JSON.stringify(updatedContext),
1508
+ };
1509
+ });
1510
+
1511
+ console.log("✅ Context updated from backend successfully");
1512
+ } else {
1513
+ console.warn(
1514
+ "⚠️ Context update received but updatedContext is null/undefined",
1515
+ );
1516
+ }
1517
+ } catch (err) {
1518
+ console.error("❌ Error handling context update:", err);
1519
+ }
1520
+ break;
1521
+
1522
+ case "error":
1523
+ console.error("❌ Stream error:", message.error);
1524
+ setError(message.error || "Stream error occurred");
1525
+ setIsWaitingForResponse(false);
1526
+ isWaitingRef.current = false;
1527
+ shouldCreateNewMessage.current = false;
1528
+ // Error ends streaming; update indicator
1529
+ resetDotsTimer();
1530
+ break;
1531
+
1532
+ default:
1533
+ console.warn("❓ Unhandled message type:", {
1534
+ type: message.type,
1535
+ typeOf: typeof message.type,
1536
+ length: message.type?.length,
1537
+ charCodes: message.type
1538
+ ?.split("")
1539
+ .map((c) => c.charCodeAt(0)),
1540
+ message: message,
1541
+ });
1542
+ break;
1543
+ }
1544
+ },
1545
+ abortController.signal,
1546
+ clientSessionIdRef.current || undefined,
1547
+ );
1548
+ } catch (err) {
1549
+ if (!abortController.signal.aborted) {
1550
+ console.error("Stream connection failed:", err);
1551
+ setError("Failed to connect to agent stream");
1552
+ }
1553
+ } finally {
1554
+ setIsConnecting(false);
1555
+ // Guard: clear waiting state if connection finished without content
1556
+ setIsWaitingForResponse(false);
1557
+ }
1558
+ },
1559
+ [agent?.id, handleContentChunk, handleToolCall, handleToolResult],
1560
+ );
1561
+ */
1446
1562
  // Listen for local approval resolution to update UI
1447
1563
  useEffect(() => {
1448
1564
  const onApprovalResolved = (ev) => {
@@ -1647,42 +1763,22 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
1647
1763
  setIsLoading(true);
1648
1764
  setError(null);
1649
1765
  const agentData = await getAgent(agentStub.id);
1766
+ console.log(`[AgentTerminal] Loaded agent ${agentStub.id}, messages count:`, agentData.messages?.length || 0, agentData.messages);
1650
1767
  setAgent(agentData);
1651
- // Merge database messages with any existing streaming messages
1652
- // to prevent losing responses that are currently being streamed
1768
+ // Merge database messages with any existing local messages using ID-based deduplication
1769
+ // This prevents both missing messages and duplicates
1653
1770
  setMessages((prevMessages) => {
1654
1771
  const dbMessages = agentData.messages || [];
1655
- // Find any streaming (incomplete) messages that exist locally but not in DB
1656
- const streamingMessages = prevMessages.filter((msg) => !msg.isCompleted && msg.role === "assistant");
1657
- // If we have streaming messages, preserve them
1658
- if (streamingMessages.length > 0) {
1659
- // Get all completed messages from DB
1660
- const completedDbMessages = dbMessages.filter((msg) => {
1661
- // Check if this message is one of our streaming messages
1662
- const isStreamingMessage = streamingMessages.some((sm) => sm.id === msg.id);
1663
- return !isStreamingMessage;
1664
- });
1665
- // Merge: use DB messages but keep streaming ones
1666
- const merged = [...completedDbMessages];
1667
- // Add back the streaming messages at the end
1668
- for (const streamingMsg of streamingMessages) {
1669
- // Check if this message ID exists in DB messages
1670
- const dbVersion = dbMessages.find((m) => m.id === streamingMsg.id);
1671
- if (dbVersion &&
1672
- dbVersion.content &&
1673
- dbVersion.content.length > streamingMsg.content.length) {
1674
- // DB has more content, skip the streaming version
1675
- merged.push(dbVersion);
1676
- }
1677
- else {
1678
- // Keep the streaming version
1679
- merged.push(streamingMsg);
1680
- }
1772
+ console.log(`[AgentTerminal] Merging messages - DB: ${dbMessages.length}, Local: ${prevMessages.length}`);
1773
+ // Track all DB message IDs as "seen" to prevent WebSocket duplicates
1774
+ dbMessages.forEach((msg) => {
1775
+ if (msg.id) {
1776
+ seenMessageIdsRef.current.add(msg.id.toLowerCase());
1681
1777
  }
1682
- return merged;
1683
- }
1684
- // No streaming messages, just use DB messages
1685
- return dbMessages;
1778
+ });
1779
+ const merged = mergeMessagesById(dbMessages, prevMessages);
1780
+ console.log(`[AgentTerminal] Merged result: ${merged.length} messages`);
1781
+ return merged;
1686
1782
  });
1687
1783
  // Set agent ID for existing agents too
1688
1784
  window.currentAgentId = agentData.id;
@@ -1744,34 +1840,12 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
1744
1840
  agentData.status === 2;
1745
1841
  const isCostLimitReached = agentData.status === "costLimitReached" ||
1746
1842
  agentData.status === 7;
1747
- if (isRunning || isWaitingForApproval || isCostLimitReached) {
1748
- // Auto-connect for running agents, agents waiting for approval, and agents at cost limit
1749
- // (stream stays open so user can extend limit and continue)
1750
- setTimeout(async () => {
1751
- if (abortControllerRef.current) {
1752
- return;
1753
- }
1754
- shouldCreateNewMessage.current = false;
1755
- // For cost limit reached, just connect to stream - don't restart agent
1756
- // For running/approval states, signal the backend we're reconnecting
1757
- if (!isCostLimitReached) {
1758
- try {
1759
- if (editContext?.sessionId) {
1760
- await startAgent({
1761
- agentId: agentData.id,
1762
- message: "",
1763
- sessionId: editContext.sessionId,
1764
- profileId: agentData.profileId || "",
1765
- model: agentData.model,
1766
- });
1767
- }
1768
- }
1769
- catch (startError) {
1770
- console.warn("Failed to call startAgent during reconnection:", startError);
1771
- }
1772
- }
1773
- await connectToStream(agentData);
1774
- }, 100);
1843
+ // NOTE: SSE reconnection logic removed - no longer needed with WebSocket
1844
+ // The WebSocket subscription (in the useEffect below) handles reconnection automatically
1845
+ // Just set the streaming state if agent is running
1846
+ if (isRunning && isActive) {
1847
+ shouldCreateNewMessage.current = false;
1848
+ // State will be set by the WebSocket subscription useEffect
1775
1849
  }
1776
1850
  }
1777
1851
  catch (err) {
@@ -1797,6 +1871,403 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
1797
1871
  useEffect(() => {
1798
1872
  loadAgent();
1799
1873
  }, [loadAgent]);
1874
+ // Reload agent when tab becomes active to get latest messages
1875
+ const previousIsActiveRef = useRef(isActive);
1876
+ useEffect(() => {
1877
+ const wasInactive = !previousIsActiveRef.current;
1878
+ const isNowActive = isActive;
1879
+ previousIsActiveRef.current = isActive;
1880
+ if (wasInactive && isNowActive && agent) {
1881
+ // Tab just became active - reload to get any new messages
1882
+ console.log(`[AgentTerminal] Tab became active, reloading agent ${agent.id}`);
1883
+ loadAgent();
1884
+ }
1885
+ }, [isActive, agent?.id, loadAgent]);
1886
+ // WebSocket message handler for agent streaming
1887
+ const handleAgentWebSocketMessage = useCallback((message) => {
1888
+ if (!agent)
1889
+ return;
1890
+ const messageType = message.type;
1891
+ // Handle agent:name:updated (payload structure is different)
1892
+ if (messageType === "agent:name:updated") {
1893
+ const { agentId: updatedAgentId, agentName } = message.payload;
1894
+ if (updatedAgentId === agent.id && agentName) {
1895
+ console.log("[AgentTerminal] Agent name updated via WebSocket:", agentName);
1896
+ setAgent((prev) => (prev ? { ...prev, name: agentName } : prev));
1897
+ }
1898
+ return;
1899
+ }
1900
+ // For other agent messages, check if this is for our agent
1901
+ const agentId = message.payload?.agentId;
1902
+ if (agentId !== agent.id)
1903
+ return;
1904
+ // Handle agent:run:start
1905
+ if (messageType === "agent:run:start") {
1906
+ console.log("[AgentTerminal] Agent run started via WebSocket:", agentId);
1907
+ return;
1908
+ }
1909
+ // Handle agent:user:message
1910
+ if (messageType === "agent:user:message") {
1911
+ const { messageId, content, timestamp } = message.payload;
1912
+ // Track in seenMessageIds for deduplication
1913
+ const normalizedId = messageId.toLowerCase();
1914
+ if (seenMessageIdsRef.current.has(normalizedId)) {
1915
+ console.log("[AgentTerminal] User message already seen, skipping:", messageId);
1916
+ return;
1917
+ }
1918
+ seenMessageIdsRef.current.add(normalizedId);
1919
+ // Add user message to the messages list
1920
+ setMessages((prev) => {
1921
+ // Double-check if message already exists (deduplication)
1922
+ if (prev.some((m) => m.id && m.id.toLowerCase() === normalizedId)) {
1923
+ console.log("[AgentTerminal] User message already in list, skipping:", messageId);
1924
+ return prev;
1925
+ }
1926
+ const userMessage = {
1927
+ id: messageId,
1928
+ agentId: agent.id,
1929
+ messageIndex: prev.length,
1930
+ role: "user",
1931
+ content: content,
1932
+ name: "user",
1933
+ messageType: "user",
1934
+ isCompleted: true,
1935
+ model: "",
1936
+ tokensUsed: 0,
1937
+ inputTokens: 0,
1938
+ outputTokens: 0,
1939
+ cachedInputTokens: 0,
1940
+ inputTokenCost: 0,
1941
+ outputTokenCost: 0,
1942
+ cachedInputTokenCost: 0,
1943
+ totalCost: 0,
1944
+ currency: "USD",
1945
+ createdDate: timestamp || new Date().toISOString(),
1946
+ toolCalls: [],
1947
+ };
1948
+ const updated = [...prev, userMessage];
1949
+ messagesRef.current = updated;
1950
+ return updated;
1951
+ });
1952
+ console.log("[AgentTerminal] User message added via WebSocket:", messageId);
1953
+ return;
1954
+ }
1955
+ // Handle agent:run:delta (content, tools, etc.)
1956
+ if (messageType === "agent:run:delta") {
1957
+ const { seq, type, data } = message.payload;
1958
+ // Deduplicate by sequence
1959
+ if (seq && seq <= lastSeqRef.current) {
1960
+ return; // Already processed
1961
+ }
1962
+ if (seq) {
1963
+ lastSeqRef.current = seq;
1964
+ }
1965
+ // Route based on delta type
1966
+ const agentStreamMessage = {
1967
+ type,
1968
+ data,
1969
+ timestamp: new Date().toISOString(),
1970
+ };
1971
+ if (type === "ContentChunk" || type === "contentChunk") {
1972
+ handleContentChunk(agentStreamMessage, agent);
1973
+ }
1974
+ else if (type === "ToolCall" || type === "toolCall") {
1975
+ handleToolCall(agentStreamMessage, agent);
1976
+ }
1977
+ else if (type === "ToolResult" || type === "toolResult") {
1978
+ handleToolResult(agentStreamMessage, agent);
1979
+ }
1980
+ return;
1981
+ }
1982
+ // Handle agent:run:status
1983
+ if (messageType === "agent:run:status") {
1984
+ const { seq, kind, data: statusData } = message.payload;
1985
+ // Deduplicate by sequence
1986
+ if (seq && seq <= lastSeqRef.current) {
1987
+ return;
1988
+ }
1989
+ if (seq) {
1990
+ lastSeqRef.current = seq;
1991
+ }
1992
+ // Route based on status kind
1993
+ try {
1994
+ if (kind === "streamOpen") {
1995
+ setIsConnecting(false);
1996
+ return;
1997
+ }
1998
+ if (kind === "tokenUsage") {
1999
+ const totals = statusData?.totals;
2000
+ if (totals) {
2001
+ const totalCost = Number(totals.totalCost) || 0;
2002
+ const nextTotals = {
2003
+ input: Number(totals.totalInputTokens) || 0,
2004
+ output: Number(totals.totalOutputTokens) || 0,
2005
+ cached: Number(totals.totalCachedInputTokens) || 0,
2006
+ cacheWrite: Number(totals.totalCacheWriteTokens) || 0,
2007
+ inputCost: Number(totals.totalInputTokenCost) || 0,
2008
+ outputCost: Number(totals.totalOutputTokenCost) || 0,
2009
+ cachedCost: Number(totals.totalCachedInputTokenCost) || 0,
2010
+ cacheWriteCost: Number(totals.totalCacheWriteTokenCost) || 0,
2011
+ totalCost: totalCost,
2012
+ currency: totals.currency,
2013
+ };
2014
+ const anyNonZero = (nextTotals.totalCost || 0) > 0 ||
2015
+ (nextTotals.input || 0) > 0 ||
2016
+ (nextTotals.output || 0) > 0 ||
2017
+ (nextTotals.cached || 0) > 0 ||
2018
+ (nextTotals.cacheWrite || 0) > 0;
2019
+ if (anyNonZero) {
2020
+ setLiveTotals(nextTotals);
2021
+ }
2022
+ if (agent?.costLimit && totalCost > agent.costLimit) {
2023
+ setCostLimitExceeded({
2024
+ totalCost: totalCost,
2025
+ costLimit: agent.costLimit,
2026
+ initialCostLimit: agent.costLimit,
2027
+ });
2028
+ setIsWaitingForResponse(false);
2029
+ shouldCreateNewMessage.current = false;
2030
+ }
2031
+ setMessages((prev) => [...prev]);
2032
+ }
2033
+ return;
2034
+ }
2035
+ if (kind === "toolApprovalsRequired") {
2036
+ const msgId = statusData.messageId;
2037
+ const ids = statusData.toolCallIds || [];
2038
+ if (msgId && Array.isArray(ids) && ids.length > 0) {
2039
+ setMessages((prev) => {
2040
+ const updated = prev.map((m) => {
2041
+ if (m.id !== msgId)
2042
+ return m;
2043
+ const existingToolCalls = m.toolCalls || [];
2044
+ const updatedToolCalls = existingToolCalls.map((tc) => {
2045
+ if (!ids.includes(tc.toolCallId))
2046
+ return tc;
2047
+ const fn = tc.functionName || "";
2048
+ return {
2049
+ ...tc,
2050
+ functionName: fn.includes("(pending approval)")
2051
+ ? fn
2052
+ : fn + " (pending approval)",
2053
+ };
2054
+ });
2055
+ return { ...m, toolCalls: updatedToolCalls };
2056
+ });
2057
+ messagesRef.current = updated;
2058
+ return updated;
2059
+ });
2060
+ }
2061
+ setIsConnecting(false);
2062
+ setIsWaitingForResponse(false);
2063
+ return;
2064
+ }
2065
+ if (kind === "cost_limit_reached") {
2066
+ const totalCost = Number(statusData.totalCost) || 0;
2067
+ const costLimit = Number(statusData.costLimit) || agent?.costLimit || 0;
2068
+ setCostLimitExceeded({
2069
+ totalCost: totalCost,
2070
+ costLimit: costLimit,
2071
+ initialCostLimit: costLimit,
2072
+ });
2073
+ setIsWaitingForResponse(false);
2074
+ setIsConnecting(false);
2075
+ return;
2076
+ }
2077
+ if (kind === "contextWindow") {
2078
+ window.__agentContextWindowStatus = {
2079
+ model: statusData.model,
2080
+ normalizedModel: statusData.normalizedModel,
2081
+ contextWindowTokens: statusData.contextWindowTokens,
2082
+ maxCompletionTokens: statusData.maxCompletionTokens,
2083
+ estimatedInputTokens: statusData.estimatedInputTokens,
2084
+ messageCount: statusData.messageCount,
2085
+ contextUsedPercent: statusData.contextUsedPercent,
2086
+ };
2087
+ setMessages((prev) => [...prev]);
2088
+ return;
2089
+ }
2090
+ if (kind === "contextChanged") {
2091
+ const nextContext = statusData.context || {};
2092
+ setAgentMetadata((prev) => {
2093
+ const current = (prev || {});
2094
+ const currentWithoutContext = { ...current };
2095
+ delete currentWithoutContext.context;
2096
+ const next = {
2097
+ ...currentWithoutContext,
2098
+ additionalData: {
2099
+ ...(current.additionalData || {}),
2100
+ context: nextContext,
2101
+ },
2102
+ };
2103
+ return next;
2104
+ });
2105
+ return;
2106
+ }
2107
+ }
2108
+ catch (err) {
2109
+ console.error("[AgentTerminal] Error handling status update:", err);
2110
+ }
2111
+ return;
2112
+ }
2113
+ // Handle agent:run:complete
2114
+ if (messageType === "agent:run:complete") {
2115
+ console.log("[AgentTerminal] Agent run completed via WebSocket:", agentId);
2116
+ // Mark the last assistant message as completed
2117
+ setMessages((prev) => {
2118
+ const updated = prev.map((msg) => msg.role === "assistant" && !msg.isCompleted
2119
+ ? {
2120
+ ...msg,
2121
+ isCompleted: true,
2122
+ messageType: "completed",
2123
+ }
2124
+ : msg);
2125
+ messagesRef.current = updated;
2126
+ return updated;
2127
+ });
2128
+ setIsWaitingForResponse(false);
2129
+ isWaitingRef.current = false;
2130
+ setIsConnecting(false);
2131
+ shouldCreateNewMessage.current = false;
2132
+ resetDotsTimer();
2133
+ return;
2134
+ }
2135
+ // Handle agent:run:error
2136
+ if (messageType === "agent:run:error") {
2137
+ const errorMsg = message.payload?.error || "Unknown error";
2138
+ console.error("[AgentTerminal] Agent run error via WebSocket:", errorMsg);
2139
+ setError(errorMsg);
2140
+ setIsWaitingForResponse(false);
2141
+ isWaitingRef.current = false;
2142
+ setIsConnecting(false);
2143
+ resetDotsTimer();
2144
+ return;
2145
+ }
2146
+ }, [
2147
+ agent,
2148
+ handleContentChunk,
2149
+ handleToolCall,
2150
+ handleToolResult,
2151
+ resetDotsTimer,
2152
+ ]);
2153
+ // Keep refs for latest agent and resetDotsTimer to avoid adding them to effect deps
2154
+ const agentRef = useRef(agent);
2155
+ const resetDotsTimerRef = useRef(resetDotsTimer);
2156
+ const handleAgentWebSocketMessageRef = useRef(handleAgentWebSocketMessage);
2157
+ useEffect(() => {
2158
+ agentRef.current = agent;
2159
+ }, [agent]);
2160
+ useEffect(() => {
2161
+ resetDotsTimerRef.current = resetDotsTimer;
2162
+ }, [resetDotsTimer]);
2163
+ useEffect(() => {
2164
+ handleAgentWebSocketMessageRef.current = handleAgentWebSocketMessage;
2165
+ }, [handleAgentWebSocketMessage]);
2166
+ // Subscribe to agent WebSocket messages when active
2167
+ useEffect(() => {
2168
+ const addListener = editContext?.addSocketMessageListener;
2169
+ if (!isActive || !addListener) {
2170
+ // Unsubscribe if we were previously subscribed
2171
+ if (subscribedAgentIdRef.current) {
2172
+ const socket = globalThis.editorSocket;
2173
+ if (socket && socket.readyState === WebSocket.OPEN) {
2174
+ socket.send(JSON.stringify({
2175
+ type: "agent:unsubscribe",
2176
+ agentId: subscribedAgentIdRef.current,
2177
+ }));
2178
+ }
2179
+ subscribedAgentIdRef.current = null;
2180
+ }
2181
+ return;
2182
+ }
2183
+ // Send subscription message to server
2184
+ const socket = globalThis.editorSocket;
2185
+ if (socket && socket.readyState === WebSocket.OPEN) {
2186
+ socket.send(JSON.stringify({
2187
+ type: "agent:subscribe",
2188
+ agentId: agentStub.id,
2189
+ }));
2190
+ console.log(`[AgentTerminal] Sent agent:subscribe for ${agentStub.id}`);
2191
+ }
2192
+ // Use the addSocketMessageListener helper from editContext
2193
+ // Wrap the handler in a stable function that uses the ref
2194
+ const stableHandler = (message) => {
2195
+ handleAgentWebSocketMessageRef.current(message);
2196
+ };
2197
+ const unsubscribe = addListener(stableHandler);
2198
+ subscribedAgentIdRef.current = agentStub.id;
2199
+ // Reset deduplication state when switching agents
2200
+ seenMessageIdsRef.current.clear();
2201
+ lastSeqRef.current = 0;
2202
+ // Set up streaming state based on agent status (uses latest values via refs)
2203
+ const currentAgent = agentRef.current;
2204
+ if (currentAgent) {
2205
+ const isRunning = currentAgent.status === "running" || currentAgent.status === 1;
2206
+ const isWaitingForApproval = currentAgent.status === "waitingForApproval" ||
2207
+ currentAgent.status === 2;
2208
+ if (isRunning) {
2209
+ console.log(`[AgentTerminal] Agent is running, setting up streaming state for ${currentAgent.id}`);
2210
+ setIsWaitingForResponse(true);
2211
+ isWaitingRef.current = true;
2212
+ shouldCreateNewMessage.current = false;
2213
+ resetDotsTimerRef.current();
2214
+ }
2215
+ else if (isWaitingForApproval) {
2216
+ console.log(`[AgentTerminal] Agent is waiting for approval for ${currentAgent.id}`);
2217
+ setIsWaitingForResponse(false);
2218
+ isWaitingRef.current = false;
2219
+ resetDotsTimerRef.current();
2220
+ }
2221
+ else {
2222
+ console.log(`[AgentTerminal] Agent status is ${currentAgent.status}, clearing streaming state`);
2223
+ setIsWaitingForResponse(false);
2224
+ isWaitingRef.current = false;
2225
+ resetDotsTimerRef.current();
2226
+ }
2227
+ }
2228
+ return () => {
2229
+ // Send unsubscribe message to server
2230
+ const socket = globalThis.editorSocket;
2231
+ if (socket &&
2232
+ socket.readyState === WebSocket.OPEN &&
2233
+ subscribedAgentIdRef.current) {
2234
+ socket.send(JSON.stringify({
2235
+ type: "agent:unsubscribe",
2236
+ agentId: subscribedAgentIdRef.current,
2237
+ }));
2238
+ console.log(`[AgentTerminal] Sent agent:unsubscribe for ${subscribedAgentIdRef.current}`);
2239
+ }
2240
+ unsubscribe();
2241
+ subscribedAgentIdRef.current = null;
2242
+ };
2243
+ }, [isActive, agentStub.id, editContext?.addSocketMessageListener]);
2244
+ // Handle stream connection when agent becomes active/inactive
2245
+ // NOTE: SSE connection disabled - now using WebSocket exclusively
2246
+ // Keeping this code commented for easy rollback if needed
2247
+ /*
2248
+ useEffect(() => {
2249
+ if (!agent) return;
2250
+
2251
+ const isRunning = agent.status === "running" || (agent.status as any) === 1;
2252
+ const isWaitingForApproval =
2253
+ agent.status === "waitingForApproval" || (agent.status as any) === 2;
2254
+ const isCostLimitReached =
2255
+ agent.status === "costLimitReached" || (agent.status as any) === 7;
2256
+
2257
+ const shouldBeConnected =
2258
+ isRunning || isWaitingForApproval || isCostLimitReached;
2259
+
2260
+ if (isActive && shouldBeConnected && !abortControllerRef.current) {
2261
+ // Agent became active and should be connected - connect to stream
2262
+ connectToStream();
2263
+ } else if (!isActive && abortControllerRef.current) {
2264
+ // Agent became inactive - disconnect from stream
2265
+ abortControllerRef.current.abort();
2266
+ abortControllerRef.current = null;
2267
+ setIsConnecting(false);
2268
+ }
2269
+ }, [isActive, agent?.status, connectToStream]);
2270
+ */
1800
2271
  // Focus prompt when requested globally (from AI command)
1801
2272
  useEffect(() => {
1802
2273
  const focusHandler = () => {
@@ -1952,29 +2423,9 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
1952
2423
  console.warn(`Context factory not found: ${factoryName}. Proceeding without it.`);
1953
2424
  }
1954
2425
  }
1955
- // Add user message to local state immediately for better UX
1956
- const userMessage = {
1957
- id: `user-${Date.now()}`,
1958
- agentId,
1959
- messageIndex: messages.length,
1960
- role: "user",
1961
- content: prompt.trim(),
1962
- name: "user",
1963
- messageType: "user",
1964
- isCompleted: true,
1965
- model: "",
1966
- tokensUsed: 0,
1967
- inputTokens: 0,
1968
- outputTokens: 0,
1969
- cachedInputTokens: 0,
1970
- inputTokenCost: 0,
1971
- outputTokenCost: 0,
1972
- cachedInputTokenCost: 0,
1973
- totalCost: 0,
1974
- currency: "USD",
1975
- createdDate: new Date().toISOString(),
1976
- };
1977
- setMessages((prev) => [...prev, userMessage]);
2426
+ // NOTE: User message is no longer added optimistically here
2427
+ // It will be added when we receive the agent:user:message broadcast from the server
2428
+ // This ensures all tabs (including the sending tab) have the same messageId from the database
1978
2429
  const metaCtx = agentMetadata?.additionalData?.context;
1979
2430
  const selectionFromCtx = metaCtx?.componentIds?.length
1980
2431
  ? metaCtx.componentIds
@@ -2015,8 +2466,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
2015
2466
  setCurrentHistoryIndex(-1);
2016
2467
  }
2017
2468
  setPrompt("");
2018
- // Connect to stream immediately to start receiving updates
2019
- await connectToStream();
2469
+ // WebSocket connection is already active via subscription - no need for SSE
2020
2470
  }
2021
2471
  catch (err) {
2022
2472
  console.error("Failed to submit prompt:", err);
@@ -2105,28 +2555,9 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
2105
2555
  console.warn(`Context factory not found: ${factoryName}. Proceeding without it.`);
2106
2556
  }
2107
2557
  }
2108
- const userMessage = {
2109
- id: `user-${Date.now()}`,
2110
- agentId,
2111
- messageIndex: messages.length,
2112
- role: "user",
2113
- content: text.trim(),
2114
- name: "user",
2115
- messageType: "user",
2116
- isCompleted: true,
2117
- model: "",
2118
- tokensUsed: 0,
2119
- inputTokens: 0,
2120
- outputTokens: 0,
2121
- cachedInputTokens: 0,
2122
- inputTokenCost: 0,
2123
- outputTokenCost: 0,
2124
- cachedInputTokenCost: 0,
2125
- totalCost: 0,
2126
- currency: "USD",
2127
- createdDate: new Date().toISOString(),
2128
- };
2129
- setMessages((prev) => [...prev, userMessage]);
2558
+ // NOTE: User message is no longer added optimistically here
2559
+ // It will be added when we receive the agent:user:message broadcast from the server
2560
+ // This ensures all tabs (including the sending tab) have the same messageId from the database
2130
2561
  const metaCtx = agentMetadata?.additionalData?.context;
2131
2562
  const selectionFromCtx = metaCtx?.componentIds?.length
2132
2563
  ? metaCtx.componentIds
@@ -2153,7 +2584,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
2153
2584
  await startAgent(request);
2154
2585
  // If user changed mode/model while the agent was new, persist them now
2155
2586
  await persistPendingSettingsIfNeeded();
2156
- await connectToStream();
2587
+ // WebSocket connection is already active via subscription - no need for SSE
2157
2588
  }
2158
2589
  catch (err) {
2159
2590
  console.error("Failed to submit quick message:", err);