@alpaca-editor/core 1.0.4064 → 1.0.4066

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 (91) hide show
  1. package/dist/editor/ContextMenu.js +0 -2
  2. package/dist/editor/ContextMenu.js.map +1 -1
  3. package/dist/editor/ImageEditButton.js +10 -3
  4. package/dist/editor/ImageEditButton.js.map +1 -1
  5. package/dist/editor/ai/AgentTerminal.d.ts +3 -2
  6. package/dist/editor/ai/AgentTerminal.js +386 -94
  7. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  8. package/dist/editor/ai/Agents.js +67 -25
  9. package/dist/editor/ai/Agents.js.map +1 -1
  10. package/dist/editor/ai/AiResponseMessage.d.ts +6 -1
  11. package/dist/editor/ai/AiResponseMessage.js +63 -3
  12. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  13. package/dist/editor/ai/AiTerminal.js +27 -2
  14. package/dist/editor/ai/AiTerminal.js.map +1 -1
  15. package/dist/editor/client/EditorClient.js +32 -19
  16. package/dist/editor/client/EditorClient.js.map +1 -1
  17. package/dist/editor/client/editContext.d.ts +4 -2
  18. package/dist/editor/client/editContext.js.map +1 -1
  19. package/dist/editor/client/operations.js +9 -6
  20. package/dist/editor/client/operations.js.map +1 -1
  21. package/dist/editor/commands/componentCommands.js +57 -7
  22. package/dist/editor/commands/componentCommands.js.map +1 -1
  23. package/dist/editor/field-types/richtext/contextMenuFactory.js +0 -3
  24. package/dist/editor/field-types/richtext/contextMenuFactory.js.map +1 -1
  25. package/dist/editor/menubar/ToolbarFactory.js +5 -2
  26. package/dist/editor/menubar/ToolbarFactory.js.map +1 -1
  27. package/dist/editor/menubar/toolbar-sections/UtilityControls.js +1 -1
  28. package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
  29. package/dist/editor/page-editor-chrome/CommentHighlighting.js +6 -4
  30. package/dist/editor/page-editor-chrome/CommentHighlighting.js.map +1 -1
  31. package/dist/editor/page-editor-chrome/CommentHighlightings.js +1 -1
  32. package/dist/editor/page-editor-chrome/CommentHighlightings.js.map +1 -1
  33. package/dist/editor/page-editor-chrome/FrameMenu.js +6 -8
  34. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  35. package/dist/editor/page-viewer/PageViewerFrame.js +70 -4
  36. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  37. package/dist/editor/reviews/Comment.js +3 -58
  38. package/dist/editor/reviews/Comment.js.map +1 -1
  39. package/dist/editor/reviews/CommentDisplayPopover.js +2 -3
  40. package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
  41. package/dist/editor/reviews/CommentEditor.js +2 -2
  42. package/dist/editor/reviews/CommentEditor.js.map +1 -1
  43. package/dist/editor/reviews/Comments.js +4 -0
  44. package/dist/editor/reviews/Comments.js.map +1 -1
  45. package/dist/editor/reviews/Reviews.js +2 -2
  46. package/dist/editor/reviews/Reviews.js.map +1 -1
  47. package/dist/editor/reviews/commentAi.d.ts +7 -0
  48. package/dist/editor/reviews/commentAi.js +86 -0
  49. package/dist/editor/reviews/commentAi.js.map +1 -0
  50. package/dist/editor/sidebar/ComponentTree.js +157 -49
  51. package/dist/editor/sidebar/ComponentTree.js.map +1 -1
  52. package/dist/editor/sidebar/Debug.js +1 -1
  53. package/dist/editor/sidebar/Debug.js.map +1 -1
  54. package/dist/revision.d.ts +2 -2
  55. package/dist/revision.js +2 -2
  56. package/dist/styles.css +15 -4
  57. package/dist/types.d.ts +1 -1
  58. package/package.json +1 -1
  59. package/src/editor/ContextMenu.tsx +0 -2
  60. package/src/editor/ImageEditButton.tsx +36 -8
  61. package/src/editor/ai/AgentTerminal.tsx +436 -65
  62. package/src/editor/ai/Agents.tsx +217 -117
  63. package/src/editor/ai/AiResponseMessage.tsx +106 -2
  64. package/src/editor/ai/AiTerminal.tsx +27 -0
  65. package/src/editor/client/EditorClient.tsx +41 -20
  66. package/src/editor/client/editContext.ts +4 -2
  67. package/src/editor/client/operations.ts +9 -8
  68. package/src/editor/commands/componentCommands.tsx +61 -13
  69. package/src/editor/field-types/richtext/components/EditorDropdown.css +1 -0
  70. package/src/editor/field-types/richtext/contextMenuFactory.tsx +0 -4
  71. package/src/editor/menubar/ToolbarFactory.tsx +6 -2
  72. package/src/editor/menubar/toolbar-sections/UtilityControls.tsx +23 -19
  73. package/src/editor/page-editor-chrome/CommentHighlighting.tsx +6 -4
  74. package/src/editor/page-editor-chrome/CommentHighlightings.tsx +3 -1
  75. package/src/editor/page-editor-chrome/FrameMenu.tsx +6 -8
  76. package/src/editor/page-viewer/PageViewerFrame.tsx +80 -4
  77. package/src/editor/reviews/Comment.tsx +4 -66
  78. package/src/editor/reviews/CommentDisplayPopover.tsx +2 -3
  79. package/src/editor/reviews/CommentEditor.tsx +2 -2
  80. package/src/editor/reviews/Comments.tsx +12 -0
  81. package/src/editor/reviews/Reviews.tsx +2 -0
  82. package/src/editor/reviews/commentAi.ts +106 -0
  83. package/src/editor/sidebar/ComponentTree.tsx +223 -69
  84. package/src/editor/sidebar/Debug.tsx +1 -1
  85. package/src/revision.ts +2 -2
  86. package/src/types.ts +1 -1
  87. package/styles.css +0 -5
  88. package/dist/editor/ai/AiPromptPopover.d.ts +0 -7
  89. package/dist/editor/ai/AiPromptPopover.js +0 -111
  90. package/dist/editor/ai/AiPromptPopover.js.map +0 -1
  91. package/src/editor/ai/AiPromptPopover.tsx +0 -206
