@alpaca-editor/core 1.0.4132 → 1.0.4134

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.
@@ -88,10 +88,116 @@ interface TodoItem {
88
88
  text: string;
89
89
  done?: boolean;
90
90
  note?: string;
91
- messageId: string;
91
+ messageId?: string;
92
92
  sourceTitle?: string;
93
93
  }
94
94
 
95
+ // Helper to extract todos from potentially incomplete JSON during streaming
96
+ const extractPartialTodos = (jsonText: string): any[] => {
97
+ // First try to parse complete JSON
98
+ try {
99
+ const parsed = JSON.parse(jsonText);
100
+ return Array.isArray(parsed) ? parsed : parsed?.items || [];
101
+ } catch (e) {
102
+ // If JSON is incomplete, try to extract whatever todo items we can find
103
+ const items: any[] = [];
104
+
105
+ // Look for individual todo objects in the partial JSON
106
+ // Match patterns like: { "text": "...", "done": false, "note": "..." }
107
+ // Handle various field orderings (text can be anywhere in the object)
108
+ const textPattern = /"text"\s*:\s*"([^"]+)"/g;
109
+ const textMatches: Array<{ text: string; startIdx: number }> = [];
110
+ let textMatch;
111
+ while ((textMatch = textPattern.exec(jsonText)) !== null) {
112
+ if (textMatch[1]) {
113
+ textMatches.push({
114
+ text: textMatch[1],
115
+ startIdx: textMatch.index,
116
+ });
117
+ }
118
+ }
119
+
120
+ // For each text field found, try to find the enclosing object
121
+ for (const { text, startIdx } of textMatches) {
122
+ // Find the opening brace before this text field
123
+ let openBrace = -1;
124
+ for (let i = startIdx - 1; i >= 0; i--) {
125
+ if (jsonText[i] === "{") {
126
+ openBrace = i;
127
+ break;
128
+ }
129
+ if (jsonText[i] === "}") break; // Hit another object's end
130
+ }
131
+
132
+ if (openBrace === -1) continue;
133
+
134
+ // Find the closing brace after this text field
135
+ let closeBrace = -1;
136
+ let depth = 0;
137
+ for (let i = openBrace; i < jsonText.length; i++) {
138
+ if (jsonText[i] === "{") depth++;
139
+ if (jsonText[i] === "}") {
140
+ depth--;
141
+ if (depth === 0) {
142
+ closeBrace = i;
143
+ break;
144
+ }
145
+ }
146
+ }
147
+
148
+ // Extract the object and try to parse it
149
+ const objStr =
150
+ closeBrace !== -1
151
+ ? jsonText.substring(openBrace, closeBrace + 1)
152
+ : jsonText.substring(openBrace) + "}"; // Try to close incomplete object
153
+
154
+ try {
155
+ const obj = JSON.parse(objStr);
156
+ if (obj.text) {
157
+ items.push({
158
+ text: obj.text,
159
+ done: obj.done === true,
160
+ note: obj.note || undefined,
161
+ });
162
+ }
163
+ } catch (e) {
164
+ // Skip malformed objects
165
+ }
166
+ }
167
+
168
+ // Also try to extract from partial objects at the end
169
+ // Look for the last opening brace and try to parse up to where we have valid content
170
+ const lines = jsonText.split("\n");
171
+ for (let i = lines.length - 1; i >= 0; i--) {
172
+ const partialJson = lines.slice(0, i + 1).join("\n");
173
+ // Try to close any open braces/brackets
174
+ let testJson = partialJson;
175
+ const openBraces = (testJson.match(/\{/g) || []).length;
176
+ const closeBraces = (testJson.match(/\}/g) || []).length;
177
+ const openBrackets = (testJson.match(/\[/g) || []).length;
178
+ const closeBrackets = (testJson.match(/\]/g) || []).length;
179
+
180
+ // Add missing closing characters
181
+ testJson += "]".repeat(openBrackets - closeBrackets);
182
+ testJson += "}".repeat(openBraces - closeBraces);
183
+
184
+ try {
185
+ const parsed = JSON.parse(testJson);
186
+ const partialItems = Array.isArray(parsed)
187
+ ? parsed
188
+ : parsed?.items || [];
189
+ if (partialItems.length > items.length) {
190
+ return partialItems;
191
+ }
192
+ } catch (e) {
193
+ continue;
194
+ }
195
+ }
196
+
197
+ return items;
198
+ }
199
+ };
200
+
95
201
  const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
96
202
  const todos: TodoItem[] = [];
97
203
  const fencedTodoToken = "```todo_list";
@@ -126,12 +232,20 @@ const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
126
232
 
