@alpaca-editor/core 1.0.4173 → 1.0.4174
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/editor/QuickItemSwitcher.d.ts +3 -3
- package/dist/editor/QuickItemSwitcher.js +25 -7
- package/dist/editor/QuickItemSwitcher.js.map +1 -1
- package/dist/editor/ai/AgentCostDisplay.js +7 -11
- package/dist/editor/ai/AgentCostDisplay.js.map +1 -1
- package/dist/editor/ai/AgentTerminal.js +192 -718
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/Agents.js +61 -38
- package/dist/editor/ai/Agents.js.map +1 -1
- package/dist/editor/client/EditorShell.js +117 -17
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/client/hooks/useSocketMessageHandler.js +0 -12
- package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
- package/dist/editor/services/agentService.d.ts +16 -2
- package/dist/editor/services/agentService.js +22 -8
- package/dist/editor/services/agentService.js.map +1 -1
- package/dist/editor/services/aiService.d.ts +1 -0
- package/dist/editor/services/aiService.js +36 -5
- package/dist/editor/services/aiService.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +6 -3
- package/dist/types.d.ts +13 -0
- package/package.json +1 -1
- package/src/editor/QuickItemSwitcher.tsx +60 -33
- package/src/editor/ai/AgentCostDisplay.tsx +59 -60
- package/src/editor/ai/AgentTerminal.tsx +213 -741
- package/src/editor/ai/Agents.tsx +54 -38
- package/src/editor/client/EditorShell.tsx +142 -21
- package/src/editor/client/hooks/useSocketMessageHandler.ts +0 -15
- package/src/editor/services/agentService.ts +39 -10
- package/src/editor/services/aiService.ts +43 -6
- package/src/revision.ts +2 -2
- package/src/types.ts +15 -0
|
@@ -489,7 +489,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
489
489
|
const [isListening, setIsListening] = useState(false);
|
|
490
490
|
const recognitionRef = useRef(null);
|
|
491
491
|
const prevPlaceholderRef = useRef(null);
|
|
492
|
-
const [voiceError, setVoiceError] = useState(null);
|
|
493
492
|
const [isWaitingForResponse, setIsWaitingForResponse] = useState(false);
|
|
494
493
|
const isWaitingRef = useRef(false);
|
|
495
494
|
useEffect(() => {
|
|
@@ -549,6 +548,30 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
549
548
|
const [activeProfile, setActiveProfile] = useState(undefined);
|
|
550
549
|
const [selectedModelId, setSelectedModelId] = useState(undefined);
|
|
551
550
|
const [mode, setMode] = useState("supervised");
|
|
551
|
+
// Remove deprecated cost limit fields from metadata to avoid confusion with agent/profile settings
|
|
552
|
+
const sanitizeAgentMetadata = useCallback((meta) => {
|
|
553
|
+
try {
|
|
554
|
+
if (!meta)
|
|
555
|
+
return meta;
|
|
556
|
+
const clean = { ...meta };
|
|
557
|
+
delete clean.costLimit;
|
|
558
|
+
delete clean.CostLimit;
|
|
559
|
+
delete clean.initialCostLimit;
|
|
560
|
+
delete clean.InitialCostLimit;
|
|
561
|
+
if (clean.additionalData && typeof clean.additionalData === "object") {
|
|
562
|
+
const ad = { ...clean.additionalData };
|
|
563
|
+
delete ad.costLimit;
|
|
564
|
+
delete ad.CostLimit;
|
|
565
|
+
delete ad.initialCostLimit;
|
|
566
|
+
delete ad.InitialCostLimit;
|
|
567
|
+
clean.additionalData = ad;
|
|
568
|
+
}
|
|
569
|
+
return clean;
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return meta;
|
|
573
|
+
}
|
|
574
|
+
}, []);
|
|
552
575
|
// Read deterministic flags from query string once
|
|
553
576
|
const deterministicFlags = React.useMemo(() => {
|
|
554
577
|
try {
|
|
@@ -637,7 +660,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
637
660
|
const SR = window.SpeechRecognition ||
|
|
638
661
|
window.webkitSpeechRecognition;
|
|
639
662
|
if (!SR) {
|
|
640
|
-
setVoiceError("Voice input is not supported in this browser");
|
|
641
663
|
return;
|
|
642
664
|
}
|
|
643
665
|
const r = new SR();
|
|
@@ -647,7 +669,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
647
669
|
r.onstart = () => {
|
|
648
670
|
setIsListening(true);
|
|
649
671
|
prevPlaceholderRef.current = inputPlaceholder;
|
|
650
|
-
setVoiceError(null);
|
|
651
672
|
setInputPlaceholder("Listening...");
|
|
652
673
|
};
|
|
653
674
|
r.onresult = (event) => {
|
|
@@ -683,7 +704,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
683
704
|
};
|
|
684
705
|
r.onerror = (e) => {
|
|
685
706
|
console.warn("Speech recognition error", e);
|
|
686
|
-
setVoiceError(e?.error || "Voice input error");
|
|
687
707
|
};
|
|
688
708
|
r.onend = () => {
|
|
689
709
|
setIsListening(false);
|
|
@@ -698,7 +718,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
698
718
|
}
|
|
699
719
|
catch (e) {
|
|
700
720
|
console.error("Failed to start voice input", e);
|
|
701
|
-
setVoiceError("Failed to start voice input");
|
|
702
721
|
}
|
|
703
722
|
}, [
|
|
704
723
|
editContext?.currentItemDescriptor?.language,
|
|
@@ -814,21 +833,21 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
814
833
|
isWaitingRef.current = false;
|
|
815
834
|
// Any content chunk is an incremental update -> reset idle timer
|
|
816
835
|
resetDotsTimer();
|
|
817
|
-
// Extract cost/token data from
|
|
818
|
-
const
|
|
819
|
-
if (
|
|
820
|
-
(
|
|
836
|
+
// Extract cost/token data from message.cost (new structure)
|
|
837
|
+
const cost = message.cost;
|
|
838
|
+
if (cost &&
|
|
839
|
+
(cost.total !== undefined || cost.tokens?.total !== undefined)) {
|
|
821
840
|
const nextTotals = {
|
|
822
|
-
input: Number(
|
|
823
|
-
output: Number(
|
|
824
|
-
cached: Number(
|
|
825
|
-
cacheWrite: Number(
|
|
826
|
-
inputCost: Number(
|
|
827
|
-
outputCost: Number(
|
|
828
|
-
cachedCost: Number(
|
|
829
|
-
cacheWriteCost: Number(
|
|
830
|
-
totalCost: Number(
|
|
831
|
-
currency:
|
|
841
|
+
input: Number(cost.tokens?.input) || 0,
|
|
842
|
+
output: Number(cost.tokens?.output) || 0,
|
|
843
|
+
cached: Number(cost.tokens?.cached) || 0,
|
|
844
|
+
cacheWrite: Number(cost.tokens?.cacheWrite) || 0,
|
|
845
|
+
inputCost: Number(cost.input) || 0,
|
|
846
|
+
outputCost: Number(cost.output) || 0,
|
|
847
|
+
cachedCost: Number(cost.cached) || 0,
|
|
848
|
+
cacheWriteCost: Number(cost.cacheWrite) || 0,
|
|
849
|
+
totalCost: Number(cost.total) || 0,
|
|
850
|
+
currency: "USD",
|
|
832
851
|
};
|
|
833
852
|
const anyNonZero = (nextTotals.totalCost || 0) > 0 ||
|
|
834
853
|
(nextTotals.input || 0) > 0 ||
|
|
@@ -838,6 +857,18 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
838
857
|
if (anyNonZero) {
|
|
839
858
|
setLiveTotals(nextTotals);
|
|
840
859
|
}
|
|
860
|
+
// Check cost limit if available
|
|
861
|
+
if (cost.limit &&
|
|
862
|
+
cost.total &&
|
|
863
|
+
Number(cost.total) > Number(cost.limit)) {
|
|
864
|
+
setCostLimitExceeded({
|
|
865
|
+
totalCost: Number(cost.total),
|
|
866
|
+
costLimit: Number(cost.limit),
|
|
867
|
+
initialCostLimit: Number(cost.limit),
|
|
868
|
+
});
|
|
869
|
+
setIsWaitingForResponse(false);
|
|
870
|
+
shouldCreateNewMessage.current = false;
|
|
871
|
+
}
|
|
841
872
|
}
|
|
842
873
|
// Always call setMessages and handle all logic in the callback with latest messages
|
|
843
874
|
setMessages((prev) => {
|
|
@@ -981,20 +1012,20 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
981
1012
|
}
|
|
982
1013
|
}
|
|
983
1014
|
// Extract cost/token data from tool result if present
|
|
984
|
-
const
|
|
985
|
-
if (
|
|
986
|
-
(
|
|
1015
|
+
const cost = message.cost;
|
|
1016
|
+
if (cost &&
|
|
1017
|
+
(cost.total !== undefined || cost.tokens?.total !== undefined)) {
|
|
987
1018
|
const nextTotals = {
|
|
988
|
-
input: Number(
|
|
989
|
-
output: Number(
|
|
990
|
-
cached: Number(
|
|
991
|
-
cacheWrite: Number(
|
|
992
|
-
inputCost: Number(
|
|
993
|
-
outputCost: Number(
|
|
994
|
-
cachedCost: Number(
|
|
995
|
-
cacheWriteCost: Number(
|
|
996
|
-
totalCost: Number(
|
|
997
|
-
currency:
|
|
1019
|
+
input: Number(cost.tokens?.input) || 0,
|
|
1020
|
+
output: Number(cost.tokens?.output) || 0,
|
|
1021
|
+
cached: Number(cost.tokens?.cached) || 0,
|
|
1022
|
+
cacheWrite: Number(cost.tokens?.cacheWrite) || 0,
|
|
1023
|
+
inputCost: Number(cost.input) || 0,
|
|
1024
|
+
outputCost: Number(cost.output) || 0,
|
|
1025
|
+
cachedCost: Number(cost.cached) || 0,
|
|
1026
|
+
cacheWriteCost: Number(cost.cacheWrite) || 0,
|
|
1027
|
+
totalCost: Number(cost.total) || 0,
|
|
1028
|
+
currency: "USD",
|
|
998
1029
|
};
|
|
999
1030
|
const anyNonZero = (nextTotals.totalCost || 0) > 0 ||
|
|
1000
1031
|
(nextTotals.input || 0) > 0 ||
|
|
@@ -1005,6 +1036,33 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
1005
1036
|
setLiveTotals(nextTotals);
|
|
1006
1037
|
}
|
|
1007
1038
|
}
|
|
1039
|
+
else {
|
|
1040
|
+
// Fallback: legacy aggregated totals included in the tool result data
|
|
1041
|
+
const data = message.data;
|
|
1042
|
+
if (data &&
|
|
1043
|
+
(data.totalCost !== undefined || data.totalTokens !== undefined)) {
|
|
1044
|
+
const nextTotals = {
|
|
1045
|
+
input: Number(data.totalInputTokens) || 0,
|
|
1046
|
+
output: Number(data.totalOutputTokens) || 0,
|
|
1047
|
+
cached: Number(data.totalCachedTokens) || 0,
|
|
1048
|
+
cacheWrite: Number(data.totalCacheWriteTokens) || 0,
|
|
1049
|
+
inputCost: Number(data.totalInputTokenCost) || 0,
|
|
1050
|
+
outputCost: Number(data.totalOutputTokenCost) || 0,
|
|
1051
|
+
cachedCost: Number(data.totalCachedTokenCost) || 0,
|
|
1052
|
+
cacheWriteCost: Number(data.totalCacheWriteTokenCost) || 0,
|
|
1053
|
+
totalCost: Number(data.totalCost) || 0,
|
|
1054
|
+
currency: data.currency || "USD",
|
|
1055
|
+
};
|
|
1056
|
+
const anyNonZero = (nextTotals.totalCost || 0) > 0 ||
|
|
1057
|
+
(nextTotals.input || 0) > 0 ||
|
|
1058
|
+
(nextTotals.output || 0) > 0 ||
|
|
1059
|
+
(nextTotals.cached || 0) > 0 ||
|
|
1060
|
+
(nextTotals.cacheWrite || 0) > 0;
|
|
1061
|
+
if (anyNonZero) {
|
|
1062
|
+
setLiveTotals(nextTotals);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1008
1066
|
// Update tool result directly in the messages array
|
|
1009
1067
|
if (!resultMessageId) {
|
|
1010
1068
|
return;
|
|
@@ -1076,492 +1134,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
1076
1134
|
// Tool result activity; reset idle timer
|
|
1077
1135
|
resetDotsTimer();
|
|
1078
1136
|
}, [resetDotsTimer]);
|
|
1079
|
-
// DEPRECATED: SSE-based streaming has been replaced by WebSocket
|
|
1080
|
-
// All streaming functionality is now handled by handleAgentWebSocketMessage
|
|
1081
|
-
// This function is kept commented for reference but is no longer used
|
|
1082
|
-
/*
|
|
1083
|
-
const connectToStream = useCallback(
|
|
1084
|
-
async (agentData?: AgentDetails) => {
|
|
1085
|
-
const currentAgent = agentData || agent;
|
|
1086
|
-
if (!currentAgent) return;
|
|
1087
|
-
|
|
1088
|
-
// Cancel any existing connection
|
|
1089
|
-
if (abortControllerRef.current) {
|
|
1090
|
-
abortControllerRef.current.abort();
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
const abortController = new AbortController();
|
|
1094
|
-
abortControllerRef.current = abortController;
|
|
1095
|
-
|
|
1096
|
-
try {
|
|
1097
|
-
setIsConnecting(true);
|
|
1098
|
-
|
|
1099
|
-
// Reduced: minimal logging
|
|
1100
|
-
|
|
1101
|
-
// Expose agent id globally for approval actions
|
|
1102
|
-
(window as any).currentAgentId = currentAgent.id;
|
|
1103
|
-
// Expose id for approval actions
|
|
1104
|
-
|
|
1105
|
-
// Connecting to agent stream
|
|
1106
|
-
await connectToAgentStream_DEPRECATED(
|
|
1107
|
-
currentAgent.id,
|
|
1108
|
-
(message: AgentStreamMessage) => {
|
|
1109
|
-
switch (message.type) {
|
|
1110
|
-
case "contentChunk":
|
|
1111
|
-
handleContentChunk(message, currentAgent);
|
|
1112
|
-
break;
|
|
1113
|
-
|
|
1114
|
-
case "toolCall":
|
|
1115
|
-
handleToolCall(message, currentAgent);
|
|
1116
|
-
break;
|
|
1117
|
-
|
|
1118
|
-
case "toolResult":
|
|
1119
|
-
handleToolResult(message, currentAgent);
|
|
1120
|
-
break;
|
|
1121
|
-
|
|
1122
|
-
case "statusUpdate":
|
|
1123
|
-
try {
|
|
1124
|
-
// Check both 'kind' and 'state' for backward compatibility
|
|
1125
|
-
const kind =
|
|
1126
|
-
(message as any)?.data?.kind ||
|
|
1127
|
-
(message as any)?.data?.state;
|
|
1128
|
-
if (kind === "streamOpen") {
|
|
1129
|
-
setIsConnecting(false);
|
|
1130
|
-
// Don't clear waiting state here - let it be cleared when first content arrives
|
|
1131
|
-
break;
|
|
1132
|
-
}
|
|
1133
|
-
// Live token usage totals update from backend
|
|
1134
|
-
if (kind === "tokenUsage") {
|
|
1135
|
-
const totals = (message as any)?.data?.totals;
|
|
1136
|
-
if (totals) {
|
|
1137
|
-
const totalCost = Number(totals.totalCost) || 0;
|
|
1138
|
-
const nextTotals = {
|
|
1139
|
-
input: Number(totals.totalInputTokens) || 0,
|
|
1140
|
-
output: Number(totals.totalOutputTokens) || 0,
|
|
1141
|
-
cached: Number(totals.totalCachedInputTokens) || 0,
|
|
1142
|
-
cacheWrite: Number(totals.totalCacheWriteTokens) || 0,
|
|
1143
|
-
inputCost: Number(totals.totalInputTokenCost) || 0,
|
|
1144
|
-
outputCost: Number(totals.totalOutputTokenCost) || 0,
|
|
1145
|
-
cachedCost:
|
|
1146
|
-
Number(totals.totalCachedInputTokenCost) || 0,
|
|
1147
|
-
cacheWriteCost:
|
|
1148
|
-
Number(totals.totalCacheWriteTokenCost) || 0,
|
|
1149
|
-
totalCost: totalCost,
|
|
1150
|
-
currency: totals.currency,
|
|
1151
|
-
};
|
|
1152
|
-
const anyNonZero =
|
|
1153
|
-
(nextTotals.totalCost || 0) > 0 ||
|
|
1154
|
-
(nextTotals.input || 0) > 0 ||
|
|
1155
|
-
(nextTotals.output || 0) > 0 ||
|
|
1156
|
-
(nextTotals.cached || 0) > 0 ||
|
|
1157
|
-
(nextTotals.cacheWrite || 0) > 0;
|
|
1158
|
-
if (anyNonZero) {
|
|
1159
|
-
setLiveTotals(nextTotals);
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Check if cost limit exceeded
|
|
1163
|
-
if (agent?.costLimit && totalCost > agent.costLimit) {
|
|
1164
|
-
setCostLimitExceeded({
|
|
1165
|
-
totalCost: totalCost,
|
|
1166
|
-
costLimit: agent.costLimit,
|
|
1167
|
-
initialCostLimit: agent.costLimit,
|
|
1168
|
-
});
|
|
1169
|
-
setIsWaitingForResponse(false);
|
|
1170
|
-
shouldCreateNewMessage.current = false;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
// Force a re-render to update cost display immediately
|
|
1174
|
-
setMessages((prev) => [...prev]);
|
|
1175
|
-
}
|
|
1176
|
-
break;
|
|
1177
|
-
}
|
|
1178
|
-
if (kind === "toolApprovalsRequired") {
|
|
1179
|
-
const data = (message as any).data || {};
|
|
1180
|
-
const msgId: string | undefined = data.messageId;
|
|
1181
|
-
const ids: string[] = data.toolCallIds || [];
|
|
1182
|
-
// Pause stream until approval
|
|
1183
|
-
|
|
1184
|
-
// Annotate tool calls with a temporary pending marker so UI can reflect paused state on reload
|
|
1185
|
-
if (msgId && Array.isArray(ids) && ids.length > 0) {
|
|
1186
|
-
setMessages((prev) => {
|
|
1187
|
-
const updated = prev.map((m) => {
|
|
1188
|
-
if (m.id !== msgId) return m;
|
|
1189
|
-
const existingToolCalls = m.toolCalls || [];
|
|
1190
|
-
const updatedToolCalls = existingToolCalls.map(
|
|
1191
|
-
(tc) => {
|
|
1192
|
-
if (!ids.includes(tc.toolCallId)) return tc;
|
|
1193
|
-
const fn = tc.functionName || "";
|
|
1194
|
-
return {
|
|
1195
|
-
...tc,
|
|
1196
|
-
functionName: fn.includes("(pending approval)")
|
|
1197
|
-
? fn
|
|
1198
|
-
: fn + " (pending approval)",
|
|
1199
|
-
};
|
|
1200
|
-
},
|
|
1201
|
-
);
|
|
1202
|
-
return { ...m, toolCalls: updatedToolCalls };
|
|
1203
|
-
});
|
|
1204
|
-
messagesRef.current = updated;
|
|
1205
|
-
return updated;
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// Keep the stream open; just clear waiting flags so UI reflects pause state
|
|
1210
|
-
try {
|
|
1211
|
-
setIsConnecting(false);
|
|
1212
|
-
setIsWaitingForResponse(false);
|
|
1213
|
-
} catch {}
|
|
1214
|
-
break;
|
|
1215
|
-
}
|
|
1216
|
-
if (kind === "contextWindow") {
|
|
1217
|
-
const data = (message as any).data || {};
|
|
1218
|
-
// Store last context window status in a ref so we can render it below
|
|
1219
|
-
(window as any).__agentContextWindowStatus = {
|
|
1220
|
-
model: data.model,
|
|
1221
|
-
normalizedModel: data.normalizedModel,
|
|
1222
|
-
contextWindowTokens: data.contextWindowTokens,
|
|
1223
|
-
maxCompletionTokens: data.maxCompletionTokens,
|
|
1224
|
-
estimatedInputTokens: data.estimatedInputTokens,
|
|
1225
|
-
messageCount: data.messageCount,
|
|
1226
|
-
contextUsedPercent: data.contextUsedPercent,
|
|
1227
|
-
};
|
|
1228
|
-
// Force a re-render by toggling state (cheap no-op)
|
|
1229
|
-
setMessages((prev) => [...prev]);
|
|
1230
|
-
} else if (kind === "contextChanged") {
|
|
1231
|
-
const data = (message as any).data || {};
|
|
1232
|
-
const nextContext = data.context || {};
|
|
1233
|
-
// Merge incoming context into local metadata
|
|
1234
|
-
setAgentMetadata((prev) => {
|
|
1235
|
-
const current = (prev || {}) as AgentMetadata;
|
|
1236
|
-
// Exclude top-level context to avoid duplicate keys when spreading
|
|
1237
|
-
const currentWithoutContext = { ...current };
|
|
1238
|
-
delete (currentWithoutContext as any).context;
|
|
1239
|
-
const next: AgentMetadata = {
|
|
1240
|
-
...currentWithoutContext,
|
|
1241
|
-
additionalData: {
|
|
1242
|
-
...(current.additionalData || {}),
|
|
1243
|
-
context: nextContext,
|
|
1244
|
-
},
|
|
1245
|
-
} as AgentMetadata;
|
|
1246
|
-
return next;
|
|
1247
|
-
});
|
|
1248
|
-
// Also reflect in agent metadata string for consistency
|
|
1249
|
-
setAgent((prevAgent) => {
|
|
1250
|
-
if (!prevAgent) return prevAgent;
|
|
1251
|
-
try {
|
|
1252
|
-
const currentMeta: AgentMetadata | null = (() => {
|
|
1253
|
-
try {
|
|
1254
|
-
return prevAgent.agentContext
|
|
1255
|
-
? (JSON.parse(
|
|
1256
|
-
prevAgent.agentContext,
|
|
1257
|
-
) as AgentMetadata)
|
|
1258
|
-
: null;
|
|
1259
|
-
} catch {
|
|
1260
|
-
return null;
|
|
1261
|
-
}
|
|
1262
|
-
})();
|
|
1263
|
-
const nextMeta: AgentMetadata = {
|
|
1264
|
-
...(currentMeta || ({} as AgentMetadata)),
|
|
1265
|
-
...nextContext,
|
|
1266
|
-
} as AgentMetadata;
|
|
1267
|
-
return {
|
|
1268
|
-
...prevAgent,
|
|
1269
|
-
agentContext: JSON.stringify(nextMeta),
|
|
1270
|
-
};
|
|
1271
|
-
} catch {
|
|
1272
|
-
return prevAgent;
|
|
1273
|
-
}
|
|
1274
|
-
});
|
|
1275
|
-
} else if (kind === "cost_limit_reached") {
|
|
1276
|
-
// Cost limit has been reached - show banner and stop waiting
|
|
1277
|
-
// NOTE: Stream stays connected so user can extend limit and continue immediately
|
|
1278
|
-
console.log(
|
|
1279
|
-
"[AgentTerminal] Cost limit reached notification received",
|
|
1280
|
-
message,
|
|
1281
|
-
);
|
|
1282
|
-
const data = (message as any).data || {};
|
|
1283
|
-
const totalCost = Number(data.totalCost) || 0;
|
|
1284
|
-
// Use costLimit from the notification data, or fall back to agent.costLimit
|
|
1285
|
-
const costLimit =
|
|
1286
|
-
Number(data.costLimit) || agent?.costLimit || 0;
|
|
1287
|
-
|
|
1288
|
-
console.log(
|
|
1289
|
-
"[AgentTerminal] Setting cost limit exceeded state:",
|
|
1290
|
-
{
|
|
1291
|
-
totalCost,
|
|
1292
|
-
costLimit,
|
|
1293
|
-
agentCostLimit: agent?.costLimit,
|
|
1294
|
-
dataCostLimit: data.costLimit,
|
|
1295
|
-
dataValues: data,
|
|
1296
|
-
},
|
|
1297
|
-
);
|
|
1298
|
-
|
|
1299
|
-
// Set the state with values from the notification
|
|
1300
|
-
setCostLimitExceeded({
|
|
1301
|
-
totalCost: totalCost,
|
|
1302
|
-
costLimit: costLimit,
|
|
1303
|
-
initialCostLimit: costLimit,
|
|
1304
|
-
});
|
|
1305
|
-
|
|
1306
|
-
// Clear waiting states but keep stream connected
|
|
1307
|
-
setIsWaitingForResponse(false);
|
|
1308
|
-
setIsConnecting(false);
|
|
1309
|
-
shouldCreateNewMessage.current = false;
|
|
1310
|
-
break;
|
|
1311
|
-
} else if (
|
|
1312
|
-
kind === "toolApprovalGranted" ||
|
|
1313
|
-
kind === "toolApprovalRejected"
|
|
1314
|
-
) {
|
|
1315
|
-
const data = (message as any).data || {};
|
|
1316
|
-
const toolCallId: string | undefined = data.toolCallId;
|
|
1317
|
-
const msgId: string | undefined = data.messageId;
|
|
1318
|
-
// Processing tool approval
|
|
1319
|
-
if (toolCallId && msgId) {
|
|
1320
|
-
setMessages((prev) => {
|
|
1321
|
-
const updated = prev.map((m) => {
|
|
1322
|
-
if (m.id !== msgId) return m;
|
|
1323
|
-
const existingToolCalls = m.toolCalls || [];
|
|
1324
|
-
const updatedToolCalls = existingToolCalls.map(
|
|
1325
|
-
(tc) => {
|
|
1326
|
-
if (tc.toolCallId !== toolCallId) return tc;
|
|
1327
|
-
const suffix =
|
|
1328
|
-
kind === "toolApprovalGranted"
|
|
1329
|
-
? " (approved)"
|
|
1330
|
-
: " (rejected)";
|
|
1331
|
-
// Remove "(pending approval)" suffix before adding new suffix
|
|
1332
|
-
const baseFunctionName = (tc.functionName || "")
|
|
1333
|
-
.replace(" (pending approval)", "")
|
|
1334
|
-
.trim();
|
|
1335
|
-
const newFunctionName = baseFunctionName + suffix;
|
|
1336
|
-
// Update function name with approval suffix
|
|
1337
|
-
return {
|
|
1338
|
-
...tc,
|
|
1339
|
-
functionName: newFunctionName,
|
|
1340
|
-
};
|
|
1341
|
-
},
|
|
1342
|
-
);
|
|
1343
|
-
return { ...m, toolCalls: updatedToolCalls };
|
|
1344
|
-
});
|
|
1345
|
-
messagesRef.current = updated;
|
|
1346
|
-
return updated;
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
|
-
break;
|
|
1350
|
-
}
|
|
1351
|
-
} catch {}
|
|
1352
|
-
break;
|
|
1353
|
-
|
|
1354
|
-
case "completed":
|
|
1355
|
-
const completedMessageId = message.data?.messageId;
|
|
1356
|
-
|
|
1357
|
-
// If the completed event carries full messages, merge them into state
|
|
1358
|
-
try {
|
|
1359
|
-
const completionMessages = (message as any)?.data
|
|
1360
|
-
?.messages as any[] | undefined;
|
|
1361
|
-
if (
|
|
1362
|
-
completionMessages &&
|
|
1363
|
-
Array.isArray(completionMessages) &&
|
|
1364
|
-
completionMessages.length > 0
|
|
1365
|
-
) {
|
|
1366
|
-
setMessages((prev) => {
|
|
1367
|
-
// Mark all completion messages as completed
|
|
1368
|
-
const dbMessages = completionMessages.map((msg) => ({
|
|
1369
|
-
...msg,
|
|
1370
|
-
isCompleted: true,
|
|
1371
|
-
messageType: "completed" as const,
|
|
1372
|
-
})) as AgentChatMessage[];
|
|
1373
|
-
|
|
1374
|
-
// Use ID-based merge to prevent duplicates
|
|
1375
|
-
const merged = mergeMessagesById(dbMessages, prev);
|
|
1376
|
-
messagesRef.current = merged;
|
|
1377
|
-
return merged;
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1380
|
-
} catch (e) {
|
|
1381
|
-
console.warn("⚠️ Failed to merge completion messages:", e);
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
// Mark the specific message as completed by messageId
|
|
1385
|
-
if (completedMessageId) {
|
|
1386
|
-
setMessages((prev) => {
|
|
1387
|
-
const updated = prev.map((msg) => {
|
|
1388
|
-
if (msg.id === completedMessageId) {
|
|
1389
|
-
const updatedMessage = {
|
|
1390
|
-
...msg,
|
|
1391
|
-
isCompleted: true,
|
|
1392
|
-
messageType: "completed" as const,
|
|
1393
|
-
};
|
|
1394
|
-
|
|
1395
|
-
// Update token usage data if provided in the completed event
|
|
1396
|
-
if (message.data) {
|
|
1397
|
-
const data = message.data;
|
|
1398
|
-
if (data.numInputTokens !== undefined) {
|
|
1399
|
-
updatedMessage.inputTokens = data.numInputTokens;
|
|
1400
|
-
}
|
|
1401
|
-
if (data.numOutputTokens !== undefined) {
|
|
1402
|
-
updatedMessage.outputTokens = data.numOutputTokens;
|
|
1403
|
-
}
|
|
1404
|
-
if (data.numCachedTokens !== undefined) {
|
|
1405
|
-
updatedMessage.cachedInputTokens =
|
|
1406
|
-
data.numCachedTokens;
|
|
1407
|
-
}
|
|
1408
|
-
// Update total tokens used
|
|
1409
|
-
updatedMessage.tokensUsed =
|
|
1410
|
-
(updatedMessage.inputTokens || 0) +
|
|
1411
|
-
(updatedMessage.outputTokens || 0);
|
|
1412
|
-
|
|
1413
|
-
// Update cost data if provided in the completed event
|
|
1414
|
-
if (data.inputTokenCost !== undefined) {
|
|
1415
|
-
updatedMessage.inputTokenCost = data.inputTokenCost;
|
|
1416
|
-
}
|
|
1417
|
-
if (data.outputTokenCost !== undefined) {
|
|
1418
|
-
updatedMessage.outputTokenCost =
|
|
1419
|
-
data.outputTokenCost;
|
|
1420
|
-
}
|
|
1421
|
-
if (
|
|
1422
|
-
data.cachedInputTokenCost !== undefined ||
|
|
1423
|
-
data.cachedTokenCost !== undefined
|
|
1424
|
-
) {
|
|
1425
|
-
updatedMessage.cachedInputTokenCost =
|
|
1426
|
-
data.cachedInputTokenCost ?? data.cachedTokenCost;
|
|
1427
|
-
}
|
|
1428
|
-
if (data.totalCost !== undefined) {
|
|
1429
|
-
updatedMessage.totalCost = data.totalCost;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
// Handle content that might only be sent in the completed event
|
|
1433
|
-
if (data.deltaContent && data.deltaContent.trim()) {
|
|
1434
|
-
if (!data.isIncremental) {
|
|
1435
|
-
// Non-incremental: replace the entire content
|
|
1436
|
-
updatedMessage.content = data.deltaContent;
|
|
1437
|
-
} else {
|
|
1438
|
-
// Incremental: append to existing content
|
|
1439
|
-
updatedMessage.content =
|
|
1440
|
-
(updatedMessage.content || "") +
|
|
1441
|
-
data.deltaContent;
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
return updatedMessage;
|
|
1447
|
-
}
|
|
1448
|
-
return msg;
|
|
1449
|
-
});
|
|
1450
|
-
messagesRef.current = updated;
|
|
1451
|
-
return updated;
|
|
1452
|
-
});
|
|
1453
|
-
} else {
|
|
1454
|
-
// Fallback: Mark any streaming messages as completed (old behavior)
|
|
1455
|
-
console.warn(
|
|
1456
|
-
"⚠️ No messageId in completed event, falling back to marking all streaming messages as completed",
|
|
1457
|
-
);
|
|
1458
|
-
setMessages((prev) => {
|
|
1459
|
-
const updated = prev.map((msg) =>
|
|
1460
|
-
!msg.isCompleted && msg.messageType === "streaming"
|
|
1461
|
-
? {
|
|
1462
|
-
...msg,
|
|
1463
|
-
isCompleted: true,
|
|
1464
|
-
messageType: "completed",
|
|
1465
|
-
}
|
|
1466
|
-
: msg,
|
|
1467
|
-
);
|
|
1468
|
-
messagesRef.current = updated;
|
|
1469
|
-
return updated;
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
|
-
// Ensure waiting state is cleared when stream completes
|
|
1473
|
-
setIsWaitingForResponse(false);
|
|
1474
|
-
isWaitingRef.current = false;
|
|
1475
|
-
shouldCreateNewMessage.current = false;
|
|
1476
|
-
// Streaming finished; update indicator
|
|
1477
|
-
resetDotsTimer();
|
|
1478
|
-
break;
|
|
1479
|
-
|
|
1480
|
-
case "contextUpdate":
|
|
1481
|
-
// Update agent context when backend sends context update
|
|
1482
|
-
try {
|
|
1483
|
-
console.log("📥 Received contextUpdate message:", message);
|
|
1484
|
-
const updatedContext = message.data as AgentMetadata;
|
|
1485
|
-
if (updatedContext) {
|
|
1486
|
-
console.log(
|
|
1487
|
-
"📝 Updating agent metadata with:",
|
|
1488
|
-
updatedContext,
|
|
1489
|
-
);
|
|
1490
|
-
|
|
1491
|
-
// Check if there's a todo list in the context
|
|
1492
|
-
if (updatedContext.additionalData?.todoList) {
|
|
1493
|
-
console.log(
|
|
1494
|
-
"✅ Todo list found in context update:",
|
|
1495
|
-
updatedContext.additionalData.todoList,
|
|
1496
|
-
);
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
// Update local metadata state
|
|
1500
|
-
setAgentMetadata((prev) => ({
|
|
1501
|
-
...prev,
|
|
1502
|
-
...updatedContext,
|
|
1503
|
-
}));
|
|
1504
|
-
|
|
1505
|
-
// Update agent state with new context
|
|
1506
|
-
setAgent((prevAgent) => {
|
|
1507
|
-
if (!prevAgent) return prevAgent;
|
|
1508
|
-
return {
|
|
1509
|
-
...prevAgent,
|
|
1510
|
-
agentContext: JSON.stringify(updatedContext),
|
|
1511
|
-
};
|
|
1512
|
-
});
|
|
1513
|
-
|
|
1514
|
-
console.log("✅ Context updated from backend successfully");
|
|
1515
|
-
} else {
|
|
1516
|
-
console.warn(
|
|
1517
|
-
"⚠️ Context update received but updatedContext is null/undefined",
|
|
1518
|
-
);
|
|
1519
|
-
}
|
|
1520
|
-
} catch (err) {
|
|
1521
|
-
console.error("❌ Error handling context update:", err);
|
|
1522
|
-
}
|
|
1523
|
-
break;
|
|
1524
|
-
|
|
1525
|
-
case "error":
|
|
1526
|
-
console.error("❌ Stream error:", message.error);
|
|
1527
|
-
setError(message.error || "Stream error occurred");
|
|
1528
|
-
setIsWaitingForResponse(false);
|
|
1529
|
-
isWaitingRef.current = false;
|
|
1530
|
-
shouldCreateNewMessage.current = false;
|
|
1531
|
-
// Error ends streaming; update indicator
|
|
1532
|
-
resetDotsTimer();
|
|
1533
|
-
break;
|
|
1534
|
-
|
|
1535
|
-
default:
|
|
1536
|
-
console.warn("❓ Unhandled message type:", {
|
|
1537
|
-
type: message.type,
|
|
1538
|
-
typeOf: typeof message.type,
|
|
1539
|
-
length: message.type?.length,
|
|
1540
|
-
charCodes: message.type
|
|
1541
|
-
?.split("")
|
|
1542
|
-
.map((c) => c.charCodeAt(0)),
|
|
1543
|
-
message: message,
|
|
1544
|
-
});
|
|
1545
|
-
break;
|
|
1546
|
-
}
|
|
1547
|
-
},
|
|
1548
|
-
abortController.signal,
|
|
1549
|
-
clientSessionIdRef.current || undefined,
|
|
1550
|
-
);
|
|
1551
|
-
} catch (err) {
|
|
1552
|
-
if (!abortController.signal.aborted) {
|
|
1553
|
-
console.error("Stream connection failed:", err);
|
|
1554
|
-
setError("Failed to connect to agent stream");
|
|
1555
|
-
}
|
|
1556
|
-
} finally {
|
|
1557
|
-
setIsConnecting(false);
|
|
1558
|
-
// Guard: clear waiting state if connection finished without content
|
|
1559
|
-
setIsWaitingForResponse(false);
|
|
1560
|
-
}
|
|
1561
|
-
},
|
|
1562
|
-
[agent?.id, handleContentChunk, handleToolCall, handleToolResult],
|
|
1563
|
-
);
|
|
1564
|
-
*/
|
|
1565
1137
|
// Listen for local approval resolution to update UI
|
|
1566
1138
|
useEffect(() => {
|
|
1567
1139
|
const onApprovalResolved = (ev) => {
|
|
@@ -1750,7 +1322,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
1750
1322
|
nextMetadata = localCtx;
|
|
1751
1323
|
}
|
|
1752
1324
|
if (nextMetadata) {
|
|
1753
|
-
setAgentMetadata(nextMetadata);
|
|
1325
|
+
setAgentMetadata(sanitizeAgentMetadata(nextMetadata));
|
|
1754
1326
|
// If an initial prompt is provided via metadata, seed the input once
|
|
1755
1327
|
try {
|
|
1756
1328
|
const maybePrompt = nextMetadata?.additionalData
|
|
@@ -1807,12 +1379,17 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
1807
1379
|
agentData.totalCost &&
|
|
1808
1380
|
agentData.totalCost > agentData.costLimit) {
|
|
1809
1381
|
// Fallback: check if current cost exceeds limit
|
|
1382
|
+
console.log(`[AgentTerminal] Cost limit exceeded on load: $${agentData.totalCost.toFixed(4)} > $${agentData.costLimit.toFixed(4)}`);
|
|
1810
1383
|
setCostLimitExceeded({
|
|
1811
1384
|
totalCost: agentData.totalCost,
|
|
1812
1385
|
costLimit: agentData.costLimit,
|
|
1813
1386
|
initialCostLimit: agentData.costLimit,
|
|
1814
1387
|
});
|
|
1815
1388
|
}
|
|
1389
|
+
else {
|
|
1390
|
+
// Debug: log why banner wasn't set
|
|
1391
|
+
console.log(`[AgentTerminal] Cost limit check on load: costLimit=${agentData.costLimit}, totalCost=${agentData.totalCost}, exceeds=${agentData.totalCost && agentData.costLimit ? agentData.totalCost > agentData.costLimit : false}`);
|
|
1392
|
+
}
|
|
1816
1393
|
}
|
|
1817
1394
|
catch (e) {
|
|
1818
1395
|
console.error("Failed to check cost limit on load:", e);
|
|
@@ -1835,21 +1412,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
1835
1412
|
}
|
|
1836
1413
|
})();
|
|
1837
1414
|
// For existing agents, use database metadata or none
|
|
1838
|
-
setAgentMetadata(parsedMeta);
|
|
1839
|
-
// Connect to stream if agent is running
|
|
1840
|
-
// Keep stream open for: running, waiting for approval, or cost limit reached
|
|
1841
|
-
const isRunning = agentData.status === "running" || agentData.status === 1;
|
|
1842
|
-
const isWaitingForApproval = agentData.status === "waitingForApproval" ||
|
|
1843
|
-
agentData.status === 2;
|
|
1844
|
-
const isCostLimitReached = agentData.status === "costLimitReached" ||
|
|
1845
|
-
agentData.status === 7;
|
|
1846
|
-
// NOTE: SSE reconnection logic removed - no longer needed with WebSocket
|
|
1847
|
-
// The WebSocket subscription (in the useEffect below) handles reconnection automatically
|
|
1848
|
-
// Just set the streaming state if agent is running
|
|
1849
|
-
if (isRunning && isActive) {
|
|
1850
|
-
shouldCreateNewMessage.current = false;
|
|
1851
|
-
// State will be set by the WebSocket subscription useEffect
|
|
1852
|
-
}
|
|
1415
|
+
setAgentMetadata(sanitizeAgentMetadata(parsedMeta));
|
|
1853
1416
|
}
|
|
1854
1417
|
catch (err) {
|
|
1855
1418
|
console.error("❌ Failed to load agent:", err);
|
|
@@ -1886,6 +1449,37 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
1886
1449
|
loadAgent();
|
|
1887
1450
|
}
|
|
1888
1451
|
}, [isActive, agent?.id, loadAgent]);
|
|
1452
|
+
// Watch for cost limit exceeded based on agent status or cost values
|
|
1453
|
+
useEffect(() => {
|
|
1454
|
+
if (!agent) {
|
|
1455
|
+
setCostLimitExceeded(null);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
// Check if cost limit exceeded based on status or cost values
|
|
1459
|
+
const statusIndicatesLimit = agent.status === "costLimitReached" || agent.status === 7;
|
|
1460
|
+
const costExceedsLimit = agent.costLimit && agent.totalCost && agent.totalCost > agent.costLimit;
|
|
1461
|
+
if (statusIndicatesLimit || costExceedsLimit) {
|
|
1462
|
+
// Only set if not already set to avoid unnecessary re-renders
|
|
1463
|
+
setCostLimitExceeded((prev) => {
|
|
1464
|
+
const totalCost = agent.totalCost || 0;
|
|
1465
|
+
const costLimit = agent.costLimit || 0;
|
|
1466
|
+
// Check if values actually changed
|
|
1467
|
+
if (prev?.totalCost === totalCost && prev?.costLimit === costLimit) {
|
|
1468
|
+
return prev;
|
|
1469
|
+
}
|
|
1470
|
+
console.log(`[AgentTerminal] Cost limit exceeded detected: $${totalCost.toFixed(4)} / $${costLimit.toFixed(4)}`);
|
|
1471
|
+
return {
|
|
1472
|
+
totalCost,
|
|
1473
|
+
costLimit,
|
|
1474
|
+
initialCostLimit: costLimit,
|
|
1475
|
+
};
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
else {
|
|
1479
|
+
// Clear cost limit exceeded if status changed back
|
|
1480
|
+
setCostLimitExceeded((prev) => (prev ? null : prev));
|
|
1481
|
+
}
|
|
1482
|
+
}, [agent?.status, agent?.totalCost, agent?.costLimit]);
|
|
1889
1483
|
// WebSocket message handler for agent streaming
|
|
1890
1484
|
const handleAgentWebSocketMessage = useCallback((message) => {
|
|
1891
1485
|
if (!agent)
|
|
@@ -1966,7 +1560,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
1966
1560
|
}
|
|
1967
1561
|
// Handle agent:run:delta (content, tools, etc.)
|
|
1968
1562
|
if (messageType === "agent:run:delta") {
|
|
1969
|
-
const { seq, type, data } = message.payload;
|
|
1563
|
+
const { seq, type, data, cost } = message.payload;
|
|
1970
1564
|
// Deduplicate by sequence
|
|
1971
1565
|
if (seq && seq <= lastSeqRef.current) {
|
|
1972
1566
|
return; // Already processed
|
|
@@ -1978,6 +1572,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
1978
1572
|
const agentStreamMessage = {
|
|
1979
1573
|
type,
|
|
1980
1574
|
data,
|
|
1575
|
+
cost,
|
|
1981
1576
|
timestamp: new Date().toISOString(),
|
|
1982
1577
|
};
|
|
1983
1578
|
if (type === "ContentChunk" || type === "contentChunk") {
|
|
@@ -1991,9 +1586,9 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
1991
1586
|
}
|
|
1992
1587
|
return;
|
|
1993
1588
|
}
|
|
1994
|
-
//
|
|
1589
|
+
// Unified: agent:run:status (state only)
|
|
1995
1590
|
if (messageType === "agent:run:status") {
|
|
1996
|
-
const { seq,
|
|
1591
|
+
const { seq, data: statusData } = message.payload;
|
|
1997
1592
|
// Deduplicate by sequence
|
|
1998
1593
|
if (seq && seq <= lastSeqRef.current) {
|
|
1999
1594
|
return;
|
|
@@ -2001,16 +1596,28 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2001
1596
|
if (seq) {
|
|
2002
1597
|
lastSeqRef.current = seq;
|
|
2003
1598
|
}
|
|
2004
|
-
// Route based on
|
|
1599
|
+
// Route based on statusData.state
|
|
2005
1600
|
try {
|
|
2006
|
-
if (
|
|
1601
|
+
if (statusData?.state === "streamOpen") {
|
|
2007
1602
|
setIsConnecting(false);
|
|
2008
1603
|
return;
|
|
2009
1604
|
}
|
|
2010
|
-
if (
|
|
1605
|
+
if (statusData?.state === "tokenUsage") {
|
|
2011
1606
|
const totals = statusData?.totals;
|
|
2012
1607
|
if (totals) {
|
|
2013
1608
|
const totalCost = Number(totals.totalCost) || 0;
|
|
1609
|
+
const statusCostLimit = (() => {
|
|
1610
|
+
try {
|
|
1611
|
+
const v = statusData?.costLimit;
|
|
1612
|
+
const n = v != null ? Number(v) : undefined;
|
|
1613
|
+
return Number.isFinite(n) && n > 0
|
|
1614
|
+
? n
|
|
1615
|
+
: undefined;
|
|
1616
|
+
}
|
|
1617
|
+
catch {
|
|
1618
|
+
return undefined;
|
|
1619
|
+
}
|
|
1620
|
+
})();
|
|
2014
1621
|
const nextTotals = {
|
|
2015
1622
|
input: Number(totals.totalInputTokens) || 0,
|
|
2016
1623
|
output: Number(totals.totalOutputTokens) || 0,
|
|
@@ -2031,6 +1638,13 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2031
1638
|
if (anyNonZero) {
|
|
2032
1639
|
setLiveTotals(nextTotals);
|
|
2033
1640
|
}
|
|
1641
|
+
// If server provides costLimit along with totals, persist it locally
|
|
1642
|
+
if (statusCostLimit) {
|
|
1643
|
+
setAgent((prev) => prev &&
|
|
1644
|
+
(!prev.costLimit || prev.costLimit !== statusCostLimit)
|
|
1645
|
+
? { ...prev, costLimit: statusCostLimit }
|
|
1646
|
+
: prev);
|
|
1647
|
+
}
|
|
2034
1648
|
if (agent?.costLimit && totalCost > agent.costLimit) {
|
|
2035
1649
|
setCostLimitExceeded({
|
|
2036
1650
|
totalCost: totalCost,
|
|
@@ -2044,7 +1658,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2044
1658
|
}
|
|
2045
1659
|
return;
|
|
2046
1660
|
}
|
|
2047
|
-
if (
|
|
1661
|
+
if (statusData?.state === "ToolApprovalsRequired") {
|
|
2048
1662
|
const msgId = statusData.messageId;
|
|
2049
1663
|
const ids = statusData.toolCallIds || [];
|
|
2050
1664
|
if (msgId && Array.isArray(ids) && ids.length > 0) {
|
|
@@ -2074,7 +1688,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2074
1688
|
setIsWaitingForResponse(false);
|
|
2075
1689
|
return;
|
|
2076
1690
|
}
|
|
2077
|
-
if (
|
|
1691
|
+
if (statusData?.state === "CostLimitReached") {
|
|
2078
1692
|
const totalCost = Number(statusData.totalCost) || 0;
|
|
2079
1693
|
const costLimit = Number(statusData.costLimit) || agent?.costLimit || 0;
|
|
2080
1694
|
setCostLimitExceeded({
|
|
@@ -2086,7 +1700,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2086
1700
|
setIsConnecting(false);
|
|
2087
1701
|
return;
|
|
2088
1702
|
}
|
|
2089
|
-
if (
|
|
1703
|
+
if (statusData?.state === "contextWindow") {
|
|
2090
1704
|
window.__agentContextWindowStatus = {
|
|
2091
1705
|
model: statusData.model,
|
|
2092
1706
|
normalizedModel: statusData.normalizedModel,
|
|
@@ -2099,7 +1713,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2099
1713
|
setMessages((prev) => [...prev]);
|
|
2100
1714
|
return;
|
|
2101
1715
|
}
|
|
2102
|
-
if (
|
|
1716
|
+
if (statusData?.state === "contextChanged") {
|
|
2103
1717
|
const nextContext = statusData.context || {};
|
|
2104
1718
|
setAgentMetadata((prev) => {
|
|
2105
1719
|
const current = (prev || {});
|
|
@@ -2122,7 +1736,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2122
1736
|
}
|
|
2123
1737
|
return;
|
|
2124
1738
|
}
|
|
2125
|
-
//
|
|
1739
|
+
// Lifecycle: agent:run:complete
|
|
2126
1740
|
if (messageType === "agent:run:complete") {
|
|
2127
1741
|
console.log("[AgentTerminal] Agent run completed via WebSocket:", agentId);
|
|
2128
1742
|
// Reset deduplication for the next run
|
|
@@ -2146,7 +1760,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2146
1760
|
resetDotsTimer();
|
|
2147
1761
|
return;
|
|
2148
1762
|
}
|
|
2149
|
-
//
|
|
1763
|
+
// Lifecycle: agent:run:error
|
|
2150
1764
|
if (messageType === "agent:run:error") {
|
|
2151
1765
|
const errorMsg = message.payload?.error || "Unknown error";
|
|
2152
1766
|
console.error("[AgentTerminal] Agent run error via WebSocket:", errorMsg);
|
|
@@ -2159,6 +1773,20 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2159
1773
|
resetDotsTimer();
|
|
2160
1774
|
return;
|
|
2161
1775
|
}
|
|
1776
|
+
// Backward-compat: map agent:status:changed to unified status event
|
|
1777
|
+
if (messageType === "agent:status:changed") {
|
|
1778
|
+
try {
|
|
1779
|
+
const { agentId: aid, status } = message.payload || {};
|
|
1780
|
+
if (!aid || aid !== agentId)
|
|
1781
|
+
return;
|
|
1782
|
+
// Treat as unified statusChanged kind
|
|
1783
|
+
setAgent((prev) => (prev ? { ...prev, status } : prev));
|
|
1784
|
+
}
|
|
1785
|
+
catch (err) {
|
|
1786
|
+
console.error("[AgentTerminal] Error handling legacy agent:status:changed:", err);
|
|
1787
|
+
}
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
2162
1790
|
}, [
|
|
2163
1791
|
agent,
|
|
2164
1792
|
handleContentChunk,
|
|
@@ -2257,33 +1885,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2257
1885
|
subscribedAgentIdRef.current = null;
|
|
2258
1886
|
};
|
|
2259
1887
|
}, [isActive, agentStub.id, editContext?.addSocketMessageListener]);
|
|
2260
|
-
// Handle stream connection when agent becomes active/inactive
|
|
2261
|
-
// NOTE: SSE connection disabled - now using WebSocket exclusively
|
|
2262
|
-
// Keeping this code commented for easy rollback if needed
|
|
2263
|
-
/*
|
|
2264
|
-
useEffect(() => {
|
|
2265
|
-
if (!agent) return;
|
|
2266
|
-
|
|
2267
|
-
const isRunning = agent.status === "running" || (agent.status as any) === 1;
|
|
2268
|
-
const isWaitingForApproval =
|
|
2269
|
-
agent.status === "waitingForApproval" || (agent.status as any) === 2;
|
|
2270
|
-
const isCostLimitReached =
|
|
2271
|
-
agent.status === "costLimitReached" || (agent.status as any) === 7;
|
|
2272
|
-
|
|
2273
|
-
const shouldBeConnected =
|
|
2274
|
-
isRunning || isWaitingForApproval || isCostLimitReached;
|
|
2275
|
-
|
|
2276
|
-
if (isActive && shouldBeConnected && !abortControllerRef.current) {
|
|
2277
|
-
// Agent became active and should be connected - connect to stream
|
|
2278
|
-
connectToStream();
|
|
2279
|
-
} else if (!isActive && abortControllerRef.current) {
|
|
2280
|
-
// Agent became inactive - disconnect from stream
|
|
2281
|
-
abortControllerRef.current.abort();
|
|
2282
|
-
abortControllerRef.current = null;
|
|
2283
|
-
setIsConnecting(false);
|
|
2284
|
-
}
|
|
2285
|
-
}, [isActive, agent?.status, connectToStream]);
|
|
2286
|
-
*/
|
|
2287
1888
|
// Focus prompt when requested globally (from AI command)
|
|
2288
1889
|
useEffect(() => {
|
|
2289
1890
|
const focusHandler = () => {
|
|
@@ -2361,30 +1962,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2361
1962
|
}
|
|
2362
1963
|
catch { }
|
|
2363
1964
|
}, [agentMetadata, agent?.mode]);
|
|
2364
|
-
const updateMode = useCallback(async (nextMode) => {
|
|
2365
|
-
setMode(nextMode);
|
|
2366
|
-
const current = agentMetadata || {};
|
|
2367
|
-
const nextMeta = {
|
|
2368
|
-
...current,
|
|
2369
|
-
mode: nextMode,
|
|
2370
|
-
};
|
|
2371
|
-
try {
|
|
2372
|
-
if (!agent?.id) {
|
|
2373
|
-
setAgentMetadata(nextMeta);
|
|
2374
|
-
return;
|
|
2375
|
-
}
|
|
2376
|
-
if (agent.status === "new") {
|
|
2377
|
-
setAgentMetadata(nextMeta);
|
|
2378
|
-
return;
|
|
2379
|
-
}
|
|
2380
|
-
await updateAgentContext(agent.id, nextMeta);
|
|
2381
|
-
setAgentMetadata(nextMeta);
|
|
2382
|
-
setAgent((prev) => prev ? { ...prev, metadata: JSON.stringify(nextMeta) } : prev);
|
|
2383
|
-
}
|
|
2384
|
-
catch (e) {
|
|
2385
|
-
console.error("Failed to persist mode change", e);
|
|
2386
|
-
}
|
|
2387
|
-
}, [agent?.id, agent?.status, agentMetadata]);
|
|
2388
1965
|
// Auto-scroll when messages change (only if user hasn't manually scrolled up)
|
|
2389
1966
|
useLayoutEffect(() => {
|
|
2390
1967
|
if (shouldAutoScroll) {
|
|
@@ -2578,8 +2155,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2578
2155
|
const selectionFromCtx = metaCtx?.componentIds?.length
|
|
2579
2156
|
? metaCtx.componentIds
|
|
2580
2157
|
: undefined;
|
|
2581
|
-
const effectiveSelection = selectionFromCtx;
|
|
2582
|
-
const selectedTextFromCtx = metaCtx?.comment?.selectedText || undefined;
|
|
2583
2158
|
const request = {
|
|
2584
2159
|
agentId: agent.id,
|
|
2585
2160
|
message: text.trim(),
|
|
@@ -2838,118 +2413,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
2838
2413
|
console.error("Failed to update agent metadata (add items/pages)", e);
|
|
2839
2414
|
}
|
|
2840
2415
|
};
|
|
2841
|
-
const handleContextDragOver = (e) => {
|
|
2842
|
-
e.preventDefault();
|
|
2843
|
-
e.dataTransfer.dropEffect = "copy";
|
|
2844
|
-
if (!isContextDragOver)
|
|
2845
|
-
setIsContextDragOver(true);
|
|
2846
|
-
};
|
|
2847
|
-
const handleContextDragLeave = () => {
|
|
2848
|
-
setIsContextDragOver(false);
|
|
2849
|
-
};
|
|
2850
|
-
const handleContextDrop = async (e) => {
|
|
2851
|
-
e.preventDefault();
|
|
2852
|
-
setIsContextDragOver(false);
|
|
2853
|
-
try {
|
|
2854
|
-
const dragObj = editContext?.dragObject;
|
|
2855
|
-
if (dragObj?.type === "component") {
|
|
2856
|
-
const idsFromComponents = (dragObj.components || [])
|
|
2857
|
-
.map((c) => c.id)
|
|
2858
|
-
.filter((x) => !!x);
|
|
2859
|
-
const idFromData = e.dataTransfer.getData("componentId");
|
|
2860
|
-
const allIds = Array.from(new Set([...idsFromComponents, idFromData].filter((x) => !!x)));
|
|
2861
|
-
if (allIds.length) {
|
|
2862
|
-
await addComponentIdsToContext(allIds);
|
|
2863
|
-
if (isContextCollapsed)
|
|
2864
|
-
setIsContextCollapsed(false);
|
|
2865
|
-
return;
|
|
2866
|
-
}
|
|
2867
|
-
}
|
|
2868
|
-
if (dragObj?.type === "items" && dragObj.items?.length) {
|
|
2869
|
-
await addPagesToContextFromItems(dragObj.items);
|
|
2870
|
-
if (isContextCollapsed)
|
|
2871
|
-
setIsContextCollapsed(false);
|
|
2872
|
-
return;
|
|
2873
|
-
}
|
|
2874
|
-
// Fallback: try to parse JSON payload for items/components if provided
|
|
2875
|
-
try {
|
|
2876
|
-
const textData = e.dataTransfer.getData("text/plain");
|
|
2877
|
-
if (textData) {
|
|
2878
|
-
const parsed = JSON.parse(textData);
|
|
2879
|
-
if (Array.isArray(parsed)) {
|
|
2880
|
-
await addPagesToContextFromItems(parsed);
|
|
2881
|
-
if (isContextCollapsed)
|
|
2882
|
-
setIsContextCollapsed(false);
|
|
2883
|
-
return;
|
|
2884
|
-
}
|
|
2885
|
-
else if (parsed?.id) {
|
|
2886
|
-
await addPagesToContextFromItems([parsed]);
|
|
2887
|
-
if (isContextCollapsed)
|
|
2888
|
-
setIsContextCollapsed(false);
|
|
2889
|
-
return;
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
}
|
|
2893
|
-
catch { }
|
|
2894
|
-
}
|
|
2895
|
-
catch (err) {
|
|
2896
|
-
console.error("Context drop failed", err);
|
|
2897
|
-
}
|
|
2898
|
-
};
|
|
2899
|
-
const addCommentToContext = async (comment) => {
|
|
2900
|
-
if (!agent?.id)
|
|
2901
|
-
return;
|
|
2902
|
-
const selectedText = (() => {
|
|
2903
|
-
if (typeof comment.rangeStart === "number" &&
|
|
2904
|
-
typeof comment.rangeEnd === "number" &&
|
|
2905
|
-
comment.fieldValue) {
|
|
2906
|
-
try {
|
|
2907
|
-
return comment.fieldValue.substring(Math.max(0, comment.rangeStart), Math.max(comment.rangeStart, comment.rangeEnd));
|
|
2908
|
-
}
|
|
2909
|
-
catch {
|
|
2910
|
-
return undefined;
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
return undefined;
|
|
2914
|
-
})();
|
|
2915
|
-
const current = agentMetadata || {};
|
|
2916
|
-
// Exclude top-level context to avoid duplicate keys when spreading
|
|
2917
|
-
const currentWithoutContext = { ...current };
|
|
2918
|
-
delete currentWithoutContext.context;
|
|
2919
|
-
const next = {
|
|
2920
|
-
...currentWithoutContext,
|
|
2921
|
-
additionalData: {
|
|
2922
|
-
...(current.additionalData || {}),
|
|
2923
|
-
context: {
|
|
2924
|
-
...((current.additionalData &&
|
|
2925
|
-
current.additionalData.context) ||
|
|
2926
|
-
{}),
|
|
2927
|
-
comment: {
|
|
2928
|
-
id: comment.id,
|
|
2929
|
-
text: comment.text,
|
|
2930
|
-
fieldName: comment.fieldName,
|
|
2931
|
-
itemName: comment.itemName,
|
|
2932
|
-
author: comment.author,
|
|
2933
|
-
selectedText,
|
|
2934
|
-
rangeStart: comment.rangeStart,
|
|
2935
|
-
rangeEnd: comment.rangeEnd,
|
|
2936
|
-
},
|
|
2937
|
-
},
|
|
2938
|
-
},
|
|
2939
|
-
};
|
|
2940
|
-
try {
|
|
2941
|
-
if (agent.status === "new") {
|
|
2942
|
-
setAgentMetadata(next);
|
|
2943
|
-
return;
|
|
2944
|
-
}
|
|
2945
|
-
await updateAgentContext(agent.id, next);
|
|
2946
|
-
setAgentMetadata(next);
|
|
2947
|
-
setAgent((prev) => prev ? { ...prev, metadata: JSON.stringify(next) } : prev);
|
|
2948
|
-
}
|
|
2949
|
-
catch (e) {
|
|
2950
|
-
console.error("Failed to update agent metadata (add comment)", e);
|
|
2951
|
-
}
|
|
2952
|
-
};
|
|
2953
2416
|
// Resolve display names when metadata or editor state changes
|
|
2954
2417
|
useEffect(() => {
|
|
2955
2418
|
const metaCtx = agentMetadata?.additionalData?.context;
|
|
@@ -3064,12 +2527,23 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
3064
2527
|
console.error("Failed to stop agent execution", e);
|
|
3065
2528
|
}
|
|
3066
2529
|
}, [agentStub?.id, resetDotsTimer]);
|
|
3067
|
-
//
|
|
3068
|
-
//
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
2530
|
+
// Determine effective cost limit from agent, profile, or metadata so the cost display
|
|
2531
|
+
// is visible immediately even before any messages or server-side persistence.
|
|
2532
|
+
const effectiveCostLimit = useMemo(() => {
|
|
2533
|
+
try {
|
|
2534
|
+
const candidates = [
|
|
2535
|
+
agent?.costLimit,
|
|
2536
|
+
activeProfile?.costLimit,
|
|
2537
|
+
];
|
|
2538
|
+
for (const c of candidates) {
|
|
2539
|
+
const n = c != null ? Number(c) : 0;
|
|
2540
|
+
if (Number.isFinite(n) && n > 0)
|
|
2541
|
+
return n;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
catch { }
|
|
2545
|
+
return undefined;
|
|
2546
|
+
}, [agent?.costLimit, activeProfile?.costLimit]);
|
|
3073
2547
|
if (isLoading) {
|
|
3074
2548
|
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "flex items-center gap-2 text-xs text-gray-500", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin", strokeWidth: 1 }), "Loading agent..."] }) }));
|
|
3075
2549
|
}
|
|
@@ -3295,7 +2769,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, isActive =
|
|
|
3295
2769
|
cacheWriteCost: liveTotals.cacheWriteCost ?? 0,
|
|
3296
2770
|
totalCost: liveTotals.totalCost,
|
|
3297
2771
|
}
|
|
3298
|
-
: totalTokens, costLimit:
|
|
2772
|
+
: totalTokens, costLimit: effectiveCostLimit }), (() => {
|
|
3299
2773
|
try {
|
|
3300
2774
|
const s = window.__agentContextWindowStatus;
|
|
3301
2775
|
if (!s || !s.contextWindowTokens)
|