@alpaca-editor/core 1.0.4037 → 1.0.4039

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.
@@ -72,6 +72,8 @@ export type ToolCall = {
72
72
  function: {
73
73
  name: string;
74
74
  arguments: string;
75
+ result?: string;
76
+ error?: string;
75
77
  };
76
78
  };
77
79
 
@@ -202,17 +204,35 @@ export function AiTerminal({
202
204
  // Agent reconnection logic
203
205
  useEffect(() => {
204
206
  async function checkAndReconnectAgent() {
205
- if (!editContext || !agentId) return;
207
+ if (!editContext || !editContext.sessionId || !agentId) {
208
+ console.log(
209
+ "RECONNECT: Skipping reconnection - missing editContext or agentId",
210
+ {
211
+ editContext: !!editContext,
212
+ sessionId: editContext?.sessionId,
213
+ agentId,
214
+ },
215
+ );
216
+ return;
217
+ }
206
218
 
207
219
  const context = createAiContext({ editContext });
208
220
 
209
221
  try {
210
- console.log("Checking agent state for reconnection:", agentId);
222
+ console.log(
223
+ "RECONNECT: Checking agent state for reconnection:",
224
+ agentId,
225
+ );
211
226
  const agentState = await checkAgentState(agentId, context);
227
+ console.log("RECONNECT: Agent state check result:", {
228
+ exists: agentState.exists,
229
+ isRunning: agentState.isRunning,
230
+ status: agentState.agent?.status,
231
+ });
212
232
 
213
233
  if (agentState.exists && agentState.agent) {
214
234
  console.log(
215
- "Found existing agent, restoring state:",
235
+ "RECONNECT: Found existing agent, restoring state:",
216
236
  agentState.agent,
217
237
  );
218
238
 
@@ -226,13 +246,32 @@ export function AiTerminal({
226
246
  const terminalMessages = convertAgentMessagesToTerminalFormat(
227
247
  agent.messages,
228
248
  );
229
- console.log("Restoring messages:", terminalMessages);
249
+ console.log(
250
+ "RECONNECT: Restoring messages:",
251
+ terminalMessages.length,
252
+ "messages",
253
+ );
230
254
  setMessages(terminalMessages);
231
255
  setResponseMessages(terminalMessages);
256
+ } else {
257
+ console.log(
258
+ "RECONNECT: Skipping message restoration - no messages or already initialized",
259
+ {
260
+ hasAgentMessages: !!agent.messages,
261
+ agentMessageCount: agent.messages?.length || 0,
262
+ hasInitialMessages: !!options?.initialMessages,
263
+ initialMessageCount: options?.initialMessages?.length || 0,
264
+ },
265
+ );
232
266
  }
233
267
 
234
268
  // Set cost information if available
235
269
  if (agent.totalCost !== undefined) {
270
+ console.log("RECONNECT: Setting cost information:", {
271
+ totalCost: agent.totalCost,
272
+ totalInputTokens: agent.totalInputTokens,
273
+ totalOutputTokens: agent.totalOutputTokens,
274
+ });
236
275
  setResponse(
237
276
  (prev) =>
238
277
  ({
@@ -250,30 +289,52 @@ export function AiTerminal({
250
289
 
251
290
  // If agent is still running, reconnect to the stream
252
291
  if (agentState.isRunning) {
253
- console.log("Agent is still running, reconnecting to stream");
292
+ console.log(
293
+ "RECONNECT: Agent is still running, attempting to reconnect to stream",
294
+ );
295
+ setIsRunning(true);
254
296
  await reconnectToRunningAgent(agentId, context);
255
297
  } else {
256
298
  console.log(
257
- "Agent completed while disconnected, showing final state",
299
+ "RECONNECT: Agent completed while disconnected, showing final state",
258
300
  );
259
301
  }
260
302
  } else {
261
- console.log("No existing agent found or agent doesn't exist");
303
+ console.log(
304
+ "RECONNECT: No existing agent found or agent doesn't exist",
305
+ );
262
306
  }
263
307
  } catch (error) {
264
- console.error("Failed to check agent state:", error);
265
- // Don't show error to user for reconnection failures - just continue as normal
308
+ console.error("RECONNECT: Failed to check agent state:", error);
309
+ setTerminalError(
310
+ `Reconnection failed: ${error instanceof Error ? error.message : "Unknown error"}`,
311
+ );
266
312
  }
267
313
  }
268
314
 
269
- // Only try to reconnect if we have an agentId and haven't already initialized with messages
270
- if (agentId && editContext) {
315
+ // Only try to reconnect if we have an agentId and edit context
316
+ if (agentId && editContext && editContext.sessionId) {
317
+ console.log("RECONNECT: Starting reconnection check for agent:", agentId);
271
318
  checkAndReconnectAgent();
319
+ } else {
320
+ console.log(
321
+ "RECONNECT: Skipping reconnection check - conditions not met",
322
+ {
323
+ hasAgentId: !!agentId,
324
+ hasEditContext: !!editContext,
325
+ hasSessionId: !!editContext?.sessionId,
326
+ },
327
+ );
272
328
  }
273
- }, [agentId]); // Only run when agentId changes
329
+ }, [agentId, editContext?.sessionId]); // Run when agentId or session becomes available
274
330
 
275
331
  // Function to reconnect to a running agent stream
276
332
  async function reconnectToRunningAgent(agentId: string, context: AiContext) {
333
+ console.log(
334
+ "RECONNECT: Starting reconnectToRunningAgent for agent:",
335
+ agentId,
336
+ );
337
+
277
338
  try {
278
339
  const abortController = new AbortController();
279
340
  abortControllerRef.current = abortController;
@@ -289,34 +350,84 @@ export function AiTerminal({
289
350
  };
290
351
  let completionProcessed = false;
291
352
 
292
- console.log("Reconnecting to agent stream:", agentId);
353
+ console.log("RECONNECT: Attempting to connect to agent stream:", agentId);
354
+ console.log(
355
+ "RECONNECT: Current messages state:",
356
+ messages.length,
357
+ "messages",
358
+ );
359
+
293
360
  await connectToAgentStream(
294
361
  agentId,
295
362
  context,
296
363
  (streamMessage) => {
297
364
  console.log(
298
- "Reconnected stream message:",
365
+ "RECONNECT: Received stream message:",
299
366
  streamMessage.type,
300
367
  streamMessage,
301
368
  );
302
369
 
303
- // Use the same stream message handling logic as the original startAgent function
304
- // (This is the same logic that was already in the component)
305
- handleStreamMessage(
306
- streamMessage,
307
- accumulatedResponse,
308
- completionProcessed,
309
- context,
310
- );
370
+ if (!streamMessage || !streamMessage.type) {
371
+ console.error(
372
+ "RECONNECT: Invalid stream message received:",
373
+ streamMessage,
374
+ );
375
+ return;
376
+ }
377
+
378
+ // Reuse stream message handling, but pipe updates into Terminal via TerminalService
379
+ try {
380
+ const terminalCallback = (
381
+ text: React.ReactNode,
382
+ finished: boolean,
383
+ ) => {
384
+ TerminalService.emit("response", { text, finished });
385
+ };
386
+
387
+ const result = handleStreamMessage(
388
+ streamMessage,
389
+ accumulatedResponse,
390
+ completionProcessed,
391
+ context,
392
+ terminalCallback,
393
+ );
394
+ if (result) {
395
+ accumulatedResponse = result.accumulatedResponse;
396
+ completionProcessed = result.completionProcessed;
397
+ }
398
+ } catch (error) {
399
+ console.error(
400
+ "RECONNECT: Error handling stream message:",
401
+ error,
402
+ streamMessage,
403
+ );
404
+ }
311
405
  },
312
406
  abortController.signal,
313
407
  );
408
+
409
+ console.log(
410
+ "RECONNECT: Successfully completed reconnection to agent stream:",
411
+ agentId,
412
+ );
413
+ setIsRunning(false);
314
414
  } catch (error) {
315
- console.error("Failed to reconnect to agent stream:", error);
415
+ console.error("RECONNECT: Failed to reconnect to agent stream:", error);
316
416
  setIsRunning(false);
317
- setTerminalError(
318
- `Failed to reconnect to agent: ${error instanceof Error ? error.message : "Unknown error"}`,
319
- );
417
+
418
+ // Check if it's an abort error (user cancelled)
419
+ const isAbortError =
420
+ error instanceof Error &&
421
+ (error.name === "AbortError" ||
422
+ error.message.toLowerCase().includes("abort"));
423
+
424
+ if (!isAbortError) {
425
+ const errorMessage = `Failed to reconnect to agent: ${error instanceof Error ? error.message : "Unknown error"}`;
426
+ console.error("RECONNECT: Setting terminal error:", errorMessage);
427
+ setTerminalError(errorMessage);
428
+ } else {
429
+ console.log("RECONNECT: Connection was aborted (user cancelled)");
430
+ }
320
431
  }
321
432
  }
322
433
 
@@ -326,7 +437,8 @@ export function AiTerminal({
326
437
  accumulatedResponse: any,
327
438
  completionProcessed: boolean,
328
439
  context: AiContext,
329
- ) {
440
+ terminalCallback: (text: React.ReactNode, finished: boolean) => void,
441
+ ): { accumulatedResponse: any; completionProcessed: boolean } | void {
330
442
  // Safety check for valid stream message
331
443
  if (!streamMessage || !streamMessage.type) {
332
444
  console.error("Invalid stream message received:", streamMessage);
@@ -392,7 +504,7 @@ export function AiTerminal({
392
504
  };
393
505
 
394
506
  // Update accumulated response with incremental content
395
- accumulatedResponse = {
507
+ const newAccumulated = {
396
508
  ...accumulatedResponse,
397
509
  ...streamMessage.data,
398
510
  messages: updatedMessages,
@@ -400,45 +512,132 @@ export function AiTerminal({
400
512
  streamMessage.data.editOperations ||
401
513
  accumulatedResponse.editOperations,
402
514
  };
515
+ accumulatedResponse = newAccumulated;
403
516
  } else {
404
517
  // Non-incremental update - use as provided
405
- accumulatedResponse = {
518
+ const newAccumulated = {
406
519
  ...accumulatedResponse,
407
520
  ...streamMessage.data,
408
521
  editOperations:
409
522
  streamMessage.data.editOperations ||
410
523
  accumulatedResponse.editOperations,
411
524
  };
525
+ accumulatedResponse = newAccumulated;
412
526
  }
413
527
 
414
528
  // Always trigger UI update for content chunks
415
- handleResponse(accumulatedResponse, () => {}, false);
529
+ handleResponse(accumulatedResponse, terminalCallback, false);
416
530
  }
417
- break;
531
+ return { accumulatedResponse, completionProcessed };
418
532
 
419
533
  case AgentStreamMessageType.ToolCall:
420
- // Handle tool calls - simplified for reconnection
421
534
  if (streamMessage.data && typeof streamMessage.data === "object") {
422
- console.log(
423
- "Tool call received during reconnection:",
424
- streamMessage.data,
535
+ // Find or create the assistant message that contains this tool call
536
+ const updatedMessages = [...(accumulatedResponse.messages || [])];
537
+ let lastAssistantMessage = null;
538
+ let lastAssistantIndex = -1;
539
+
540
+ for (let i = updatedMessages.length - 1; i >= 0; i--) {
541
+ if (updatedMessages[i].role === "assistant") {
542
+ lastAssistantMessage = updatedMessages[i];
543
+ lastAssistantIndex = i;
544
+ break;
545
+ }
546
+ }
547
+
548
+ // If no assistant message exists, create one
549
+ if (!lastAssistantMessage) {
550
+ lastAssistantMessage = {
551
+ id: crypto.randomUUID(),
552
+ role: "assistant",
553
+ name: "assistant",
554
+ content: "",
555
+ tool_calls: [],
556
+ } as Message;
557
+ updatedMessages.push(lastAssistantMessage);
558
+ lastAssistantIndex = updatedMessages.length - 1;
559
+ }
560
+
561
+ const toolCall = {
562
+ id: streamMessage.data.id,
563
+ displayName: streamMessage.data.displayName,
564
+ function: {
565
+ name:
566
+ streamMessage.data.function?.name || streamMessage.data.name,
567
+ arguments:
568
+ streamMessage.data.function?.arguments ||
569
+ streamMessage.data.arguments,
570
+ },
571
+ } as any;
572
+
573
+ const existingToolCalls = lastAssistantMessage.tool_calls || [];
574
+ const existingIndex = existingToolCalls.findIndex(
575
+ (tc: any) => tc.id === toolCall.id,
425
576
  );
426
- // For reconnection, we mainly need to update the UI
427
- // The detailed tool call logic can be handled the same as in the original
577
+ let updatedToolCalls;
578
+ if (existingIndex >= 0) {
579
+ updatedToolCalls = [...existingToolCalls];
580
+ updatedToolCalls[existingIndex] = toolCall;
581
+ } else {
582
+ updatedToolCalls = [...existingToolCalls, toolCall];
583
+ }
584
+
585
+ updatedMessages[lastAssistantIndex] = {
586
+ ...lastAssistantMessage,
587
+ tool_calls: updatedToolCalls,
588
+ } as Message;
589
+
590
+ const newAccumulated = {
591
+ ...accumulatedResponse,
592
+ messages: updatedMessages,
593
+ };
594
+ accumulatedResponse = newAccumulated;
595
+
596
+ handleResponse(accumulatedResponse, terminalCallback, false);
428
597
  }
429
- break;
598
+ return { accumulatedResponse, completionProcessed };
430
599
 
431
600
  case AgentStreamMessageType.ToolResult:
432
- // Handle tool results - simplified for reconnection
433
601
  if (streamMessage.data && typeof streamMessage.data === "object") {
434
- console.log(
435
- "Tool result received during reconnection:",
436
- streamMessage.data,
437
- );
438
- // For reconnection, we mainly need to update the UI
439
- // The detailed tool result logic can be handled the same as in the original
602
+ const updatedMessages = [...(accumulatedResponse.messages || [])];
603
+ // Find the assistant message with the matching tool call ID
604
+ for (let i = updatedMessages.length - 1; i >= 0; i--) {
605
+ if (
606
+ updatedMessages[i].role === "assistant" &&
607
+ updatedMessages[i].tool_calls
608
+ ) {
609
+ const toolCalls = updatedMessages[i].tool_calls || [];
610
+ const toolCallIndex = toolCalls.findIndex(
611
+ (tc: any) => tc.id === streamMessage.data.toolCallId,
612
+ );
613
+ if (toolCallIndex >= 0) {
614
+ const updatedToolCalls = [...toolCalls];
615
+ updatedToolCalls[toolCallIndex] = {
616
+ ...updatedToolCalls[toolCallIndex],
617
+ function: {
618
+ ...updatedToolCalls[toolCallIndex].function,
619
+ result: streamMessage.data.result,
620
+ error: streamMessage.data.error,
621
+ },
622
+ };
623
+ updatedMessages[i] = {
624
+ ...updatedMessages[i],
625
+ tool_calls: updatedToolCalls,
626
+ } as Message;
627
+ break;
628
+ }
629
+ }
630
+ }
631
+
632
+ const newAccumulated = {
633
+ ...accumulatedResponse,
634
+ messages: updatedMessages,
635
+ };
636
+ accumulatedResponse = newAccumulated;
637
+
638
+ handleResponse(accumulatedResponse, terminalCallback, false);
440
639
  }
441
- break;
640
+ return { accumulatedResponse, completionProcessed };
442
641
 
443
642
  case AgentStreamMessageType.Completed:
444
643
  if (!completionProcessed) {
@@ -446,16 +645,16 @@ export function AiTerminal({
446
645
  console.log("Agent execution completed");
447
646
  setIsRunning(false);
448
647
  if (streamMessage.data) {
449
- handleResponse(streamMessage.data, () => {}, true);
648
+ handleResponse(streamMessage.data, terminalCallback, true);
450
649
  }
451
650
  }
452
- break;
651
+ return { accumulatedResponse, completionProcessed };
453
652
 
454
653
  case AgentStreamMessageType.Error:
455
654
  console.error("Agent execution error:", streamMessage);
456
655
  setIsRunning(false);
457
656
  setTerminalError(streamMessage.error || "Agent execution failed");
458
- break;
657
+ return { accumulatedResponse, completionProcessed };
459
658
 
460
659
  default:
461
660
  console.warn("Unknown stream message type:", streamMessage.type);
@@ -967,13 +1166,14 @@ export function AiTerminal({
967
1166
  );
968
1167
 
969
1168
  if (toolCallIndex >= 0) {
970
- // Update the tool call with the result
1169
+ // Update the tool call with the result or error
971
1170
  const updatedToolCalls = [...toolCalls];
972
1171
  updatedToolCalls[toolCallIndex] = {
973
1172
  ...updatedToolCalls[toolCallIndex],
974
1173
  function: {
975
1174
  ...updatedToolCalls[toolCallIndex].function,
976
1175
  result: streamMessage.data.result,
1176
+ error: streamMessage.data.error,
977
1177
  },
978
1178
  };
979
1179
 
@@ -10,6 +10,7 @@ import {
10
10
  import { useFieldModification } from "../client/fieldModificationStore";
11
11
 
12
12
  import { useEffect, useRef, useState, useMemo } from "react";
13
+ import { RefreshCw } from "lucide-react";
13
14
 
14
15
  import { Field } from "../pageModel";
15
16
 
@@ -152,6 +153,12 @@ export function SingleLineText({
152
153
  }, 500);
153
154
  }, [editContext?.focusedField]);
154
155
 
156
+ const customSource = (field as any)?.customProperties?.source as
157
+ | string
158
+ | undefined;
159
+ const isManualRefreshEnabled =
160
+ typeof customSource === "string" && customSource.includes("manual-refresh");
161
+
155
162
  return (
156
163
  <div className="relative">
157
164
  {/* <div
@@ -168,12 +175,22 @@ export function SingleLineText({
168
175
  key={fieldItem.id + field.id + fieldItem.language + fieldItem.version}
169
176
  value={value || ""}
170
177
  disabled={readOnly}
171
- className="focus-shadow bg-gray-5 p-1.5 text-xs"
178
+ className="focus-shadow bg-gray-5 p-1.5 pr-7 text-xs"
172
179
  style={{ width: "100%" }}
173
180
  onChange={handleChange}
174
181
  onSelect={handleSelect}
175
182
  onBlur={editContextRef.current?.operations.onFieldBlur}
176
183
  />
184
+ {isManualRefreshEnabled && (
185
+ <button
186
+ type="button"
187
+ title="Re-render"
188
+ className="absolute right-1 top-1/2 -translate-y-1/2 rounded p-1 text-gray-600 hover:text-gray-900"
189
+ onClick={() => editContext?.requestRefresh("immediate")}
190
+ >
191
+ <RefreshCw className="h-4 w-4" strokeWidth={1} />
192
+ </button>
193
+ )}
177
194
  </div>
178
195
  );
179
196
  }