127
233
  try {
128
234
  let jsonText = "";
235
+ let isComplete = true;
236
+
129
237
  if (isFenced) {
130
238
  const afterToken = todoStart + fencedTodoToken.length;
131
239
  const closePos = content.indexOf("```", afterToken);
132
- if (closePos === -1) break;
133
- jsonText = content.slice(afterToken, closePos).trim();
134
- cursor = closePos + 3;
240
+ if (closePos === -1) {
241
+ // Incomplete fenced block - extract what we have so far
242
+ jsonText = content.slice(afterToken).trim();
243
+ isComplete = false;
244
+ cursor = content.length; // Process till end
245
+ } else {
246
+ jsonText = content.slice(afterToken, closePos).trim();
247
+ cursor = closePos + 3;
248
+ }
135
249
  } else {
136
250
  const afterToken = todoStart + plainTodoToken.length;
137
251
  const braceStart = content.indexOf("{", afterToken);
@@ -149,24 +263,56 @@ const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
149
263
  }
150
264
  }
151
265
  }
152
- if (braceEnd === -1) break;
153
- jsonText = content.slice(braceStart, braceEnd + 1).trim();
154
- cursor = braceEnd + 1;
266
+
267
+ if (braceEnd === -1) {
268
+ // Incomplete JSON - extract what we have
269
+ jsonText = content.slice(braceStart).trim();
270
+ isComplete = false;
271
+ cursor = content.length;
272
+ } else {
273
+ jsonText = content.slice(braceStart, braceEnd + 1).trim();
274
+ cursor = braceEnd + 1;
275
+ }
155
276
  }
156
277
 
157
- const parsed = JSON.parse(jsonText);
158
- const todoItems = Array.isArray(parsed) ? parsed : parsed?.items || [];
159
- const title = Array.isArray(parsed) ? undefined : parsed?.title;
278
+ // Use the partial extraction helper for incomplete JSON
279
+ const todoItems = isComplete
280
+ ? (() => {
281
+ try {
282
+ const parsed = JSON.parse(jsonText);
283
+ return Array.isArray(parsed) ? parsed : parsed?.items || [];
284
+ } catch (e) {
285
+ return [];
286
+ }
287
+ })()
288
+ : extractPartialTodos(jsonText);
289
+
290
+ const title = (() => {
291
+ try {
292
+ const parsed = JSON.parse(jsonText);
293
+ return Array.isArray(parsed) ? undefined : parsed?.title;
294
+ } catch (e) {
295
+ return undefined;
296
+ }
297
+ })();
160
298
 