@@ -16,9 +16,16 @@ import {
16
16
  getClosedAgents,
17
17
  closeAgent as closeAgentService,
18
18
  deleteAgent,
19
+ AgentMetadata,
19
20
  } from "../services/agentService";
20
21
  import { AgentTerminal } from "./AgentTerminal";
21
22
  import { useEditContext } from "../client/editContext";
23
+ import {
24
+ Tooltip,
25
+ TooltipContent,
26
+ TooltipTrigger,
27
+ } from "../../components/ui/tooltip";
28
+ import { Button } from "../../components/ui/button";
22
29
 
23
30
  // function convertAgentMessagesToTerminalMessages(
24
31
  // agentMessages: AgentChatMessage[],
@@ -49,13 +56,30 @@ const ACTIVE_AGENT_STORAGE_KEY = "editor.activeAgentId";
49
56
  export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
50
57
  const [agents, setAgents] = useState<Agent[]>([]);
51
58
  const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
59
+ const activeAgentIdRef = useRef<string | null>(null);
52
60
  const [historyPopoverOpen, setHistoryPopoverOpen] = useState(false);
53
61
  const [menuPopoverOpen, setMenuPopoverOpen] = useState(false);
54
62
  const [loadingAgents, setLoadingAgents] = useState(false);
55
63
  const [inactiveAgents, setInactiveAgents] = useState<Agent[]>([]);
