@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.
Files changed (34) hide show
  1. package/dist/editor/QuickItemSwitcher.d.ts +3 -3
  2. package/dist/editor/QuickItemSwitcher.js +25 -7
  3. package/dist/editor/QuickItemSwitcher.js.map +1 -1
  4. package/dist/editor/ai/AgentCostDisplay.js +7 -11
  5. package/dist/editor/ai/AgentCostDisplay.js.map +1 -1
  6. package/dist/editor/ai/AgentTerminal.js +192 -718
  7. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  8. package/dist/editor/ai/Agents.js +61 -38
  9. package/dist/editor/ai/Agents.js.map +1 -1
  10. package/dist/editor/client/EditorShell.js +117 -17
  11. package/dist/editor/client/EditorShell.js.map +1 -1
  12. package/dist/editor/client/hooks/useSocketMessageHandler.js +0 -12
  13. package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
  14. package/dist/editor/services/agentService.d.ts +16 -2
  15. package/dist/editor/services/agentService.js +22 -8
  16. package/dist/editor/services/agentService.js.map +1 -1
  17. package/dist/editor/services/aiService.d.ts +1 -0
  18. package/dist/editor/services/aiService.js +36 -5
  19. package/dist/editor/services/aiService.js.map +1 -1
  20. package/dist/revision.d.ts +2 -2
  21. package/dist/revision.js +2 -2
  22. package/dist/styles.css +6 -3
  23. package/dist/types.d.ts +13 -0
  24. package/package.json +1 -1
  25. package/src/editor/QuickItemSwitcher.tsx +60 -33
  26. package/src/editor/ai/AgentCostDisplay.tsx +59 -60
  27. package/src/editor/ai/AgentTerminal.tsx +213 -741
  28. package/src/editor/ai/Agents.tsx +54 -38
  29. package/src/editor/client/EditorShell.tsx +142 -21
  30. package/src/editor/client/hooks/useSocketMessageHandler.ts +0 -15
  31. package/src/editor/services/agentService.ts +39 -10
  32. package/src/editor/services/aiService.ts +43 -6
  33. package/src/revision.ts +2 -2
  34. 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 content chunk if present
818
- const data = message.data;
819
- if (data &&
820
- (data.totalCost !== undefined || data.totalTokens !== undefined)) {
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(data.totalInputTokens) || 0,
823
- output: Number(data.totalOutputTokens) || 0,
824
- cached: Number(data.totalCachedTokens) || 0,
825
- cacheWrite: Number(data.totalCacheWriteTokens) || 0,
826
- inputCost: Number(data.totalInputTokenCost) || 0,
827
- outputCost: Number(data.totalOutputTokenCost) || 0,
828
- cachedCost: Number(data.totalCachedTokenCost) || 0,
829
- cacheWriteCost: Number(data.totalCacheWriteTokenCost) || 0,
830
- totalCost: Number(data.totalCost) || 0,
831
- currency: data.currency || "USD",
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 data = message.data;
985
- if (data &&
986
- (data.totalCost !== undefined || data.totalTokens !== undefined)) {
1015
+ const cost = message.cost;
1016
+ if (cost &&
1017
+ (cost.total !== undefined || cost.tokens?.total !== undefined)) {
987
1018
  const nextTotals = {
988
- input: Number(data.totalInputTokens) || 0,
989
- output: Number(data.totalOutputTokens) || 0,
990
- cached: Number(data.totalCachedTokens) || 0,
991
- cacheWrite: Number(data.totalCacheWriteTokens) || 0,
992
- inputCost: Number(data.totalInputTokenCost) || 0,
993
- outputCost: Number(data.totalOutputTokenCost) || 0,
994
- cachedCost: Number(data.totalCachedTokenCost) || 0,
995
- cacheWriteCost: Number(data.totalCacheWriteTokenCost) || 0,
996
- totalCost: Number(data.totalCost) || 0,
997
- currency: data.currency || "USD",
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
- // Handle agent:run:status
1589
+ // Unified: agent:run:status (state only)
1995
1590
  if (messageType === "agent:run:status") {
1996
- const { seq, kind, data: statusData } = message.payload;
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 status kind
1599
+ // Route based on statusData.state
2005
1600
  try {
2006
- if (kind === "streamOpen") {
1601
+ if (statusData?.state === "streamOpen") {
2007
1602
  setIsConnecting(false);
2008
1603
  return;
2009
1604
  }
2010
- if (kind === "tokenUsage") {
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 (kind === "toolApprovalsRequired") {
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 (kind === "cost_limit_reached") {
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 (kind === "contextWindow") {
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 (kind === "contextChanged") {
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
- // Handle agent:run:complete
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
- // Handle agent:run:error
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
- // Waiting state is explicitly managed by stream events:
3068
- // - Cleared when first content arrives
3069
- // - Cleared on stream completion
3070
- // - Cleared on errors
3071
- // - Cleared when stopped by user
3072
- // No need for an automatic cleanup effect that can fire prematurely
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: agent?.costLimit }), (() => {
2772
+ : totalTokens, costLimit: effectiveCostLimit }), (() => {
3299
2773
  try {
3300
2774
  const s = window.__agentContextWindowStatus;
3301
2775
  if (!s || !s.contextWindowTokens)