161
299
  todoItems.forEach((item: any) => {
162
300
  if (!item) return;
163
301
  const text =
164
- item.text || item.label || String(item.task || item.title || "");
302
+ item.text ||
303
+ item.content ||
304
+ item.label ||
305
+ String(item.task || item.title || "");
165
306
  if (!text) return;
166
307
  todos.push({
167
308
  id: item.id,
168
309
  text,
169
- done: !!(item.done ?? item.completed ?? item.checked),
310
+ done: !!(
311
+ item.done ??
312
+ item.completed ??
313
+ item.checked ??
314
+ item.status === "completed"
315
+ ),
170
316
  note: item.note || item.description,
171
317
  messageId: message.id,
172
318
  sourceTitle: title,
@@ -183,22 +329,101 @@ const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
183
329
  };
184
330
 
185
331
  // TodoListPanel component
186
- const TodoListPanel = ({ messages }: { messages: AgentChatMessage[] }) => {
332
+ const TodoListPanel = ({
333
+ messages,
334
+ agentMetadata,
335
+ }: {
336
+ messages: AgentChatMessage[];
337
+ agentMetadata: AgentMetadata | null;
338
+ }) => {
187
339
  const [isExpanded, setIsExpanded] = useState(true);
188
- const todos = useMemo(() => extractTodosFromMessages(messages), [messages]);
189
340
 
190
- // Check if there's an active streaming message with todo content
341
+ const todos = useMemo(() => {
342
+ // First try to get todos from agent metadata (real-time updates)
343
+ const metadataTodos = (() => {
344
+ try {
345
+ const context = (agentMetadata as any)?.additionalData?.context;
346
+ const todoList = context?.todoList;
347
+ if (todoList?.items && Array.isArray(todoList.items)) {
348
+ return todoList.items
349
+ .map((item: any, idx: number) => ({
350
+ id: item.id || `metadata-${idx}`,
351
+ text:
352
+ item.text ||
353
+ item.label ||
354
+ String(item.task || item.title || ""),
355
+ done: !!(item.done ?? item.completed ?? item.checked),
356
+ note: item.note || item.description,
357
+ messageId: undefined,
358
+ sourceTitle: todoList.title,
359
+ }))
360
+ .filter((item: any) => item.text);
361
+ }
362
+ } catch (e) {
363
+ // Fallback to extracting from messages
364
+ }
365
+ return null;
366
+ })();
367
+
368
+ // If we have metadata todos, use them; otherwise extract from messages
369
+ if (metadataTodos && metadataTodos.length > 0) {
370
+ return metadataTodos;
371
+ }
372
+
373
+ return extractTodosFromMessages(messages);
374
+ }, [messages, agentMetadata]);
375
+
376
+ // Check if there's an active streaming message with incomplete todo content
191
377
  const isUpdating = useMemo(() => {
192
378
  return messages.some((msg) => {
193
379
  if (msg.role !== "assistant" || msg.isCompleted) return false;
194
380
  const content = msg.content || "";
195
- return content.includes("```todo_list") || content.includes("todo_list");
381
+
382
+ // Check for incomplete fenced todo blocks
383
+ const fencedStart = content.indexOf("```todo_list");
384
+ if (fencedStart !== -1) {
385
+ const afterStart = fencedStart + "```todo_list".length;
386
+ const closePos = content.indexOf("```", afterStart);
387
+ if (closePos === -1) {
388
+ // Incomplete fenced block
389
+ return true;
390
+ }
391
+ }
392
+
393
+ // Check for incomplete plain todo blocks
394
+ const plainStart = content.indexOf("todo_list");
395
+ if (plainStart !== -1 && plainStart !== fencedStart) {
396
+ const before = plainStart > 0 ? content[plainStart - 1] : "\n";
397
+ if (before === "\n" || before === "\r" || plainStart === 0) {
398
+ const braceStart = content.indexOf("{", plainStart);
399
+ if (braceStart !== -1) {
400
+ let depth = 0;
401
+ let braceEnd = -1;
402
+ for (let i = braceStart; i < content.length; i++) {
403
+ if (content[i] === "{") depth++;
404
+ if (content[i] === "}") {
405
+ depth--;
406
+ if (depth === 0) {
407
+ braceEnd = i;
408
+ break;
409
+ }
410
+ }
411
+ }
412
+ if (braceEnd === -1) {
413
+ // Incomplete plain block
414
+ return true;
415
+ }
416
+ }
417
+ }
418
+ }
419
+
420
+ return false;
196
421
  });
197
422
  }, [messages]);
198
423
 
199
424
  if (todos.length === 0 && !isUpdating) return null;
200
425
 
201
- const completedCount = todos.filter((t) => t.done).length;
426
+ const completedCount = todos.filter((t: TodoItem) => t.done).length;
202
427
  const totalCount = todos.length;
203
428
 
204
429
  return (
@@ -229,66 +454,69 @@ const TodoListPanel = ({ messages }: { messages: AgentChatMessage[] }) => {
229
454
  </button>
230
455
  {isExpanded && (
231
456
  <div className="max-h-64 overflow-y-auto px-4 pb-3">
232
- {isUpdating && todos.length === 0 ? (
233
- <div className="flex items-center justify-center gap-2 py-4 text-xs text-gray-500">
234
- <Loader2 className="h-4 w-4 animate-spin" strokeWidth={1} />
235
- <span>Loading todo list...</span>
236
- </div>
237
- ) : (
238
- <>
239
- <div className="space-y-1.5">
240
- {todos.map((todo, idx) => (
241
- <div
242
- key={todo.id || `${todo.messageId}-${idx}`}
243
- className="flex items-start gap-2 rounded bg-white p-2 text-xs"
244
- >
245
- <div className="flex-shrink-0 pt-0.5">
246
- {todo.done ? (
247
- <div className="flex h-4 w-4 items-center justify-center rounded border-2 border-green-500 bg-green-500">
248
- <svg
249
- className="h-3 w-3 text-white"
250
- fill="none"
251
- strokeWidth={2}
252
- stroke="currentColor"
253
- viewBox="0 0 24 24"
254
- >
255
- <path
256
- strokeLinecap="round"
257
- strokeLinejoin="round"
258
- d="M5 13l4 4L19 7"
259
- />
260
- </svg>
261
- </div>
262
- ) : (
263
- <div className="h-4 w-4 rounded border-2 border-gray-300" />
264
- )}
265
- </div>
266
- <div className="min-w-0 flex-1">
267
- <div
268
- className={`${
269
- todo.done
270
- ? "text-gray-500 line-through"
271
- : "text-gray-900"
272
- }`}
273
- >
274
- {todo.text}
457
+ {todos.length > 0 && (
458
+ <div className="space-y-1.5">
459
+ {todos.map((todo: TodoItem, idx: number) => (
460
+ <div
461
+ key={todo.id || `${todo.messageId}-${idx}`}
462
+ className="flex items-start gap-2 rounded bg-white p-2 text-xs"
463
+ >
464
+ <div className="flex-shrink-0 pt-0.5">
465
+ {todo.done ? (
466
+ <div className="flex h-4 w-4 items-center justify-center rounded border-2 border-green-500 bg-green-500">
467
+ <svg
468
+ className="h-3 w-3 text-white"
469
+ fill="none"
470
+ strokeWidth={2}
471
+ stroke="currentColor"
472
+ viewBox="0 0 24 24"
473
+ >
474
+ <path
475
+ strokeLinecap="round"
476
+ strokeLinejoin="round"
477
+ d="M5 13l4 4L19 7"
478
+ />
479
+ </svg>
275
480
  </div>
276
- {todo.note && (
277
- <div className="mt-0.5 text-xs text-gray-500">
278
- {todo.note}
279
- </div>
280
- )}
481
+ ) : (
482
+ <div className="h-4 w-4 rounded border-2 border-gray-300" />
483
+ )}
484
+ </div>
485
+ <div className="min-w-0 flex-1">
486
+ <div
487
+ className={`${
488
+ todo.done
489
+ ? "text-gray-500 line-through"
490
+ : "text-gray-900"
491
+ }`}
492
+ >
493
+ {todo.text}
281
494
  </div>
495
+ {todo.note && (
496
+ <div className="mt-0.5 text-xs text-gray-500">
497
+ {todo.note}
498
+ </div>
499
+ )}
282
500
  </div>
283
- ))}
284
- </div>
285
- {isUpdating && todos.length > 0 && (
286
- <div className="mt-2 flex items-center gap-2 rounded bg-blue-50 px-3 py-2 text-xs text-blue-700">
287
- <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1} />
288
- <span>Updating todo list...</span>
289
501
  </div>
290
- )}
291
- </>
502
+ ))}
503
+ </div>
504
+ )}
505
+ {isUpdating && (
506
+ <div
507
+ className={`flex items-center gap-2 rounded px-3 py-2 text-xs ${
508
+ todos.length > 0
509
+ ? "mt-2 bg-blue-50 text-blue-700"
510
+ : "justify-center bg-white text-gray-500"
511
+ }`}
512
+ >
513
+ <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1} />
514
+ <span>
515
+ {todos.length > 0
516
+ ? "Updating todo list..."
517
+ : "Loading todo list..."}
518
+ </span>
519
+ </div>
292
520
  )}
293
521
  </div>
294
522
  )}
@@ -423,7 +651,7 @@ export function AgentTerminal({
423
651
  const [messages, setMessages] = useState<AgentChatMessage[]>([]);
424
652
  const [prompt, setPrompt] = useState("");
425
653
  const [inputPlaceholder, setInputPlaceholder] = useState<string>(
426
- "Type your message... (Enter to send, Ctrl+Enter for new line)",
654
+ "Type your message... (Enter to send, Shift+Enter or Ctrl+Enter for new line)",
427
655
  );
428
656
  const [isLoading, setIsLoading] = useState(false);
429
657
  const [isConnecting, setIsConnecting] = useState(false);
@@ -552,6 +780,18 @@ export function AgentTerminal({
552
780
  initialCostLimit: number;
553
781
  } | null>(null);
554
782
 
783
+ // Live running totals from backend status updates (tokenUsage)
784
+ const [liveTotals, setLiveTotals] = useState<{
785
+ input: number;
786
+ output: number;
787
+ cached: number;
788
+ inputCost: number;
789
+ outputCost: number;
790
+ cachedCost: number;
791
+ totalCost: number;
792
+ currency?: string;
793
+ } | null>(null);
794
+
555
795
  // Flag to track when we should create a new message
556
796
  const shouldCreateNewMessage = useRef(false);
557
797
 
@@ -724,10 +964,7 @@ export function AgentTerminal({
724
964
  });
725
965
  throw new Error("No agent available");
726
966
  }
727
- console.log(
728
- "✅ Creating new stream message with agent:",
729
- currentAgent.id,
730
- );
967
+ // Reduced: avoid verbose logging during streaming
731
968
  return {
732
969
  id: messageId,
733
970
  agentId: currentAgent.id,
@@ -873,10 +1110,6 @@ export function AgentTerminal({
873
1110
  (msg) => msg.id === toolCallMessageId,
874
1111
  );
875
1112
  if (finalCheck) {
876
- console.log(
877
- "#!# ⚠️ Tool call message already exists in state, skipping:",
878
- toolCallMessageId,
879
- );
880
1113
  return prev;
881
1114
  }
882
1115
 
@@ -886,11 +1119,6 @@ export function AgentTerminal({
886
1119
  return updated;
887
1120
  });
888
1121
  }
889
- } else {
890
- console.log(
891
- "📋 Adding tool call to existing message:",
892
- toolCallMessageId,
893
- );
894
1122
  }
895
1123
  }
896
1124
 
@@ -971,12 +1199,9 @@ export function AgentTerminal({
971
1199
 
972
1200
  // Update tool result directly in the messages array
973
1201
  if (!resultMessageId) {
974
- console.warn("⚠️ No messageId available for tool result");
975
1202
  return;
976
1203
  }
977
1204
 
978
- console.log("📋 Updating tool result in message:", resultMessageId);
979
-
980
1205
  // Update the message with tool result
981
1206
  setMessages((prev) => {
982
1207
  const updated = prev.map((msg) => {
@@ -1049,10 +1274,7 @@ export function AgentTerminal({
1049
1274
  updatedMessage.toolCalls = [...updatedMessage.toolCalls, toolCall];
1050
1275
  }
1051
1276
 
1052
- console.log(
1053
- "🔧 Updated tool calls count:",
1054
- updatedMessage.toolCalls.length,
1055
- );
1277
+ // Updated tool calls count
1056
1278
 
1057
1279
  return updatedMessage;
1058
1280
  });
@@ -1094,23 +1316,16 @@ export function AgentTerminal({
1094
1316
  try {
1095
1317
  setIsConnecting(true);
1096
1318
 
1097
- console.log("🔌 connectToStream: Starting stream connection");
1319
+ // Reduced: minimal logging
1098
1320
 
1099
1321
  // Expose agent id globally for approval actions
1100
1322
  (window as any).currentAgentId = currentAgent.id;
1101
- console.log("🔗 Setting currentAgentId:", currentAgent.id);
1323
+ // Expose id for approval actions
1102
1324
 
1103
- console.log(
1104
- "🌐 Attempting to connect to agent stream for:",
1105
- currentAgent.id,
1106
- );
1325
+ // Connecting to agent stream
1107
1326
  await connectToAgentStream(
1108
1327
  currentAgent.id,
1109
1328
  (message: AgentStreamMessage) => {
1110
- console.log("📨 Received stream message:", {
1111
- type: message.type,
1112
- data: message.data,
1113
- });
1114
1329
  switch (message.type) {
1115
1330
  case "contentChunk":
1116
1331
  handleContentChunk(message, currentAgent);
@@ -1127,18 +1342,31 @@ export function AgentTerminal({
1127
1342
  case "statusUpdate":
1128
1343
  try {
1129
1344
  const kind = (message as any)?.data?.kind;
1130
- console.log("📡 Received status update:", {
1131
- kind,
1132
- data: (message as any).data,
1133
- });
1345
+ // Live token usage totals update from backend
1346
+ if (kind === "tokenUsage") {
1347
+ const totals = (message as any)?.data?.totals;
1348
+ if (totals) {
1349
+ setLiveTotals({
1350
+ input: Number(totals.totalInputTokens) || 0,
1351
+ output: Number(totals.totalOutputTokens) || 0,
1352
+ cached: Number(totals.totalCachedInputTokens) || 0,
1353
+ inputCost: Number(totals.totalInputTokenCost) || 0,
1354
+ outputCost: Number(totals.totalOutputTokenCost) || 0,
1355
+ cachedCost:
1356
+ Number(totals.totalCachedInputTokenCost) || 0,
1357
+ totalCost: Number(totals.totalCost) || 0,
1358
+ currency: totals.currency,
1359
+ });
1360
+ // Force a re-render to update cost display immediately
1361
+ setMessages((prev) => [...prev]);
1362
+ }
1363
+ break;
1364
+ }
1134
1365
  if (kind === "toolApprovalsRequired") {
1135
1366
  const data = (message as any).data || {};
1136
1367
  const msgId: string | undefined = data.messageId;
1137
1368
  const ids: string[] = data.toolCallIds || [];
1138
- console.log(
1139
- "⏸️ Approvals required; pausing stream until approval:",
1140
- { msgId, ids },
1141
- );
1369
+ // Pause stream until approval
1142
1370
 
1143
1371
  // Annotate tool calls with a temporary pending marker so UI can reflect paused state on reload
1144
1372
  if (msgId && Array.isArray(ids) && ids.length > 0) {
@@ -1243,21 +1471,9 @@ export function AgentTerminal({
1243
1471
  const data = (message as any).data || {};
1244
1472
  const toolCallId: string | undefined = data.toolCallId;
1245
1473
  const msgId: string | undefined = data.messageId;
1246
- console.log("🔧 Processing tool approval:", {
1247
- kind,
1248
- toolCallId,
1249
- msgId,
1250
- data,
1251
- });
1474
+ // Processing tool approval
1252
1475
  if (toolCallId && msgId) {
1253
1476
  setMessages((prev) => {
1254
- console.log("🔍 Looking for message:", {
1255
- targetMsgId: msgId,
1256
- availableMessages: prev.map((m) => ({
1257
- id: m.id,
1258
- toolCallsCount: m.toolCalls?.length || 0,
1259
- })),
1260
- });
1261
1477
  const updated = prev.map((m) => {
1262
1478
  if (m.id !== msgId) return m;
1263
1479
  const existingToolCalls = m.toolCalls || [];
@@ -1270,11 +1486,7 @@ export function AgentTerminal({
1270
1486
  : " (rejected)";
1271
1487
  const newFunctionName =
1272
1488
  (tc.functionName || "") + suffix;
1273
- console.log("🏷️ Updating function name:", {
1274
- toolCallId,
1275
- oldName: tc.functionName,
1276
- newName: newFunctionName,
1277
- });
1489
+ // Update function name with approval suffix
1278
1490
  return {
1279
1491
  ...tc,
1280
1492
  functionName: newFunctionName,
@@ -1294,12 +1506,6 @@ export function AgentTerminal({
1294
1506
 
1295
1507
  case "completed":
1296
1508
  const completedMessageId = message.data?.messageId;
1297
- console.log(
1298
- "💾 Stream completed for message:",
1299
- completedMessageId,
1300
- "messages count:",
1301
- messages.length,
1302
- );
1303
1509
 
1304
1510
  // If the completed event carries full messages, merge them into state
1305
1511
  try {
@@ -1367,6 +1573,25 @@ export function AgentTerminal({
1367
1573
  (updatedMessage.inputTokens || 0) +
1368
1574
  (updatedMessage.outputTokens || 0);
1369
1575
 
1576
+ // Update cost data if provided in the completed event
1577
+ if (data.inputTokenCost !== undefined) {
1578
+ updatedMessage.inputTokenCost = data.inputTokenCost;
1579
+ }
1580
+ if (data.outputTokenCost !== undefined) {
1581
+ updatedMessage.outputTokenCost =
1582
+ data.outputTokenCost;
1583
+ }
1584
+ if (
1585
+ data.cachedInputTokenCost !== undefined ||
1586
+ data.cachedTokenCost !== undefined
1587
+ ) {
1588
+ updatedMessage.cachedInputTokenCost =
1589
+ data.cachedInputTokenCost ?? data.cachedTokenCost;
1590
+ }
1591
+ if (data.totalCost !== undefined) {
1592
+ updatedMessage.totalCost = data.totalCost;
1593
+ }
1594
+
1370
1595
  // Handle content that might only be sent in the completed event
1371
1596
  if (data.deltaContent && data.deltaContent.trim()) {
1372
1597
  if (!data.isIncremental) {
@@ -1462,7 +1687,6 @@ export function AgentTerminal({
1462
1687
  setError("Failed to connect to agent stream");
1463
1688
  }
1464
1689
  } finally {
1465
- console.log("🔌 Stream connection finished, cleaning up");
1466
1690
  setIsConnecting(false);
1467
1691
  // Guard: clear waiting state if connection finished without content
1468
1692
  setIsWaitingForResponse(false);
@@ -1479,7 +1703,6 @@ export function AgentTerminal({
1479
1703
 
1480
1704
  // Check if we're already connected
1481
1705
  if (abortControllerRef.current) {
1482
- console.log("🔄 Already connected to stream, skipping reconnect");
1483
1706
  return;
1484
1707
  }
1485
1708
 
@@ -1491,10 +1714,7 @@ export function AgentTerminal({
1491
1714
  );
1492
1715
 
1493
1716
  if (!hasPending) {
1494
- console.log("🔄 No pending approvals; reconnecting stream");
1495
1717
  await connectToStream(currentAgent);
1496
- } else {
1497
- console.log("⏸️ Still have pending approvals, not reconnecting yet");
1498
1718
  }
1499
1719
  } catch (err) {
1500
1720
  console.error("❌ Error attempting reconnect:", err);
@@ -1511,11 +1731,7 @@ export function AgentTerminal({
1511
1731
  const approved: boolean = !!detail.approved;
1512
1732
  if (!messageId || !toolCallId) return;
1513
1733
 
1514
- console.log("🔔 Approval resolved:", {
1515
- messageId,
1516
- toolCallId,
1517
- approved,
1518
- });
1734
+ // Approval resolved; update local state
1519
1735
 
1520
1736
  setMessages((prev) => {
1521
1737
  const updated = prev.map((m) => {
@@ -1561,10 +1777,9 @@ export function AgentTerminal({
1561
1777
  const loadAgent = useCallback(async () => {
1562
1778
  try {
1563
1779
  if (agentStub.status === "new") {
1564
- console.log("✅ Setting up new agent", agentStub.id);
1565
1780
  // Set agent ID immediately for new agents
1566
1781
  (window as any).currentAgentId = agentStub.id;
1567
- console.log("🔗 Setting currentAgentId for new agent:", agentStub.id);
1782
+ // Set currentAgentId for new agent
1568
1783
  // Derive initial profile from provided metadata if present
1569
1784
  const initialProfileIdFromMeta = (() => {
1570
1785
  try {
@@ -1772,10 +1987,6 @@ export function AgentTerminal({
1772
1987
 
1773
1988
  // Set agent ID for existing agents too
1774
1989
  (window as any).currentAgentId = agentData.id;
1775
- console.log(
1776
- "🔗 Setting currentAgentId for existing agent:",
1777
- agentData.id,
1778
- );
1779
1990
 
1780
1991
  // Parse metadata from DB if present (do not seed for existing agents)
1781
1992
  const parsedMeta: AgentMetadata | null = (() => {
@@ -1800,18 +2011,10 @@ export function AgentTerminal({
1800
2011
  const isRunning =
1801
2012
  agentData.status === "running" || (agentData.status as any) === 1;
1802
2013
  if (isRunning) {
1803
- console.log("🚀 Agent is running, connecting to stream...", {
1804
- agentId: agentData.id,
1805
- status: agentData.status,
1806
- messageCount: agentData.messages?.length || 0,
1807
- });
1808
2014
  // Use setTimeout to ensure state updates are processed first
1809
2015
  setTimeout(async () => {
1810
2016
  // Check if we're already connecting to avoid duplicate connections
1811
2017
  if (abortControllerRef.current) {
1812
- console.log(
1813
- "⚠️ loadAgent: Already connected to stream, skipping duplicate connection",
1814
- );
1815
2018
  return;
1816
2019
  }
1817
2020
 
@@ -1828,9 +2031,6 @@ export function AgentTerminal({
1828
2031
  ),
1829
2032
  );
1830
2033
  if (hasPending) {
1831
- console.log(
1832
- "⏸️ loadAgent: Pending approvals detected, delaying stream reconnect",
1833
- );
1834
2034
  return;
1835
2035
  }
1836
2036
  } catch {}
@@ -1846,7 +2046,7 @@ export function AgentTerminal({
1846
2046
  err?.message?.includes("404") ||
1847
2047
  err?.message?.includes("not found")
1848
2048
  ) {
1849
- console.log("ℹ️ Agent does not exist, treating as new");
2049
+ // Agent does not exist, treat as new
1850
2050
  setAgent(undefined);
1851
2051
  setMessages([]);
1852
2052
  setError(null);
@@ -1924,7 +2124,6 @@ export function AgentTerminal({
1924
2124
  useEffect(() => {
1925
2125
  return () => {
1926
2126
  if (abortControllerRef.current) {
1927
- console.log("Cleaning up stream connection");
1928
2127
  abortControllerRef.current.abort();
1929
2128
  }
1930
2129
  };
@@ -2107,7 +2306,7 @@ export function AgentTerminal({
2107
2306
  seed: deterministicFlags.seed,
2108
2307
  };
2109
2308
 
2110
- console.log("Starting agent:", request);
2309
+ // Starting agent
2111
2310
 
2112
2311
  // Set waiting state to show dancing dots immediately
2113
2312
  setIsWaitingForResponse(true);
@@ -2149,7 +2348,14 @@ export function AgentTerminal({
2149
2348
  };
2150
2349
 
2151
2350
  const handleKeyPress = (e: React.KeyboardEvent) => {
2152
- if (e.key === "Enter" && !e.ctrlKey && !e.metaKey) {
2351
+ // Submit only on plain Enter (no Ctrl/Meta/Shift/Alt)
2352
+ if (
2353
+ e.key === "Enter" &&
2354
+ !e.ctrlKey &&
2355
+ !e.metaKey &&
2356
+ !e.shiftKey &&
2357
+ !e.altKey
2358
+ ) {
2153
2359
  e.preventDefault();
2154
2360
  handleSubmit();
2155
2361
  }
@@ -2825,7 +3031,7 @@ export function AgentTerminal({
2825
3031
  if (isLoading) {
2826
3032
  return (
2827
3033
  <div className="flex h-full items-center justify-center">
2828
- <div className="flex items-center gap-2 text-sm text-gray-500">
3034
+ <div className="flex items-center gap-2 text-xs text-gray-500">
2829
3035
  <Loader2 className="h-4 w-4 animate-spin" strokeWidth={1} />
2830
3036
  Loading agent...
2831
3037
  </div>
@@ -3024,7 +3230,7 @@ export function AgentTerminal({
3024
3230
  {renderContextInfoBar()}
3025
3231
 
3026
3232
  {/* Todo List Panel */}
3027
- <TodoListPanel messages={messages} />
3233
+ <TodoListPanel messages={messages} agentMetadata={agentMetadata} />
3028
3234
 
3029
3235
  {/* Input */}
3030
3236
  <div className="border-t border-gray-200 p-4">
@@ -3102,19 +3308,25 @@ export function AgentTerminal({
3102
3308
  </TooltipTrigger>
3103
3309
  <TooltipContent side="top" sideOffset={6}>
3104
3310
  <div className="max-w-[320px] space-y-1">
3105
- <div>
3106
- <span className="font-semibold text-green-500">Ask</span>: Limited tool
3107
- access as configured by the profile (Ask Mode Tools).
3311
+ <div>
3312
+ <span className="font-semibold text-green-500">Ask</span>:
3313
+ Limited tool access as configured by the profile (Ask Mode
3314
+ Tools).
3108
3315
  </div>
3109
3316
  <div>
3110
- <span className="font-semibold text-amber-500">Restricted</span>:
3111
- Full tool access, but writes are limited to pages/items in the current
3112
- context. Creating new items or updating existing items outside the current context requires explicit approval.
3317
+ <span className="font-semibold text-amber-500">
3318
+ Restricted
3319
+ </span>
3320
+ : Full tool access, but writes are limited to pages/items in
3321
+ the current context. Creating new items or updating existing
3322
+ items outside the current context requires explicit
3323
+ approval.
3113
3324
  </div>
3114
3325
  <div>
3115
- <span className="font-semibold text-red-500">Agent</span>: Full tool
3116
- access; can write across the site/project only limited by user permissions.
3117
- </div>
3326
+ <span className="font-semibold text-red-500">Agent</span>:
3327
+ Full tool access; can write across the site/project only
3328
+ limited by user permissions.
3329
+ </div>
3118
3330
  </div>
3119
3331
  </TooltipContent>
3120
3332
  </Tooltip>
@@ -3246,7 +3458,21 @@ export function AgentTerminal({
3246
3458
  </div>
3247
3459
  </div>
3248
3460
  <div className="mt-1 flex items-center gap-2 text-[10px] text-gray-500">
3249
- <AgentCostDisplay totalTokens={totalTokens} />
3461
+ <AgentCostDisplay
3462
+ totalTokens={
3463
+ liveTotals
3464
+ ? {
3465
+ input: liveTotals.input,
3466
+ output: liveTotals.output,
3467
+ cached: liveTotals.cached,
3468
+ inputCost: liveTotals.inputCost,
3469
+ outputCost: liveTotals.outputCost,
3470
+ cachedCost: liveTotals.cachedCost,
3471
+ totalCost: liveTotals.totalCost,
3472
+ }
3473
+ : totalTokens
3474
+ }
3475
+ />
3250
3476
  {(() => {
3251
3477
  try {
3252
3478
  const s = (window as any).__agentContextWindowStatus;
@@ -3255,7 +3481,7 @@ export function AgentTerminal({
3255
3481
  typeof s.contextUsedPercent === "number"
3256
3482
  ? `${s.contextUsedPercent.toFixed(1)}%`
3257
3483
  : undefined;
3258
-
3484
+
3259
3485
  // Helper function to format tokens as "k"
3260
3486
  const formatTokens = (tokens: number) => {
3261
3487
  if (tokens >= 1000) {
@@ -3269,7 +3495,7 @@ export function AgentTerminal({
3269
3495
  return (
3270
3496
  <Tooltip>
3271
3497
  <TooltipTrigger asChild>
3272
- <div className="rounded border border-gray-200 bg-gray-50 px-2 py-0.5 cursor-help">
3498
+ <div className="cursor-help rounded border border-gray-200 bg-gray-50 px-2 py-0.5">
3273
3499
  Context: {pct}
3274
3500
  </div>
3275
3501
  </TooltipTrigger>