64
+ const [historyFilter, setHistoryFilter] = useState("");
65
+ const [initialMetadataMap, setInitialMetadataMap] = useState<
66
+ Record<string, AgentMetadata | undefined>
67
+ >({});
56
68
 
57
69
  const editContext = useEditContext();
58
70
 
71
+ // Helper function to filter agents by name
72
+ const getFilteredInactiveAgents = (): Agent[] => {
73
+ if (!historyFilter.trim()) {
74
+ return inactiveAgents;
75
+ }
76
+
77
+ const filterLower = historyFilter.toLowerCase();
78
+ return inactiveAgents.filter((agent) =>
79
+ agent.name.toLowerCase().includes(filterLower),
80
+ );
81
+ };
82
+
59
83
  // Helper function to get the most recently updated agent
60
84
  const getMostRecentAgent = (agentList: Agent[]): Agent | null => {
61
85
  if (agentList.length === 0) return null;
@@ -69,6 +93,7 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
69
93
  // Helper function to set active agent and persist to localStorage
70
94
  const setActiveAgentIdWithStorage = (agentId: string | null) => {
71
95
  setActiveAgentId(agentId);
96
+ activeAgentIdRef.current = agentId;
72
97
 
73
98
  if (agentId) {
74
99
  localStorage.setItem(ACTIVE_AGENT_STORAGE_KEY, agentId);
@@ -77,26 +102,43 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
77
102
  }
78
103
  };
79
104
 
80
- // Initialize with a default agent if none exist
81
- useEffect(() => {
82
- if (agents.length === 0) {
83
- const defaultAgent: Agent = {
84
- status: "new",
85
- id: crypto.randomUUID(),
86
- name: `New Agent`,
87
- updatedDate: new Date().toISOString(),
88
- userId: "",
89
- };
90
- setAgents([defaultAgent]);
91
- setActiveAgentId(defaultAgent.id);
92
- }
93
- }, [agents.length]);
94
105
 
95
106
  // Load agents from backend on mount
96
107
  useEffect(() => {
97
108
  loadAgentsFromBackend();
98
109
  }, []);
99
110
 
111
+ // Listen for external requests to add a new agent (e.g., AI command)
112
+ useEffect(() => {
113
+ const handleAddNewAgent = (ev: Event) => {
114
+ let initialMetadata: AgentMetadata | undefined = undefined;
115
+ try {
116
+ const ce = ev as unknown as CustomEvent;
117
+ initialMetadata = (ce.detail && (ce.detail as any).metadata) as
118
+ | AgentMetadata
119
+ | undefined;
120
+ } catch {}
121
+
122
+ addAgent(initialMetadata);
123
+ // Ensure the prompt focuses after the agent tab mounts
124
+ setTimeout(() => {
125
+ try {
126
+ window.dispatchEvent(new CustomEvent("editor:focusAgentPrompt"));
127
+ } catch {}
128
+ }, 60);
129
+ };
130
+
131
+ window.addEventListener(
132
+ "editor:addNewAgent",
133
+ handleAddNewAgent as EventListener,
134
+ );
135
+ return () =>
136
+ window.removeEventListener(
137
+ "editor:addNewAgent",
138
+ handleAddNewAgent as EventListener,
139
+ );
140
+ }, []);
141
+
100
142
  // Subscribe to websocket messages for agent-started events
101
143
  useEffect(() => {
102
144
  if (!editContext?.addSocketMessageListener) return;
@@ -210,7 +252,7 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
210
252
  // return null;
211
253
  // };
212
254
 
213
- const addAgent = () => {
255
+ const addAgent = (metadata?: AgentMetadata) => {
214
256
  const newAgent: Agent = {
215
257
  status: "new",
216
258
  id: crypto.randomUUID(),
@@ -220,6 +262,9 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
220
262
  };
221
263
  setAgents((prev) => [...prev, newAgent]);
222
264
  setActiveAgentIdWithStorage(newAgent.id);
265
+ if (metadata) {
266
+ setInitialMetadataMap((prev) => ({ ...prev, [newAgent.id]: metadata }));
267
+ }
223
268
  };
224
269
 
225
270
  const closeAgent = async (agentId: string) => {
@@ -235,7 +280,7 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
235
280
  const filtered = prev.filter((a) => a.id !== agentId);
236
281
 
237
282
  // If we're closing the active terminal, switch to the most recent remaining one or clear storage
238
- if (activeAgentId === agentId) {
283
+ if (activeAgentIdRef.current === agentId) {
239
284
  if (filtered.length > 0) {
240
285
  const mostRecentAgent = getMostRecentAgent(filtered);
241
286
  setActiveAgentIdWithStorage(mostRecentAgent?.id || null);
@@ -249,10 +294,12 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
249
294
  };
250
295
 
251
296
  const closeOtherAgents = async () => {
252
- if (!activeAgentId) return;
297
+ if (!activeAgentIdRef.current) return;
253
298
 
254
299
  // Get agents to close (all except active)
255
- const agentsToClose = agents.filter((a) => a.id !== activeAgentId);
300
+ const agentsToClose = agents.filter(
301
+ (a) => a.id !== activeAgentIdRef.current,
302
+ );
256
303
 
257
304
  // Permanently close each agent in the backend
258
305
  const closePromises = agentsToClose.map(async (agent) => {
@@ -267,7 +314,7 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
267
314
  await Promise.all(closePromises);
268
315
 
269
316
  setAgents((prev) => {
270
- return prev.filter((a) => a.id === activeAgentId);
317
+ return prev.filter((a) => a.id === activeAgentIdRef.current);
271
318
  });
272
319
  setMenuPopoverOpen(false);
273
320
  };
@@ -333,11 +380,22 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
333
380
  );
334
381
  }
335
382
 
383
+ // Empty state when no agents are open
384
+ if (!loadingAgents && agents.length === 0) {
385
+ return (
386
+ <div className="flex h-full items-center justify-center">
387
+ <Button onClick={() => addAgent()}>
388
+ <Plus className="mr-2 h-4 w-4" strokeWidth={1} /> Create agent
389
+ </Button>
390
+ </div>
391
+ );
392
+ }
393
+
336
394
  return (
337
395
  <div className="flex h-full flex-col">
338
396
  {/* Tab Header */}
339
397
  <div className="flex items-center border-b border-gray-200 bg-gray-50">
340
- <div className="flex flex-1 overflow-x-auto">
398
+ <div className="scrollbar-hide flex flex-1 overflow-x-auto">
341
399
  {agents.map((agent) => (
342
400
  <div
343
401
  key={agent.id}
@@ -374,121 +432,160 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
374
432
  getTabsMenuItems(),
375
433
  );
376
434
  // Then update the active tab on the next tick to avoid interfering with positioning
377
- }, 200);
435
+ }, 150);
378
436
  setActiveAgentIdWithStorage(agent.id);
379
437
  }}
380
438
  >
381
- <span className="truncate">{agent.name}</span>
382
- {agents.length > 1 && (
383
- <SimpleIconButton
384
- onClick={(e) => {
385
- e.stopPropagation();
386
- closeAgent(agent.id);
387
- }}
388
- icon={<X className="size-2" strokeWidth={1} />}
389
- label="Close"
390
- className="ml-1 opacity-60 hover:opacity-100"
391
- />
392
- )}
439
+ <Tooltip>
440
+ <TooltipTrigger asChild>
441
+ <span className="truncate" title={agent.name}>
442
+ {agent.name}
443
+ </span>
444
+ </TooltipTrigger>
445
+ <TooltipContent>{agent.name}</TooltipContent>
446
+ </Tooltip>
447
+ <SimpleIconButton
448
+ onClick={(e) => {
449
+ e.stopPropagation();
450
+ closeAgent(agent.id);
451
+ }}
452
+ icon={<X className="size-2" strokeWidth={1} />}
453
+ label="Close"
454
+ className="ml-1 opacity-60 hover:opacity-100"
455
+ />
393
456
  </div>
394
457
  ))}
395
458
  </div>
396
459
 
397
460
  {/* History Popover */}
398
- <div className="flex items-center px-1">
399
- <Popover
400
- open={historyPopoverOpen}
401
- onOpenChange={setHistoryPopoverOpen}
402
- >
403
- <PopoverTrigger asChild>
404
- <SimpleIconButton
405
- onClick={() => {}}
406
- icon={<History className="size-4" strokeWidth={1} />}
407
- label="Agent History"
408
- className="text-gray-600 hover:text-gray-800"
409
- />
410
- </PopoverTrigger>
411
- <PopoverContent className="w-64 p-0" align="end">
412
- <div className="border-b border-gray-100 px-3 py-2 text-xs font-medium text-gray-500">
413
- Closed Agents
414
- </div>
415
- <div className="max-h-80 overflow-y-auto">
416
- {inactiveAgents.length === 0 ? (
417
- <div className="px-3 py-2 text-xs text-gray-500">
418
- No closed agents found
461
+ {agents.length > 0 && (
462
+ <div className="flex items-center px-1">
463
+ <Popover
464
+ open={historyPopoverOpen}
465
+ onOpenChange={(open) => {
466
+ setHistoryPopoverOpen(open);
467
+ if (!open) {
468
+ setHistoryFilter(""); // Clear filter when popover closes
469
+ }
470
+ }}
471
+ >
472
+ <PopoverTrigger asChild>
473
+ <SimpleIconButton
474
+ onClick={() => {}}
475
+ icon={<History className="size-4" strokeWidth={1} />}
476
+ label="Agent History"
477
+ className="text-gray-600 hover:text-gray-800"
478
+ />
479
+ </PopoverTrigger>
480
+ <PopoverContent className="w-64 p-0" align="end">
481
+ <div className="border-b border-gray-100 px-3 py-2 text-xs font-medium text-gray-500">
482
+ Closed Agents
483
+ </div>
484
+ {inactiveAgents.length > 0 && (
485
+ <div className="border-b border-gray-100 px-3 py-2">
486
+ <input
487
+ type="text"
488
+ placeholder="Filter agents..."
489
+ value={historyFilter}
490
+ onChange={(e) => setHistoryFilter(e.target.value)}
491
+ className="w-full rounded border border-gray-200 px-2 py-1 text-xs focus:border-blue-500 focus:outline-none"
492
+ />
419
493
  </div>
420
- ) : (
421
- inactiveAgents.map((agent) => (
422
- <div
423
- key={agent.id}
424
- className="cursor-pointer border-b border-gray-50 px-3 py-2 text-xs hover:bg-gray-50"
425
- onClick={() => openAgentFromHistory(agent)}
426
- >
427
- <div className="flex items-center justify-between">
428
- <div className="min-w-0 flex-1">
429
- <div className="truncate font-medium text-gray-900">
430
- {agent.name}
431
- </div>
432
- <div className="text-xs text-gray-400">
433
- {new Date(agent.updatedDate).toLocaleString()}
494
+ )}
495
+ <div className="max-h-80 overflow-y-auto">
496
+ {(() => {
497
+ const filteredAgents = getFilteredInactiveAgents();
498
+
499
+ if (inactiveAgents.length === 0) {
500
+ return (
501
+ <div className="px-3 py-2 text-xs text-gray-500">
502
+ No closed agents found
503
+ </div>
504
+ );
505
+ }
506
+
507
+ if (filteredAgents.length === 0) {
508
+ return (
509
+ <div className="px-3 py-2 text-xs text-gray-500">
510
+ No agents match your filter
511
+ </div>
512
+ );
513
+ }
514
+
515
+ return filteredAgents.map((agent) => (
516
+ <div
517
+ key={agent.id}
518
+ className="cursor-pointer border-b border-gray-50 px-3 py-2 text-xs hover:bg-gray-50"
519
+ onClick={() => openAgentFromHistory(agent)}
520
+ >
521
+ <div className="flex items-center justify-between">
522
+ <div className="min-w-0 flex-1">
523
+ <div className="truncate font-medium text-gray-900">
524
+ {agent.name}
525
+ </div>
526
+ <div className="text-xs text-gray-400">
527
+ {new Date(agent.updatedDate).toLocaleString()}
528
+ </div>
434
529
  </div>
530
+ <SimpleIconButton
531
+ onClick={(e) => deleteAgentFromHistory(agent.id, e)}
532
+ icon={<Trash className="size-3" strokeWidth={1} />}
533
+ label="Delete Agent"
534
+ className="ml-2 text-red-600 opacity-60 hover:text-red-700 hover:opacity-100"
535
+ />
435
536
  </div>
436
- <SimpleIconButton
437
- onClick={(e) => deleteAgentFromHistory(agent.id, e)}
438
- icon={<Trash className="size-3" strokeWidth={1} />}
439
- label="Delete Agent"
440
- className="ml-2 text-red-600 opacity-60 hover:text-red-700 hover:opacity-100"
441
- />
442
537
  </div>
443
- </div>
444
- ))
445
- )}
446
- </div>
447
- </PopoverContent>
448
- </Popover>
449
- </div>
538
+ ));
539
+ })()}
540
+ </div>
541
+ </PopoverContent>
542
+ </Popover>
543
+ </div>
544
+ )}
450
545
 
451
546
  {/* Menu Popover */}
452
- <div className="flex items-center px-1">
453
- <Popover open={menuPopoverOpen} onOpenChange={setMenuPopoverOpen}>
454
- <PopoverTrigger asChild>
455
- <SimpleIconButton
456
- onClick={() => {}}
457
- icon={<MoreVertical className="size-4" strokeWidth={1} />}
458
- label="Menu"
459
- className="text-gray-600 hover:text-gray-800"
460
- />
461
- </PopoverTrigger>
462
- <PopoverContent className="w-48 p-0" align="end">
463
- <div className="py-1">
464
- {getTabsMenuItems().map((item) =>
465
- item.separator ? (
466
- <div key={item.id} className="my-1 h-px bg-gray-100" />
467
- ) : (
468
- <button
469
- key={item.id}
470
- onClick={async (e) => {
471
- if (item.command) await item.command(e);
472
- setMenuPopoverOpen(false);
473
- }}
474
- disabled={!!item.disabled}
475
- className="w-full px-3 py-2 text-left text-xs hover:bg-gray-50 disabled:cursor-not-allowed disabled:text-gray-400"
476
- >
477
- {item.label}
478
- </button>
479
- ),
480
- )}
481
- </div>
482
- </PopoverContent>
483
- </Popover>
484
- </div>
547
+ {agents.length > 0 && (
548
+ <div className="flex items-center px-1">
549
+ <Popover open={menuPopoverOpen} onOpenChange={setMenuPopoverOpen}>
550
+ <PopoverTrigger asChild>
551
+ <SimpleIconButton
552
+ onClick={() => {}}
553
+ icon={<MoreVertical className="size-4" strokeWidth={1} />}
554
+ label="Menu"
555
+ className="text-gray-600 hover:text-gray-800"
556
+ />
557
+ </PopoverTrigger>
558
+ <PopoverContent className="w-48 p-0" align="end">
559
+ <div className="py-1">
560
+ {getTabsMenuItems().map((item) =>
561
+ item.separator ? (
562
+ <div key={item.id} className="my-1 h-px bg-gray-100" />
563
+ ) : (
564
+ <button
565
+ key={item.id}
566
+ onClick={async (e) => {
567
+ if (item.command) await item.command(e);
568
+ setMenuPopoverOpen(false);
569
+ }}
570
+ disabled={!!item.disabled}
571
+ className="w-full px-3 py-2 text-left text-xs hover:bg-gray-50 disabled:cursor-not-allowed disabled:text-gray-400"
572
+ >
573
+ {item.label}
574
+ </button>
575
+ ),
576
+ )}
577
+ </div>
578
+ </PopoverContent>
579
+ </Popover>
580
+ </div>
581
+ )}
485
582
 
486
583
  {/* Add Terminal Button */}
487
584
  <div className="flex items-center px-1">
488
585
  <SimpleIconButton
489
- onClick={addAgent}
586
+ onClick={() => addAgent()}
490
587
  icon={<Plus className="size-4" strokeWidth={1} />}
491
- label="Add Terminal"
588
+ label={agents.length === 0 ? "Create Agent" : "Add Agent"}
492
589
  className="text-gray-600 hover:text-gray-800"
493
590
  />
494
591
  </div>
@@ -509,7 +606,10 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
509
606
  activeAgentId === agent.id ? "block" : "hidden",
510
607
  )}
