@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.
- package/dist/components/ui/context-menu.js +47 -3
- package/dist/components/ui/context-menu.js.map +1 -1
- package/dist/config/config.d.ts +1 -2
- package/dist/config/config.js +47 -7
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +1 -1
- package/dist/editor/ItemInfo.js +8 -2
- package/dist/editor/ItemInfo.js.map +1 -1
- package/dist/editor/PictureEditor.js +2 -4
- package/dist/editor/PictureEditor.js.map +1 -1
- package/dist/editor/ai/AgentCostDisplay.js +4 -4
- package/dist/editor/ai/AgentCostDisplay.js.map +1 -1
- package/dist/editor/ai/AgentTerminal.d.ts +2 -1
- package/dist/editor/ai/AgentTerminal.js +960 -529
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/Agents.js +56 -14
- package/dist/editor/ai/Agents.js.map +1 -1
- package/dist/editor/ai/ToolCallDisplay.js +4 -2
- package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
- package/dist/editor/ai/useAgentStatus.js +14 -9
- package/dist/editor/ai/useAgentStatus.js.map +1 -1
- package/dist/editor/client/EditorShell.js +46 -21
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/client/hooks/useSocketMessageHandler.js +12 -0
- package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
- package/dist/editor/commands/itemCommands.d.ts +1 -0
- package/dist/editor/commands/itemCommands.js +52 -1
- package/dist/editor/commands/itemCommands.js.map +1 -1
- package/dist/editor/control-center/WebSocketMessages.js +4 -1
- package/dist/editor/control-center/WebSocketMessages.js.map +1 -1
- package/dist/editor/control-center/setup-steps/AiSetupStep/tools/GenerateToolsSection.js +4 -0
- package/dist/editor/control-center/setup-steps/AiSetupStep/tools/GenerateToolsSection.js.map +1 -1
- package/dist/editor/field-types/AttachmentEditor.d.ts +7 -2
- package/dist/editor/field-types/AttachmentEditor.js +74 -3
- package/dist/editor/field-types/AttachmentEditor.js.map +1 -1
- package/dist/editor/fieldTypes.d.ts +4 -0
- package/dist/editor/menubar/toolbar-sections/UtilityControls.js +1 -1
- package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +14 -4
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
- package/dist/editor/page-viewer/PageViewerFrame.js +17 -7
- package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
- package/dist/editor/services/agentService.d.ts +1 -1
- package/dist/editor/services/agentService.js +11 -2
- package/dist/editor/services/agentService.js.map +1 -1
- package/dist/editor/services/editService.d.ts +1 -0
- package/dist/editor/services/editService.js +6 -0
- package/dist/editor/services/editService.js.map +1 -1
- package/dist/editor/ui/SimpleIconButton.js +1 -1
- package/dist/editor/ui/SimpleIconButton.js.map +1 -1
- package/dist/editor/ui/TemplateSelectorDialog.d.ts +8 -0
- package/dist/editor/ui/TemplateSelectorDialog.js +61 -0
- package/dist/editor/ui/TemplateSelectorDialog.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +13 -0
- package/dist/types.d.ts +7 -1
- package/package.json +1 -1
- package/src/components/ui/context-menu.tsx +58 -3
- package/src/config/config.tsx +51 -7
- package/src/config/types.ts +8 -2
- package/src/editor/ItemInfo.tsx +15 -1
- package/src/editor/PictureEditor.tsx +21 -23
- package/src/editor/ai/AgentCostDisplay.tsx +28 -6
- package/src/editor/ai/AgentTerminal.tsx +628 -155
- package/src/editor/ai/Agents.tsx +62 -12
- package/src/editor/ai/ToolCallDisplay.tsx +2 -5
- package/src/editor/ai/useAgentStatus.ts +18 -9
- package/src/editor/client/EditorShell.tsx +68 -32
- package/src/editor/client/hooks/useSocketMessageHandler.ts +28 -13
- package/src/editor/commands/itemCommands.tsx +76 -0
- package/src/editor/control-center/WebSocketMessages.tsx +4 -1
- package/src/editor/control-center/setup-steps/AiSetupStep/tools/GenerateToolsSection.tsx +5 -0
- package/src/editor/field-types/AttachmentEditor.tsx +148 -3
- package/src/editor/fieldTypes.ts +4 -0
- package/src/editor/menubar/toolbar-sections/UtilityControls.tsx +1 -1
- package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +12 -4
- package/src/editor/page-viewer/PageViewerFrame.tsx +15 -6
- package/src/editor/services/agentService.ts +11 -1
- package/src/editor/services/editService.ts +15 -0
- package/src/editor/ui/SimpleIconButton.tsx +1 -0
- package/src/editor/ui/TemplateSelectorDialog.tsx +129 -0
- package/src/index.ts +3 -3
- package/src/revision.ts +2 -2
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1017
|
-
|
|
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
|
-
|
|
1083
|
+
if (!currentAgent) return;
|
|
1084
|
+
|
|
1021
1085
|
// Cancel any existing connection
|
|
1022
1086
|
if (abortControllerRef.current) {
|
|
1023
|
-
|
|
1087
|
+
abortControllerRef.current.abort();
|
|
1024
1088
|
}
|
|
1089
|
+
|
|
1025
1090
|
const abortController = new AbortController();
|
|
1026
1091
|
abortControllerRef.current = abortController;
|
|
1092
|
+
|
|
1027
1093
|
try {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
}
|
|
1055
|
-
|
|
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
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
-
|
|
1137
|
-
|
|
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
|
-
|
|
1140
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
-
|
|
1252
|
-
|
|
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
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
|
1652
|
-
//
|
|
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
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
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
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
return
|
|
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
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
-
//
|
|
1956
|
-
|
|
1957
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
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
|
-
|
|
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);
|