@alpaca-editor/core 1.0.4123 → 1.0.4128

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.
@@ -1,7 +1,15 @@
1
1
  import React, { useCallback, useState } from "react";
2
2
  import { useEditContext } from "../client/editContext";
3
3
  import { Button } from "../../components/ui/button";
4
- import { FileText, Puzzle, Type, Plus, ChevronDown, X } from "lucide-react";
4
+ import {
5
+ FileText,
6
+ Puzzle,
7
+ Type,
8
+ Plus,
9
+ ChevronDown,
10
+ ChevronUp,
11
+ X,
12
+ } from "lucide-react";
5
13
  import { getComponentById } from "../componentTreeHelper";
6
14
  import {
7
15
  AgentDetails,
@@ -39,8 +47,10 @@ export function ContextInfoBar({
39
47
  ) => {
40
48
  if (!agent?.id) return;
41
49
  const current = agentMetadata || {};
50
+ // Exclude top-level context to avoid duplicate keys when spreading
51
+ const { context: _, ...currentWithoutContext } = current;
42
52
  const next: AgentMetadata = {
43
- ...current,
53
+ ...currentWithoutContext,
44
54
  additionalData: {
45
55
  ...(current.additionalData || {}),
46
56
  context: {
@@ -115,8 +125,10 @@ export function ContextInfoBar({
115
125
  return;
116
126
  }
117
127
 
128
+ // Exclude top-level context to avoid duplicate keys when spreading
129
+ const { context: _, ...currentWithoutContext } = current;
118
130
  const next: AgentMetadata = {
119
- ...current,
131
+ ...currentWithoutContext,
120
132
  additionalData: {
121
133
  ...(current.additionalData || {}),
122
134
  context: {
@@ -158,8 +170,10 @@ export function ContextInfoBar({
158
170
 
159
171
  if (newComponentIds.length === 0) return;
160
172
 
173
+ // Exclude top-level context to avoid duplicate keys when spreading
174
+ const { context: _, ...currentWithoutContext } = current;
161
175
  const next: AgentMetadata = {
162
- ...current,
176
+ ...currentWithoutContext,
163
177
  additionalData: {
164
178
  ...(current.additionalData || {}),
165
179
  context: {
@@ -198,8 +212,10 @@ export function ContextInfoBar({
198
212
  const newComponentIds = ids.filter((id) => !!id && !existingIds.has(id));
199
213
  if (newComponentIds.length === 0) return;
200
214
 
215
+ // Exclude top-level context to avoid duplicate keys when spreading
216
+ const { context: _, ...currentWithoutContext } = current;
201
217
  const next: AgentMetadata = {
202
- ...current,
218
+ ...currentWithoutContext,
203
219
  additionalData: {
204
220
  ...(current.additionalData || {}),
205
221
  context: {
@@ -239,15 +255,34 @@ export function ContextInfoBar({
239
255
 
240
256
  const pagesToAdd = items
241
257
  .filter((it) => !!it?.id)
242
- .map((it) => ({ id: it.id, language: it.language, version: it.version }))
258
+ .map((it) => {
259
+ const anyIt = it as any;
260
+ const page = {
261
+ id: it.id,
262
+ language: it.language,
263
+ version: it.version,
264
+ } as {
265
+ id: string;
266
+ language: string;
267
+ version: number;
268
+ name?: string;
269
+ path?: string;
270
+ };
271
+ const candidateName = anyIt?.displayName || anyIt?.name;
272
+ if (candidateName) page.name = candidateName as string;
273
+ if (anyIt?.path) page.path = anyIt.path as string;
274
+ return page;
275
+ })
243
276
  .filter(
244
277
  (p) => !existingPageIds.has(`${p.id}-${p.language}-${p.version}`),
245
278
  );
246
279
 
247
280
  if (pagesToAdd.length === 0) return;
248
281
 
282
+ // Exclude top-level context to avoid duplicate keys when spreading
283
+ const { context: _, ...currentWithoutContext } = current;
249
284
  const next: AgentMetadata = {
250
- ...current,
285
+ ...currentWithoutContext,
251
286
  additionalData: {
252
287
  ...(current.additionalData || {}),
253
288
  context: {
@@ -502,70 +537,60 @@ export function ContextInfoBar({
502
537
 
503
538
  return (
504
539
  <div
505
- className={
506
- "flex flex-col gap-2 border-t border-gray-100 px-4 py-2 " +
507
- (isDragOver ? "bg-gray-50" : "")
508
- }
540
+ className="border-t border-gray-200 bg-gray-50"
509
541
  onDragOver={handleDragOver}
510
542
  onDragEnter={handleDragOver}
511
543
  onDragLeave={handleDragLeave}
512
544
  onDrop={handleDrop}
513
545
  >
514
- <div
515
- className="flex cursor-pointer items-center justify-between"
546
+ <button
516
547
  onClick={() => setIsCollapsed(!isCollapsed)}
517
- role="button"
548
+ className="flex w-full cursor-pointer items-center justify-between px-4 py-2 text-left transition-colors hover:bg-gray-100"
518
549
  aria-label={isCollapsed ? "Expand context" : "Collapse context"}
519
550
  data-testid="context-toggle"
520
551
  >
521
- <div className="min-w-0 flex-1">
522
- <div className="text-2xs text-gray-1">Context</div>
552
+ <div className="flex items-center gap-2">
553
+ <FileText className="h-4 w-4 text-gray-500" strokeWidth={1} />
554
+ <span className="text-xs font-medium text-gray-700">Context</span>
523
555
  {isCollapsed && summaryText && (
524
- <div
525
- className="text-2xs truncate text-gray-400"
526
- title={summaryText}
527
- >
528
- {summaryText}
529
- </div>
556
+ <span className="text-xs text-gray-500">· {summaryText}</span>
530
557
  )}
531
558
  </div>
532
- <ChevronDown
533
- className={
534
- "h-3 w-3 text-gray-500 transition-transform " +
535
- (isCollapsed ? "rotate-0" : "rotate-180")
536
- }
537
- strokeWidth={1}
538
- />
539
- </div>
559
+ {isCollapsed ? (
560
+ <ChevronDown className="h-4 w-4 text-gray-500" strokeWidth={1} />
561
+ ) : (
562
+ <ChevronUp className="h-4 w-4 text-gray-500" strokeWidth={1} />
563
+ )}
564
+ </button>
540
565
  {!isCollapsed && (
541
- <div className="flex flex-col gap-2">
542
- <div className="flex items-center justify-between text-xs text-gray-600">
543
- <div className="flex flex-wrap items-center gap-2">{chips}</div>
544
- </div>
545
- <div className="flex flex-col items-start gap-1">
546
- {canAddPage && (
547
- <Button
548
- size="xs"
549
- variant="outline"
550
- onClick={addPagesToContext}
551
- data-testid="add-current-item"
552
- >
553
- <Plus className="mr-1 h-3 w-3" strokeWidth={1} /> Add current
554
- item
555
- </Button>
556
- )}
557
- {canAddSelection && (
558
- <Button
559
- size="xs"
560
- variant="outline"
561
- onClick={addSelectedComponentsToContext}
562
- >
563
- <Plus className="mr-1 h-3 w-3" strokeWidth={1} /> Add selected
564
- components
565
- </Button>
566
- )}
567
- </div>
568
- <div className="text-2xs text-gray-400">
566
+ <div className="px-4 pb-3">
567
+ <div className="flex flex-wrap items-center gap-2 pb-2">{chips}</div>
568
+ {(canAddPage || canAddSelection) && (
569
+ <div className="flex flex-col items-start gap-1 pb-2">
570
+ {canAddPage && (
571
+ <Button
572
+ size="xs"
573
+ variant="outline"
574
+ onClick={addPagesToContext}
575
+ data-testid="add-current-item"
576
+ >
577
+ <Plus className="mr-1 h-3 w-3" strokeWidth={1} /> Add current
578
+ item
579
+ </Button>
580
+ )}
581
+ {canAddSelection && (
582
+ <Button
583
+ size="xs"
584
+ variant="outline"
585
+ onClick={addSelectedComponentsToContext}
586
+ >
587
+ <Plus className="mr-1 h-3 w-3" strokeWidth={1} /> Add selected
588
+ components
589
+ </Button>
590
+ )}
591
+ </div>
592
+ )}
593
+ <div className="text-xs text-gray-500">
569
594
  Tip: Drag items or components here to add them
570
595
  </div>
571
596
  </div>
@@ -10,6 +10,8 @@ import {
10
10
  import { SimpleTabs, Tab } from "../ui/SimpleTabs";
11
11
  import { Spinner } from "../ui/Spinner";
12
12
  import { SimpleIconButton } from "../ui/SimpleIconButton";
13
+ import { Button } from "../../components/ui/button";
14
+ import { approveToolCall, rejectToolCall } from "../services/agentService";
13
15
  import {
14
16
  X,
15
17
  FileText,
@@ -73,6 +75,11 @@ const getToolIcon = (toolName: string) => {
73
75
  };
74
76
 
75
77
  // Types for tool calls - supporting both AI and Agent formats
78
+ export type ApprovalInfo = {
79
+ summary: string;
80
+ riskLevel?: 'low' | 'medium' | 'high';
81
+ };
82
+
76
83
  export interface BaseToolCall {
77
84
  id: string;
78
85
  displayName?: string;
@@ -82,6 +89,7 @@ export interface BaseToolCall {
82
89
  result?: string;
83
90
  error?: string;
84
91
  };
92
+ requiresApproval?: ApprovalInfo;
85
93
  }
86
94
 
87
95
  export interface AgentToolCall {
@@ -95,6 +103,7 @@ export interface AgentToolCall {
95
103
  isCompleted: boolean;
96
104
  responseTimeMs?: number;
97
105
  createdDate: string;
106
+ requiresApproval?: ApprovalInfo;
98
107
  }
99
108
 
100
109
  interface ToolCallDisplayProps {
@@ -107,6 +116,7 @@ interface ToolCallDisplayProps {
107
116
  messageId: string;
108
117
  }
109
118
 
119
+
110
120
  // Helper function to normalize tool calls to a common format
111
121
  const normalizeToolCall = (
112
122
  toolCall: BaseToolCall | AgentToolCall,
@@ -122,6 +132,7 @@ const normalizeToolCall = (
122
132
  result: toolCall.functionResult,
123
133
  error: toolCall.functionError,
124
134
  },
135
+ requiresApproval: toolCall.requiresApproval,
125
136
  };
126
137
  }
127
138
  // Already in base format
@@ -297,13 +308,67 @@ export function ToolCallDisplay({
297
308
  const toolResult = toolCall.function?.result;
298
309
  const popoverKey = `${messageId}-${toolIndex}`;
299
310
  const isAgentToolCall = "isCompleted" in originalToolCall;
311
+
312
+ // Get approval information to check if tool call is pending approval
313
+ const approvalInfo = originalToolCall.requiresApproval || toolCall.requiresApproval;
314
+
300
315
  const isCompleted = isAgentToolCall
301
316
  ? originalToolCall.isCompleted
302
- : !!(toolResult || toolCall.function?.error);
317
+ : !!(toolResult || toolCall.function?.error) && !approvalInfo;
318
+
319
+ // Use the approval info from the backend
320
+ const finalApprovalInfo = approvalInfo;
321
+
322
+ // Check if this tool call has been approved/rejected (look for status indicators in function name)
323
+ // The AgentTerminal adds " (approved)" or " (rejected)" or " (pending approval)" to the functionName field
324
+ const originalFunctionName = ("functionName" in originalToolCall ? originalToolCall.functionName : toolCall?.function?.name) || '';
325
+ const isApproved = originalFunctionName.includes('(approved)');
326
+ const isRejected = originalFunctionName.includes('(rejected)');
327
+ const isPending = originalFunctionName.includes('(pending approval)');
328
+ // Treat only approved/rejected as final; pending should still show buttons
329
+ const hasApprovalStatus = isApproved || isRejected;
330
+
331
+ console.log("🔍 Approval status check:", {
332
+ toolCallId: toolCall.id,
333
+ originalFunctionName,
334
+ displayName: toolCall?.displayName,
335
+ functionName: toolCall?.function?.name,
336
+ isApproved,
337
+ isRejected,
338
+ hasApprovalStatus
339
+ });
340
+
341
+ // Debug: Check what we're receiving
342
+ console.log("🔍 Approval debug:", {
343
+ toolIndex,
344
+ functionName: toolCall?.function?.name,
345
+ originalRequiresApproval: originalToolCall.requiresApproval,
346
+ normalizedRequiresApproval: toolCall.requiresApproval,
347
+ approvalInfo,
348
+ approvalInfoType: typeof approvalInfo,
349
+ isCompleted,
350
+ hasApprovalStatus,
351
+ finalApprovalInfo,
352
+ shouldShowApprovalUI: !isCompleted && finalApprovalInfo && !hasApprovalStatus,
353
+ toolResult: toolCall.function?.result,
354
+ toolError: toolCall.function?.error,
355
+ });
356
+
357
+ // Debug: Check if agent ID is available
358
+ if (finalApprovalInfo) {
359
+ console.log("🔍 Agent ID check:", {
360
+ currentAgentId: (window as any)?.currentAgentId,
361
+ messageId,
362
+ toolCallId: toolCall.id,
363
+ hasAgentId: !!(window as any)?.currentAgentId,
364
+ windowKeys: Object.keys(window).filter(k => k.includes('agent') || k.includes('Agent'))
365
+ });
366
+ }
367
+
303
368
 
304
369
  return (
370
+ <div key={toolIndex}>
305
371
  <Popover
306
- key={toolIndex}
307
372
  enableIframeClickDetection={false}
308
373
  open={openPopovers[popoverKey] || false}
309
374
  onOpenChange={(open) => {
@@ -314,13 +379,17 @@ export function ToolCallDisplay({
314
379
  }}
315
380
  >
316
381
  <PopoverTrigger asChild>
317
- <div className="inline-flex cursor-pointer items-center gap-1 text-xs text-gray-500 hover:text-gray-700">
382
+ <div className="flex items-center gap-2 text-xs text-gray-500">
318
383
  {isCompleted ? (
319
384
  <div
320
385
  className={`flex items-center ${toolCall.function?.error ? "text-red-500" : ""}`}
321
386
  >
322
387
  {getToolIcon(toolCall?.function?.name || "")}
323
388
  </div>
389
+ ) : finalApprovalInfo && !hasApprovalStatus ? (
390
+ <div className="flex items-center text-amber-600">
391
+ {getToolIcon(toolCall?.function?.name || "")}
392
+ </div>
324
393
  ) : finished ? (
325
394
  <div className="flex items-center opacity-50">
326
395
  {getToolIcon(toolCall?.function?.name || "")}
@@ -328,14 +397,33 @@ export function ToolCallDisplay({
328
397
  ) : (
329
398
  <Spinner size="xs" />
330
399
  )}
331
- <span
332
- className={toolCall.function?.error ? "text-red-500" : ""}
333
- >
334
- {toolCall?.displayName ||
335
- toolCall?.function?.name ||
336
- "tool call"}
337
- {toolCall.function?.error && " (error)"}
338
- </span>
400
+ <div className="inline-flex items-center gap-2">
401
+ <span
402
+ className={`${toolCall.function?.error ? "text-red-500" : "hover:text-gray-700"} cursor-pointer`}
403
+ >
404
+ {/* Show clean function name without approval suffix */}
405
+ {(originalFunctionName || toolCall?.function?.name || "tool call")
406
+ .replace(' (approved)', '')
407
+ .replace(' (rejected)', '')
408
+ .replace(' (pending approval)', '')}
409
+ {toolCall.function?.error && " (error)"}
410
+ {finalApprovalInfo && isApproved && (
411
+ <span className="ml-2 text-xs text-green-600 font-medium">
412
+ ✓ Approved
413
+ </span>
414
+ )}
415
+ {finalApprovalInfo && isRejected && (
416
+ <span className="ml-2 text-xs text-red-600 font-medium">
417
+ ✗ Rejected
418
+ </span>
419
+ )}
420
+ {finalApprovalInfo && isPending && (
421
+ <span className="ml-2 text-xs text-amber-600 font-medium">
422
+ ⏸ Pending approval
423
+ </span>
424
+ )}
425
+ </span>
426
+ </div>
339
427
  </div>
340
428
  </PopoverTrigger>
341
429
  <PopoverContent
@@ -358,6 +446,100 @@ export function ToolCallDisplay({
358
446
  />
359
447
  </PopoverContent>
360
448
  </Popover>
449
+ {!isCompleted && finalApprovalInfo && !hasApprovalStatus && (
450
+ <div className="border rounded mt-2">
451
+ <div className="p-3 border-b bg-amber-50">
452
+ <div className="flex items-start gap-2">
453
+ <div className={`w-2 h-2 rounded-full mt-1.5 ${
454
+ finalApprovalInfo.riskLevel === 'high' ? 'bg-red-500' :
455
+ finalApprovalInfo.riskLevel === 'medium' ? 'bg-amber-500' :
456
+ 'bg-green-500'
457
+ }`} />
458
+ <div>
459
+ <div className="text-xs font-medium text-gray-900 mb-1">
460
+ Action requires approval
461
+ </div>
462
+ <div className="text-xs text-gray-700">
463
+ {finalApprovalInfo.summary}
464
+ </div>
465
+ {finalApprovalInfo.riskLevel && (
466
+ <div className={`text-xs mt-1 font-medium ${
467
+ finalApprovalInfo.riskLevel === 'high' ? 'text-red-600' :
468
+ finalApprovalInfo.riskLevel === 'medium' ? 'text-amber-600' :
469
+ 'text-green-600'
470
+ }`}>
471
+ Risk level: {finalApprovalInfo.riskLevel}
472
+ </div>
473
+ )}
474
+ </div>
475
+ </div>
476
+ </div>
477
+ <div className="flex items-center justify-end gap-2 p-2">
478
+ <Button
479
+ size="sm"
480
+ variant="secondary"
481
+ onClick={async () => {
482
+ console.log("🚫 Rejecting tool call:", {
483
+ agentId: (window as any)?.currentAgentId,
484
+ messageId: messageId,
485
+ toolCallId: toolCall.id,
486
+ });
487
+ try {
488
+ const result = await rejectToolCall({
489
+ agentId: (window as any)?.currentAgentId,
490
+ messageId: messageId,
491
+ toolCallId: toolCall.id,
492
+ });
493
+ console.log("✅ Reject successful:", result);
494
+ const ev = new CustomEvent('agent:toolApprovalResolved', {
495
+ detail: {
496
+ messageId: messageId,
497
+ toolCallId: toolCall.id,
498
+ approved: false,
499
+ },
500
+ } as any);
501
+ window.dispatchEvent(ev);
502
+ } catch (error) {
503
+ console.error("❌ Reject failed:", error);
504
+ }
505
+ }}
506
+ >
507
+ Reject
508
+ </Button>
509
+ <Button
510
+ size="sm"
511
+ onClick={async () => {
512
+ console.log("✅ Approving tool call:", {
513
+ agentId: (window as any)?.currentAgentId,
514
+ messageId: messageId,
515
+ toolCallId: toolCall.id,
516
+ });
517
+ try {
518
+ const result = await approveToolCall({
519
+ agentId: (window as any)?.currentAgentId,
520
+ messageId: messageId,
521
+ toolCallId: toolCall.id,
522
+ });
523
+ console.log("✅ Approve successful:", result);
524
+ const ev = new CustomEvent('agent:toolApprovalResolved', {
525
+ detail: {
526
+ messageId: messageId,
527
+ toolCallId: toolCall.id,
528
+ approved: true,
529
+ },
530
+ } as any);
531
+ window.dispatchEvent(ev);
532
+ } catch (error) {
533
+ console.error("❌ Approve failed:", error);
534
+ }
535
+ }}
536
+ >
537
+ Approve
538
+ </Button>
539
+ </div>
540
+ </div>
541
+ )}
542
+ </div>
361
543
  );
362
544
  })}
363
545
  </div>
@@ -1,3 +1,8 @@
1
+ export type ApprovalInfo = {
2
+ summary: string;
3
+ riskLevel?: 'low' | 'medium' | 'high';
4
+ };
5
+
1
6
  export type ToolCall = {
2
7
  id: string;
3
8
  displayName?: string;
@@ -7,6 +12,7 @@ export type ToolCall = {
7
12
  result?: string;
8
13
  error?: string;
9
14
  };
15
+ requiresApproval?: ApprovalInfo;
10
16
  };
11
17
 
12
18
  export type Message = {
@@ -86,6 +86,46 @@ export type AgentDetails = Agent & {
86
86
  messages?: AgentChatMessage[];
87
87
  };
88
88
 
89
+ export async function approveToolCall(params: {
90
+ agentId: string;
91
+ messageId: string;
92
+ toolCallId: string;
93
+ note?: string;
94
+ }): Promise<{ success: boolean }> {
95
+ const result = await post<{ success: boolean }>(
96
+ AGENT_BASE_URL + "/approveToolCall",
97
+ params,
98
+ );
99
+ if (result.type !== "success") {
100
+ throw new Error(
101
+ `Failed to approve tool call: ${result.summary || "Unknown error"} ${
102
+ result.details || ""
103
+ }`,
104
+ );
105
+ }
106
+ return result.data ?? { success: true };
107
+ }
108
+
109
+ export async function rejectToolCall(params: {
110
+ agentId: string;
111
+ messageId: string;
112
+ toolCallId: string;
113
+ note?: string;
114
+ }): Promise<{ success: boolean }> {
115
+ const result = await post<{ success: boolean }>(
116
+ AGENT_BASE_URL + "/rejectToolCall",
117
+ params,
118
+ );
119
+ if (result.type !== "success") {
120
+ throw new Error(
121
+ `Failed to reject tool call: ${result.summary || "Unknown error"} ${
122
+ result.details || ""
123
+ }`,
124
+ );
125
+ }
126
+ return result.data ?? { success: true };
127
+ }
128
+
89
129
  // Metadata shape stored on the agent. Server accepts a flexible JSON object; keep it permissive.
90
130
  export type AgentMetadata = {
91
131
  selection?: string[];
@@ -529,9 +569,12 @@ export async function updateAgentMetadata(
529
569
  agentId: string,
530
570
  metadata: AgentMetadata,
531
571
  ): Promise<{ success: boolean }> {
572
+ // Remove top-level context to avoid duplicate keys (context should only be in additionalData)
573
+ const { context: _, ...cleanMetadata } = metadata;
574
+
532
575
  const result = await post<{ success: boolean }>(
533
576
  AGENT_BASE_URL + "/updateMetadata",
534
- { agentId, metadata },
577
+ { agentId, metadata: cleanMetadata },
535
578
  );
536
579
 
537
580
  if (result.type !== "success") {
@@ -611,16 +654,31 @@ export function convertAgentMessagesToTerminalFormat(
611
654
 
612
655
  // Add tool calls if they exist
613
656
  if (agentMessage.toolCalls && agentMessage.toolCalls.length > 0) {
614
- terminalMessage.tool_calls = agentMessage.toolCalls.map((tc) => ({
615
- id: tc.toolCallId,
616
- displayName: tc.functionName,
617
- function: {
618
- name: tc.functionName,
619
- arguments: tc.functionArguments,
620
- result: tc.functionResult,
621
- error: tc.functionError,
622
- },
623
- }));
657
+ terminalMessage.tool_calls = agentMessage.toolCalls.map((tc) => {
658
+ const requiresApproval =
659
+ (tc as any)?.requiresApproval === true ||
660
+ (tc as any)?.requiresApproval === 1 ||
661
+ (tc as any)?.RequiresApproval === true;
662
+ const toolCall: any = {
663
+ id: tc.toolCallId,
664
+ displayName: tc.functionName,
665
+ function: {
666
+ name: tc.functionName,
667
+ arguments: tc.functionArguments,
668
+ result: tc.functionResult,
669
+ error: tc.functionError,
670
+ },
671
+ };
672
+
673
+ if (requiresApproval) {
674
+ toolCall.requiresApproval = {
675
+ summary: `Function call: ${tc.functionName}`,
676
+ riskLevel: "medium" as const,
677
+ };
678
+ }
679
+
680
+ return toolCall;
681
+ });
624
682
  }
625
683
 
626
684
  terminalMessages.push(terminalMessage);