511
608
  >
512
- <AgentTerminal agentStub={agent} />
609
+ <AgentTerminal
610
+ agentStub={agent}
611
+ initialMetadata={initialMetadataMap[agent.id]}
612
+ />
513
613
  </div>
514
614
  ))}
515
615
  </div>
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from "react";
1
+ import React, { useState, useEffect, useMemo } from "react";
2
2
 
3
3
  import { useEditContext } from "../client/editContext";
4
4
  import { EditOperation } from "../../types";
@@ -6,17 +6,80 @@ import { Message } from "./AiTerminal";
6
6
  import { ToolCallDisplay } from "./ToolCallDisplay";
7
7
 
8
8
  import { X, Bot, Loader2 } from "lucide-react";
9
+ import { Button } from "../../components/ui/button";
10
+
11
+ type QuickAction = {
12
+ id?: string;
13
+ label: string;
14
+ prompt?: string;
15
+ value?: string;
16
+ style?: "primary" | "secondary" | "destructive" | "outline";
17
+ };
18
+
19
+ function parseQuickActionsFromContent(content?: string): {
20
+ cleanText: string;
21
+ actions: QuickAction[];
22
+ } {
23
+ if (!content) return { cleanText: "", actions: [] };
24
+
25
+ // Match fenced code blocks like ```quick_action_buttons ...```
26
+ const fenceRegex = /```quick_action_buttons\s+([\s\S]*?)```/gi;
27
+
28
+ let cleanText = content;
29
+ const actions: QuickAction[] = [];
30
+
31
+ let match: RegExpExecArray | null;
32
+ while ((match = fenceRegex.exec(content)) !== null) {
33
+ const jsonText = match[1]?.trim();
34
+ if (!jsonText) {
35
+ continue;
36
+ }
37
+ try {
38
+ const parsed = JSON.parse(jsonText);
39
+ const rawActions =
40
+ parsed?.actions || parsed?.buttons || parsed?.choices || [];
41
+ if (Array.isArray(rawActions)) {
42
+ rawActions.forEach((a) => {
43
+ if (!a) return;
44
+ const action: QuickAction = {
45
+ id: a.id,
46
+ label: a.label || a.text || String(a.value || a.prompt || ""),
47
+ prompt: a.prompt,
48
+ value: a.value,
49
+ style: a.style,
50
+ };
51
+ if (action.label) actions.push(action);
52
+ });
53
+ }
54
+ } catch {}
55
+ // Remove this code block from the rendered text
56
+ cleanText = cleanText.replace(match[0], "");
57
+ }
58
+
59
+ return { cleanText: cleanText.trim(), actions };
60
+ }
61
+
62
+ function simpleFormatToHtml(text: string): string {
63
+ return text
64
+ .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
65
+ .replace(/\n/g, "<br/>");
66
+ }
9
67
 
10
68
  export function AiResponseMessage({
11
69
  messages,
12
70
  finished,
13
71
  editOperations,
14
72
  error,
73
+ onQuickAction,
15
74
  }: {
16
75
  messages: Message[];
17
76
  finished: boolean;
18
77
  editOperations: EditOperation[];
19
78
  error?: string;
79
+ onQuickAction?: (
80
+ action: { label: string; prompt?: string; value?: string },
81
+ message: Message,
82
+ ) => void;
20
83
  }) {
21
84
  const editContext = useEditContext();
22
85
  if (!editContext) return <></>;
@@ -108,6 +171,13 @@ export function AiResponseMessage({
108
171
  ? message.tool_calls
109
172
  : preservedToolCalls[message.id] || [];
110
173
 
174
+ // Extract quick actions from content
175
+ const { cleanText, actions } = parseQuickActionsFromContent(
176
+ (message.content || message.formattedContent || "") as string,
177
+ );
178
+
179
+ const html = simpleFormatToHtml(cleanText);
180
+
111
181
  return (
112
182
  <div
113
183
  key={filteredIndex}
@@ -116,9 +186,43 @@ export function AiResponseMessage({
116
186
  <div
117
187
  className="prose prose-sm max-w-none text-sm text-gray-700 select-text"
118
188
  dangerouslySetInnerHTML={{
119
- __html: message.formattedContent || message.content || "",
189
+ __html: html,
120
190
  }}
121
191
  />
192
+
193
+ {actions.length > 0 && (
194
+ <div className="mt-2 flex flex-wrap gap-2">
195
+ {actions.map((a, idx) => {
196
+ const variant =
197
+ a.style === "destructive"
198
+ ? "destructive"
199
+ : a.style === "outline"
200
+ ? "outline"
201
+ : a.style === "secondary"
202
+ ? "secondary"
203
+ : "default"; // primary
204
+ return (
205
+ <Button
206
+ key={(a.id || a.label || "btn") + "-" + idx}
207
+ size="sm"
208
+ variant={variant as any}
209
+ onClick={() =>
210
+ onQuickAction?.(
211
+ {
212
+ label: a.label,
213
+ prompt: a.prompt,
214
+ value: a.value,
215
+ },
216
+ message,
217
+ )
218
+ }
219
+ >
220
+ {a.label}
221
+ </Button>
222
+ );
223
+ })}
224
+ </div>
225
+ )}
122
226
  <ToolCallDisplay
123
227
  toolCalls={toolCalls}
124
228
  finished={finished}
@@ -279,6 +279,20 @@ export function AiTerminal({
279
279
  messages={formattedMessages}
280
280
  editOperations={response.editOperations}
281
281
  finished={isFinished}
282
+ onQuickAction={(action) => {
283
+ const text = (
284
+ action.prompt ||
285
+ action.value ||
286
+ action.label ||
287
+ ""
288
+ ).trim();
289
+ if (!text) return;
290
+ // Submit this as a new command
291
+ commandHandler(text, (node, done) => {
292
+ TerminalService.emit("response", node);
293
+ if (done) TerminalService.emit("response", undefined);
294
+ });
295
+ }}
282
296
  />,
283
297
  isFinished,
284
298
  );
@@ -289,6 +303,19 @@ export function AiTerminal({
289
303
  messages={messagesRef.current}
290
304
  editOperations={response.editOperations}
291
305
  finished={isFinished}
306
+ onQuickAction={(action) => {
307
+ const text = (
308
+ action.prompt ||
309
+ action.value ||
310
+ action.label ||
311
+ ""
312
+ ).trim();
313
+ if (!text) return;
314
+ commandHandler(text, (node, done) => {
315
+ TerminalService.emit("response", node);
316
+ if (done) TerminalService.emit("response", undefined);
317
+ });
318
+ }}
292
319
  />,
293
320
  isFinished,
294
321
  );