@alpaca-editor/core 1.0.4063 → 1.0.4064

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 (106) hide show
  1. package/dist/config/config.js +3 -21
  2. package/dist/config/config.js.map +1 -1
  3. package/dist/editor/ConfirmationDialog.js +1 -1
  4. package/dist/editor/ConfirmationDialog.js.map +1 -1
  5. package/dist/editor/ContextMenu.js +1 -0
  6. package/dist/editor/ContextMenu.js.map +1 -1
  7. package/dist/editor/FieldListField.js +2 -2
  8. package/dist/editor/FieldListField.js.map +1 -1
  9. package/dist/editor/FieldListFieldWithFallbacks.js +1 -1
  10. package/dist/editor/FieldListFieldWithFallbacks.js.map +1 -1
  11. package/dist/editor/MainLayout.d.ts +2 -0
  12. package/dist/editor/MainLayout.js +8 -0
  13. package/dist/editor/MainLayout.js.map +1 -1
  14. package/dist/editor/ScrollingContentTree.js +9 -4
  15. package/dist/editor/ScrollingContentTree.js.map +1 -1
  16. package/dist/editor/ai/AgentTerminal.js +382 -3
  17. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  18. package/dist/editor/ai/Agents.js +44 -3
  19. package/dist/editor/ai/Agents.js.map +1 -1
  20. package/dist/editor/client/EditorClient.js +54 -16
  21. package/dist/editor/client/EditorClient.js.map +1 -1
  22. package/dist/editor/client/editContext.d.ts +4 -2
  23. package/dist/editor/client/editContext.js.map +1 -1
  24. package/dist/editor/media-selector/TreeSelector.js +1 -1
  25. package/dist/editor/media-selector/TreeSelector.js.map +1 -1
  26. package/dist/editor/menubar/VersionSelector.js +1 -1
  27. package/dist/editor/menubar/VersionSelector.js.map +1 -1
  28. package/dist/editor/menubar/toolbar-sections/UtilityControls.js +2 -1
  29. package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
  30. package/dist/editor/page-viewer/EditorForm.js +7 -0
  31. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  32. package/dist/editor/page-viewer/PageViewerFrame.js +2 -1
  33. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  34. package/dist/editor/reviews/Comment.js +57 -9
  35. package/dist/editor/reviews/Comment.js.map +1 -1
  36. package/dist/editor/reviews/CommentEditor.js +2 -2
  37. package/dist/editor/reviews/CommentEditor.js.map +1 -1
  38. package/dist/editor/reviews/CommentPopover.js +1 -1
  39. package/dist/editor/reviews/CommentPopover.js.map +1 -1
  40. package/dist/editor/reviews/CommentView.js +5 -5
  41. package/dist/editor/reviews/CommentView.js.map +1 -1
  42. package/dist/editor/services/agentService.d.ts +41 -0
  43. package/dist/editor/services/agentService.js +10 -0
  44. package/dist/editor/services/agentService.js.map +1 -1
  45. package/dist/editor/sidebar/ComponentTree.js +3 -2
  46. package/dist/editor/sidebar/ComponentTree.js.map +1 -1
  47. package/dist/editor/sidebar/SidebarView.d.ts +2 -1
  48. package/dist/editor/sidebar/SidebarView.js +2 -2
  49. package/dist/editor/sidebar/SidebarView.js.map +1 -1
  50. package/dist/editor/ui/PerfectTree.js +1 -1
  51. package/dist/editor/ui/PerfectTree.js.map +1 -1
  52. package/dist/editor/ui/SimpleTabs.js +1 -1
  53. package/dist/index.d.ts +0 -1
  54. package/dist/index.js +0 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/revision.d.ts +2 -2
  57. package/dist/revision.js +2 -2
  58. package/dist/splash-screen/NewPage.js +2 -2
  59. package/dist/splash-screen/NewPage.js.map +1 -1
  60. package/dist/styles.css +6 -0
  61. package/dist/types.d.ts +2 -1
  62. package/package.json +1 -1
  63. package/src/config/config.tsx +5 -20
  64. package/src/editor/ConfirmationDialog.tsx +4 -1
  65. package/src/editor/ContextMenu.tsx +1 -0
  66. package/src/editor/FieldListField.tsx +13 -11
  67. package/src/editor/FieldListFieldWithFallbacks.tsx +1 -0
  68. package/src/editor/MainLayout.tsx +11 -0
  69. package/src/editor/ScrollingContentTree.tsx +10 -5
  70. package/src/editor/ai/AgentTerminal.tsx +555 -1
  71. package/src/editor/ai/Agents.tsx +64 -8
  72. package/src/editor/client/EditorClient.tsx +98 -29
  73. package/src/editor/client/editContext.ts +5 -2
  74. package/src/editor/media-selector/TreeSelector.tsx +1 -0
  75. package/src/editor/menubar/VersionSelector.tsx +4 -1
  76. package/src/editor/menubar/toolbar-sections/UtilityControls.tsx +15 -2
  77. package/src/editor/page-viewer/EditorForm.tsx +13 -0
  78. package/src/editor/page-viewer/PageViewerFrame.tsx +2 -1
  79. package/src/editor/reviews/Comment.tsx +65 -9
  80. package/src/editor/reviews/CommentEditor.tsx +8 -1
  81. package/src/editor/reviews/CommentPopover.tsx +1 -0
  82. package/src/editor/reviews/CommentView.tsx +24 -3
  83. package/src/editor/services/agentService.ts +58 -0
  84. package/src/editor/sidebar/ComponentTree.tsx +6 -2
  85. package/src/editor/sidebar/SidebarView.tsx +8 -7
  86. package/src/editor/ui/PerfectTree.tsx +2 -1
  87. package/src/editor/ui/SimpleTabs.tsx +1 -1
  88. package/src/index.ts +0 -2
  89. package/src/revision.ts +2 -2
  90. package/src/splash-screen/NewPage.tsx +2 -2
  91. package/src/types.ts +2 -1
  92. package/styles.css +0 -2
  93. package/dist/fonts/index.d.ts +0 -4
  94. package/dist/fonts/index.js +0 -9
  95. package/dist/fonts/index.js.map +0 -1
  96. package/src/fonts/Geist-Black.woff2 +0 -0
  97. package/src/fonts/Geist-Bold.woff2 +0 -0
  98. package/src/fonts/Geist-ExtraBold.woff2 +0 -0
  99. package/src/fonts/Geist-ExtraLight.woff2 +0 -0
  100. package/src/fonts/Geist-Light.woff2 +0 -0
  101. package/src/fonts/Geist-Medium.woff2 +0 -0
  102. package/src/fonts/Geist-Regular.woff2 +0 -0
  103. package/src/fonts/Geist-SemiBold.woff2 +0 -0
  104. package/src/fonts/Geist-Thin.woff2 +0 -0
  105. package/src/fonts/Geist[wght].woff2 +0 -0
  106. package/src/fonts/index.ts +0 -10
@@ -3,6 +3,7 @@ import React, { useState, useRef, useEffect } from "react";
3
3
  import { SimpleIconButton } from "../ui/SimpleIconButton";
4
4
  import { Plus, X, History, MoreVertical, Trash } from "lucide-react";
5
5
  import { cn } from "../../lib/utils";
6
+ import { MenuItem } from "../../config/types";
6
7
 
7
8
  import {
8
9
  Popover,
@@ -139,6 +140,10 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
139
140
  userId: "", // Will be populated from backend if needed
140
141
  updatedDate: new Date().toISOString(),
141
142
  };
143
+
144
+ // Automatically select the new agent
145
+ setActiveAgentIdWithStorage(agentId);
146
+
142
147
  return [...prevAgents, newAgent];
143
148
  }
144
149
  });
@@ -173,7 +178,6 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
173
178
  selectedAgentId = storedAgent.id;
174
179
  } else {
175
180
  // Fall back to the most recently updated agent
176
- console.log("get most recent agent", activeAgentsResult);
177
181
  const mostRecentAgent = getMostRecentAgent(activeAgentsResult);
178
182
  selectedAgentId = mostRecentAgent?.id || null;
179
183
  }
@@ -268,6 +272,19 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
268
272
  setMenuPopoverOpen(false);
269
273
  };
270
274
 
275
+ const getTabsMenuItems = (): MenuItem[] => {
276
+ return [
277
+ {
278
+ id: "close-other",
279
+ label: "Close Other",
280
+ command: async () => {
281
+ await closeOtherAgents();
282
+ },
283
+ disabled: agents.length <= 1,
284
+ },
285
+ ];
286
+ };
287
+
271
288
  const openAgentFromHistory = async (agent: Agent) => {
272
289
  // Check if this agent is already open as a terminal
273
290
  const existingAgent = agents.find((a) => a.id === agent.id);
@@ -331,6 +348,35 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
331
348
  : "hover:bg-gray-100",
332
349
  )}
333
350
  onClick={() => setActiveAgentIdWithStorage(agent.id)}
351
+ onContextMenu={(e) => {
352
+ e.preventDefault();
353
+ e.stopPropagation();
354
+ // Capture coordinates before state update to avoid pooled/react event issues
355
+ const contextEvent = {
356
+ clientX: e.clientX,
357
+ clientY: e.clientY,
358
+ pageX: e.pageX,
359
+ pageY: e.pageY,
360
+ screenX: (e as any).screenX,
361
+ screenY: (e as any).screenY,
362
+ button: 2,
363
+ buttons: 2,
364
+ ctrlKey: e.ctrlKey,
365
+ shiftKey: e.shiftKey,
366
+ altKey: e.altKey,
367
+ preventDefault: () => {},
368
+ } as any;
369
+
370
+ // Show the menu first at the correct position
371
+ setTimeout(() => {
372
+ editContext?.showContextMenu(
373
+ contextEvent,
374
+ getTabsMenuItems(),
375
+ );
376
+ // Then update the active tab on the next tick to avoid interfering with positioning
377
+ }, 200);
378
+ setActiveAgentIdWithStorage(agent.id);
379
+ }}
334
380
  >
335
381
  <span className="truncate">{agent.name}</span>
336
382
  {agents.length > 1 && (
@@ -415,13 +461,23 @@ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
415
461
  </PopoverTrigger>
416
462
  <PopoverContent className="w-48 p-0" align="end">
417
463
  <div className="py-1">
418
- <button
419
- onClick={closeOtherAgents}
420
- disabled={agents.length <= 1}
421
- className="w-full px-3 py-2 text-left text-xs hover:bg-gray-50 disabled:cursor-not-allowed disabled:text-gray-400"
422
- >
423
- Close Other
424
- </button>
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
+ )}
425
481
  </div>
426
482
  </PopoverContent>
427
483
  </Popover>
@@ -129,6 +129,7 @@ import { usePageWizard } from "../../page-wizard/usePageWizard";
129
129
  import { requestQuota } from "../services/aiService";
130
130
 
131
131
  import { Shrink, Monitor, Smartphone } from "lucide-react";
132
+ import { Agents } from "../ai/Agents";
132
133
 
133
134
  export type FieldAction = {
134
135
  field: FieldDescriptor;
@@ -363,8 +364,11 @@ export function EditorClient({
363
364
  const [focusFieldComponentId, setFocusFieldComponentId] = useState<string>();
364
365
 
365
366
  const [enableCompletions, setEnableCompletions] = useState(false);
366
- const [showRightSidebar, setShowRightSidebar] = useState(
367
- userPreferences.showRightSidebar ?? true,
367
+ const [showComponentNavigator, setShowComponentNavigator] = useState(
368
+ userPreferences.showComponentNavigator ?? true,
369
+ );
370
+ const [showAgentsPanel, setShowAgentsPanel] = useState(
371
+ userPreferences.showAgentsPanel ?? false,
368
372
  );
369
373
  const [activeEditorTab, setActiveEditorTab] = useState<string | null>(null);
370
374
  const [hideNonEditableComponents, setHideNonEditableComponents] = useState(
@@ -490,14 +494,24 @@ export function EditorClient({
490
494
  setIsTourActive(true);
491
495
  }, [setIsTourActive]);
492
496
 
493
- const handleSetShowRightSidebar = useCallback(
497
+ const handleSetShowComponentNavigator = useCallback(
498
+ (value: boolean | ((prev: boolean) => boolean)) => {
499
+ const newValue =
500
+ typeof value === "function" ? value(showComponentNavigator) : value;
501
+ setShowComponentNavigator(newValue);
502
+ setUserPreferences({ showComponentNavigator: newValue });
503
+ },
504
+ [showComponentNavigator, setShowComponentNavigator, setUserPreferences],
505
+ );
506
+
507
+ const handleSetShowAgentsPanel = useCallback(
494
508
  (value: boolean | ((prev: boolean) => boolean)) => {
495
509
  const newValue =
496
- typeof value === "function" ? value(showRightSidebar) : value;
497
- setShowRightSidebar(newValue);
498
- setUserPreferences({ showRightSidebar: newValue });
510
+ typeof value === "function" ? value(showAgentsPanel) : value;
511
+ setShowAgentsPanel(newValue);
512
+ setUserPreferences({ showAgentsPanel: newValue });
499
513
  },
500
- [showRightSidebar, setShowRightSidebar, setUserPreferences],
514
+ [showAgentsPanel, setShowAgentsPanel, setUserPreferences],
501
515
  );
502
516
 
503
517
  const handleSetHideNonEditableComponents = useCallback(
@@ -1001,6 +1015,10 @@ export function EditorClient({
1001
1015
  const isMobile = useMediaQuery("(max-width: 768px)");
1002
1016
 
1003
1017
  useEffect(() => {
1018
+ // Suppress auto-start tour when `noTour` query parameter is present
1019
+ if (searchParams.get("noTour") !== null) {
1020
+ return;
1021
+ }
1004
1022
  const tour = configuration.activeTour;
1005
1023
  const key =
1006
1024
  tour === "default" ? "editor.tourShown" : "editor.tourShown." + tour;
@@ -1281,7 +1299,10 @@ export function EditorClient({
1281
1299
 
1282
1300
  useEffect(() => {
1283
1301
  // Handle fullscreen on initial load only
1284
- if (isInitialLoad && (searchParams.get("fullscreen") || configuration.forceFullscreen)) {
1302
+ if (
1303
+ isInitialLoad &&
1304
+ (searchParams.get("fullscreen") || configuration.forceFullscreen)
1305
+ ) {
1285
1306
  pageViewContext.setFullscreen(true);
1286
1307
  }
1287
1308
  const handleMessage = (event: MessageEvent) => {
@@ -1295,7 +1316,13 @@ export function EditorClient({
1295
1316
  return () => {
1296
1317
  window.removeEventListener("message", handleMessage);
1297
1318
  };
1298
- }, [isInitialLoad, searchParams, pathname, pageViewContext, configuration.forceFullscreen]);
1319
+ }, [
1320
+ isInitialLoad,
1321
+ searchParams,
1322
+ pathname,
1323
+ pageViewContext,
1324
+ configuration.forceFullscreen,
1325
+ ]);
1299
1326
 
1300
1327
  const loadHistory = useDebouncedCallback(async (item: ItemDescriptor) => {
1301
1328
  const result = await getEditHistory(item);
@@ -1552,8 +1579,11 @@ export function EditorClient({
1552
1579
  );
1553
1580
 
1554
1581
  useEffect(() => {
1555
- if (pageViewContext.fullscreen && !searchParams.get("fullscreen") &&
1556
- !configuration.forceFullscreen)
1582
+ if (
1583
+ pageViewContext.fullscreen &&
1584
+ !searchParams.get("fullscreen") &&
1585
+ !configuration.forceFullscreen
1586
+ )
1557
1587
  setShowFullscreenHint(true);
1558
1588
  }, [pageViewContext.fullscreen, configuration.forceFullscreen]);
1559
1589
 
@@ -2419,8 +2449,10 @@ export function EditorClient({
2419
2449
  setShowSuggestedEditsDiff,
2420
2450
  enableCompletions,
2421
2451
  setEnableCompletions,
2422
- showRightSidebar,
2423
- setShowRightSidebar: handleSetShowRightSidebar,
2452
+ showComponentNavigator,
2453
+ setShowComponentNavigator: handleSetShowComponentNavigator,
2454
+ showAgentsPanel,
2455
+ setShowAgentsPanel: handleSetShowAgentsPanel,
2424
2456
  activeEditorTab,
2425
2457
  setActiveEditorTab,
2426
2458
  hideNonEditableComponents,
@@ -2514,8 +2546,10 @@ export function EditorClient({
2514
2546
  setShowSuggestedEdits,
2515
2547
  showSuggestedEditsDiff,
2516
2548
  setShowSuggestedEditsDiff,
2517
- showRightSidebar,
2518
- handleSetShowRightSidebar,
2549
+ showComponentNavigator,
2550
+ handleSetShowComponentNavigator,
2551
+ showAgentsPanel,
2552
+ handleSetShowAgentsPanel,
2519
2553
  activeEditorTab,
2520
2554
  setActiveEditorTab,
2521
2555
  hideNonEditableComponents,
@@ -2791,7 +2825,7 @@ export function EditorClient({
2791
2825
  />
2792
2826
  {/* Control buttons in top right corner */}
2793
2827
  <div className="fixed top-4 right-4 z-[9999] flex gap-2">
2794
- {/* Device toggle button */}
2828
+ {/* Device toggle button */}
2795
2829
  <button
2796
2830
  onClick={() => {
2797
2831
  const currentDevice = pageViewContext.device;
@@ -2807,8 +2841,17 @@ export function EditorClient({
2807
2841
  }
2808
2842
  }}
2809
2843
  className="flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-black/20 text-white backdrop-blur-sm transition-colors duration-200 hover:bg-black/40"
2810
- aria-label={pageViewContext.device === "desktop" ? "Switch to mobile view" : "Switch to desktop view"}
2811
- title={pageViewContext.device === "desktop" ? "Switch to mobile view" : "Switch to desktop view"}
2844
+ aria-label={
2845
+ pageViewContext.device === "desktop"
2846
+ ? "Switch to mobile view"
2847
+ : "Switch to desktop view"
2848
+ }
2849
+ title={
2850
+ pageViewContext.device === "desktop"
2851
+ ? "Switch to mobile view"
2852
+ : "Switch to desktop view"
2853
+ }
2854
+ data-testid="fullscreen-device-toggle"
2812
2855
  >
2813
2856
  {pageViewContext.device === "desktop" ? (
2814
2857
  <Smartphone className="h-5 w-5" />
@@ -2816,17 +2859,18 @@ export function EditorClient({
2816
2859
  <Monitor className="h-5 w-5" />
2817
2860
  )}
2818
2861
  </button>
2819
-
2862
+
2820
2863
  {/* Exit fullscreen button */}
2821
2864
  {!configuration.forceFullscreen && (
2822
- <button
2823
- onClick={() => pageViewContext.setFullscreen(false)}
2824
- className="flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-black/20 text-white backdrop-blur-sm transition-colors duration-200 hover:bg-black/40"
2825
- aria-label="Exit fullscreen"
2826
- title="Return to normal view"
2827
- >
2828
- <Shrink className="h-5 w-5" />
2829
- </button>
2865
+ <button
2866
+ onClick={() => pageViewContext.setFullscreen(false)}
2867
+ className="flex h-10 w-10 cursor-pointer items-center justify-center rounded-full bg-black/20 text-white backdrop-blur-sm transition-colors duration-200 hover:bg-black/40"
2868
+ aria-label="Exit fullscreen"
2869
+ title="Return to normal view"
2870
+ data-testid="fullscreen-exit-button"
2871
+ >
2872
+ <Shrink className="h-5 w-5" />
2873
+ </button>
2830
2874
  )}
2831
2875
  </div>
2832
2876
  </div>
@@ -2842,6 +2886,7 @@ export function EditorClient({
2842
2886
  setShowFullscreenHint(false);
2843
2887
  }, 600);
2844
2888
  }}
2889
+ data-testid="fullscreen-hint-overlay"
2845
2890
  >
2846
2891
  <div className="fixed top-3 left-1/2 -translate-x-1/2 transform rounded-sm bg-gray-200 p-12">
2847
2892
  Press Ctrl + F11 to exit fullscreen mode
@@ -2857,17 +2902,41 @@ export function EditorClient({
2857
2902
  centerPanelView={centerPanelView}
2858
2903
  rightSidebar={
2859
2904
  currentView.rightSidebar &&
2860
- showRightSidebar && (
2905
+ showComponentNavigator && (
2861
2906
  <SidebarView
2862
2907
  sidebar={currentView.rightSidebar}
2863
2908
  editContext={editContext}
2864
2909
  active={true}
2865
2910
  detached={true}
2866
- onClose={() => handleSetShowRightSidebar(false)}
2911
+ onClose={() => handleSetShowComponentNavigator(false)}
2867
2912
  />
2868
2913
  )
2869
2914
  }
2870
2915
  rightSidebarTitle={currentView.rightSidebar?.title}
2916
+ farRightSidebar={
2917
+ showAgentsPanel && (
2918
+ <SidebarView
2919
+ sidebar={{
2920
+ title: "Agents",
2921
+ panels: [
2922
+ {
2923
+ name: "agents",
2924
+ title: "Agents",
2925
+ content: <Agents />,
2926
+ initialSize: 70,
2927
+ noOverflow: true,
2928
+ },
2929
+ ],
2930
+ }}
2931
+ editContext={editContext}
2932
+ active={true}
2933
+ detached={true}
2934
+ paddingRight={true}
2935
+ onClose={() => handleSetShowAgentsPanel(false)}
2936
+ />
2937
+ )
2938
+ }
2939
+ farRightSidebarTitle={"AGENTS"}
2871
2940
  />
2872
2941
 
2873
2942
  {isTourActive && <Tour tourStopCallback={() => setIsTourActive(false)} />}
@@ -322,8 +322,11 @@ export type EditContextType = {
322
322
  getQuotaWarningMessage: () => string | null;
323
323
  isMobile: boolean;
324
324
 
325
- showRightSidebar: boolean;
326
- setShowRightSidebar: React.Dispatch<React.SetStateAction<boolean>>;
325
+ showComponentNavigator: boolean;
326
+ setShowComponentNavigator: React.Dispatch<React.SetStateAction<boolean>>;
327
+
328
+ showAgentsPanel: boolean;
329
+ setShowAgentsPanel: React.Dispatch<React.SetStateAction<boolean>>;
327
330
 
328
331
  activeEditorTab: string | null;
329
332
  setActiveEditorTab: React.Dispatch<React.SetStateAction<string | null>>;
@@ -246,6 +246,7 @@ export const TreeSelector = ({
246
246
  </div>
247
247
  )}
248
248
  <div
249
+ data-testid="media-selector-tree"
249
250
  className={`absolute inset-0 overflow-auto p-2 ${isPopoverOpen ? "pointer-events-none" : ""}`}
250
251
  >
251
252
  <ContentTree
@@ -92,7 +92,10 @@ export function VersionSelector({
92
92
  </PopoverTrigger>
93
93
  <PopoverContent className="w-64 p-0" align="start">
94
94
  <Command>
95
- <CommandList className="max-h-[30vh]">
95
+ <CommandList
96
+ className="max-h-[30vh]"
97
+ data-testid="version-selector-list"
98
+ >
96
99
  <CommandEmpty>No versions available</CommandEmpty>
97
100
  <CommandGroup>
98
101
  {versions.length > 0 && (
@@ -1,6 +1,7 @@
1
1
  import { useEditContext } from "../../client/editContext";
2
2
  import { SimpleIconButton } from "../../ui/SimpleIconButton";
3
3
  import { Layers } from "lucide-react";
4
+ import { SecretAgentIcon } from "../../ui/Icons";
4
5
  import { EnterFullScreenIcon } from "@radix-ui/react-icons";
5
6
  import { CompareIcon } from "../../ui/Icons";
6
7
 
@@ -25,9 +26,21 @@ export function UtilityControls() {
25
26
  label="Component Navigator"
26
27
  size="large"
27
28
  data-testid="component-navigator-button"
28
- selected={editContext.showRightSidebar}
29
+ selected={editContext.showComponentNavigator}
29
30
  onClick={() =>
30
- editContext.setShowRightSidebar(!editContext.showRightSidebar)
31
+ editContext.setShowComponentNavigator(
32
+ !editContext.showComponentNavigator,
33
+ )
34
+ }
35
+ />
36
+ <SimpleIconButton
37
+ icon={<SecretAgentIcon />}
38
+ label="Agents"
39
+ size="large"
40
+ data-testid="agents-panel-button"
41
+ selected={editContext.showAgentsPanel}
42
+ onClick={() =>
43
+ editContext.setShowAgentsPanel(!editContext.showAgentsPanel)
31
44
  }
32
45
  />
33
46
  </>
@@ -12,6 +12,7 @@ import { Spinner } from "../ui/Spinner";
12
12
  import { PageViewContext } from "./pageViewContext";
13
13
  import { SimpleTabs, Tab } from "../ui/SimpleTabs";
14
14
  import { ChevronLeft, X } from "lucide-react";
15
+ import { ComponentTree } from "../sidebar/ComponentTree";
15
16
 
16
17
  export function EditorForm({
17
18
  pageViewContext,
@@ -322,6 +323,18 @@ export function EditorForm({
322
323
  ),
323
324
  id: "advanced",
324
325
  });
326
+ // Component tree tab
327
+ tabPanels.push({
328
+ label: "Components",
329
+ content: (
330
+ <div className="relative h-full">
331
+ <div className="absolute inset-0 overflow-auto">
332
+ <ComponentTree />
333
+ </div>
334
+ </div>
335
+ ),
336
+ id: "tree",
337
+ });
325
338
 
326
339
  return (
327
340
  <div className="flex h-full flex-col" data-testid="editor-sidepanel">
@@ -775,7 +775,8 @@ export function PageViewerFrame({
775
775
  "relative flex h-full w-full flex-col items-center select-none",
776
776
 
777
777
  className,
778
- editContext.showRightSidebar && "pr-0",
778
+ (editContext.showComponentNavigator || editContext.showAgentsPanel) &&
779
+ "pr-0",
779
780
  )}
780
781
  >
781
782
  {!editContext.pageView.fullscreen && (
@@ -7,6 +7,7 @@ import {
7
7
  resolveComment,
8
8
  unresolveComment,
9
9
  } from "../services/reviewsService";
10
+ import { startAgent, StartAgentRequest } from "../services/agentService";
10
11
  import { useDebouncedCallback } from "use-debounce";
11
12
  import { CommentView } from "./CommentView";
12
13
  import { CommentEditor } from "./CommentEditor";
@@ -99,15 +100,70 @@ export function Comment({
99
100
  await unresolveComment(comment);
100
101
  };
101
102
 
102
- const handleAiAction = () => {
103
- // TODO: Implement AI action
104
- // editContext?.showAiPopup(event as any, {
105
- // initialPrompt:
106
- // 'Please help me resolve this comment: "' +
107
- // comment.text +
108
- // '"',
109
- // hiddenSystemPrompt: getHiddenSystemPrompt(comment),
110
- // });
103
+ const handleAiAction = async () => {
104
+ if (!editContext) return;
105
+
106
+ // Respect quota if available
107
+ if ((editContext as any).isQuotaExceeded) {
108
+ editContext.showToast?.(
109
+ "AI quota exceeded. Please try again later or adjust settings.",
110
+ );
111
+ return;
112
+ }
113
+
114
+ const selectedText = (() => {
115
+ if (
116
+ typeof comment.rangeStart === "number" &&
117
+ typeof comment.rangeEnd === "number" &&
118
+ comment.fieldValue
119
+ ) {
120
+ try {
121
+ return comment.fieldValue.substring(
122
+ Math.max(0, comment.rangeStart),
123
+ Math.max(comment.rangeStart, comment.rangeEnd),
124
+ );
125
+ } catch {
126
+ return undefined;
127
+ }
128
+ }
129
+ return undefined;
130
+ })();
131
+
132
+ const promptParts: string[] = [];
133
+ const text = (comment.text || "").trim();
134
+ if (text) promptParts.push(`Comment: "${text}"`);
135
+ if (comment.fieldName) promptParts.push(`Field: ${comment.fieldName}`);
136
+ if (comment.itemName) promptParts.push(`Item: ${comment.itemName}`);
137
+ if (selectedText) promptParts.push(`Selected text: "${selectedText}"`);
138
+
139
+ const initialPrompt =
140
+ promptParts.length > 0
141
+ ? `Please help resolve this review comment. ${promptParts.join(" | ")}`
142
+ : "Please help resolve this review comment.";
143
+
144
+ const agentId = crypto.randomUUID();
145
+
146
+ const request: StartAgentRequest = {
147
+ agentId,
148
+ message: initialPrompt,
149
+ sessionId: editContext.sessionId,
150
+ profileId: "Editor",
151
+ itemid: comment.mainItemId || editContext.currentItemDescriptor?.id || "",
152
+ language: comment.language,
153
+ version: comment.version,
154
+ selectedText: selectedText,
155
+ addContextContent: true,
156
+ addSelectedComponents: false,
157
+ profile: "Editor",
158
+ };
159
+
160
+ try {
161
+ editContext.setShowAgentsPanel(true);
162
+ await startAgent(request);
163
+ } catch (e) {
164
+ console.error("Failed to start agent for comment:", e);
165
+ editContext.showToast?.("Failed to start AI agent for this comment.");
166
+ }
111
167
  };
112
168
 
113
169
  if (isEditing) {
@@ -82,6 +82,7 @@ export function CommentEditor({
82
82
  <div className="space-y-3">
83
83
  <Textarea
84
84
  ref={textareaRef}
85
+ data-testid="add-comment-textarea"
85
86
  value={text}
86
87
  onChange={(e) => setText(e.target.value)}
87
88
  onKeyDown={handleKeyDown}
@@ -129,13 +130,19 @@ export function CommentEditor({
129
130
 
130
131
  {/* Action Buttons */}
131
132
  <div className="flex justify-end gap-2">
132
- <ActionButton variant="outline" onClick={onCancel} disabled={isSaving}>
133
+ <ActionButton
134
+ variant="outline"
135
+ onClick={onCancel}
136
+ disabled={isSaving}
137
+ data-testid="add-comment-cancel"
138
+ >
133
139
  {cancelLabel}
134
140
  </ActionButton>
135
141
  <ActionButton
136
142
  onClick={handleSave}
137
143
  disabled={!text.trim() || isSaving}
138
144
  className="flex items-center gap-1"
145
+ data-testid="add-comment-submit"
139
146
  >
140
147
  {compact && <Send size={14} strokeWidth={1} />}
141
148
  {isSaving ? "Saving..." : submitLabel}
@@ -178,6 +178,7 @@ export function CommentPopover({
178
178
  ref={contentRef}
179
179
  tabIndex={-1}
180
180
  className="w-96 p-4"
181
+ data-testid="add-comment-popover"
181
182
  side="bottom"
182
183
  align="start"
183
184
  onOpenAutoFocus={(e) => {
@@ -81,15 +81,26 @@ export function CommentView({
81
81
  return (
82
82
  <div
83
83
  className={`${compact ? "mt-2" : "mt-3"} flex items-center border-t pt-${compact ? "2" : "3"} text-xs`}
84
+ data-testid="comment-context-info"
84
85
  >
85
86
  {showItemName && (
86
- <div className="text-2xs text-gray-500">{comment.itemName}</div>
87
+ <div
88
+ className="text-2xs text-gray-500"
89
+ data-testid="comment-item-name"
90
+ >
91
+ {comment.itemName}
92
+ </div>
87
93
  )}
88
94
  {showFieldName && showItemName && (
89
95
  <div className="text-2xs mx-2 text-gray-500">&gt;</div>
90
96
  )}
91
97
  {showFieldName && (
92
- <div className="text-2xs text-gray-500">{comment.fieldName}</div>
98
+ <div
99
+ className="text-2xs text-gray-500"
100
+ data-testid="comment-field-name"
101
+ >
102
+ {comment.fieldName}
103
+ </div>
93
104
  )}
94
105
  </div>
95
106
  );
@@ -118,6 +129,7 @@ export function CommentView({
118
129
  <SimpleIconButton
119
130
  icon={<Edit className="h-3.5 w-3.5" strokeWidth={1} />}
120
131
  label="Edit"
132
+ data-testid="comment-action-edit"
121
133
  onClick={onEdit}
122
134
  />
123
135
  )}
@@ -130,11 +142,16 @@ export function CommentView({
130
142
  <button
131
143
  className="hover:bg-gray-5 cursor-pointer rounded-full p-[6px]"
132
144
  title="Delete"
145
+ data-testid="comment-action-delete"
133
146
  >
134
147
  <Trash2 className="h-3.5 w-3.5" strokeWidth={1} />
135
148
  </button>
136
149
  </PopoverTrigger>
137
- <PopoverContent className="w-auto p-2" align="end">
150
+ <PopoverContent
151
+ className="w-auto p-2"
152
+ align="end"
153
+ data-testid="comment-delete-popover"
154
+ >
138
155
  <Button
139
156
  variant="outline"
140
157
  size="sm"
@@ -152,6 +169,7 @@ export function CommentView({
152
169
  <SimpleIconButton
153
170
  icon={<Check className="h-3.5 w-3.5" strokeWidth={1} />}
154
171
  label="Resolve"
172
+ data-testid="comment-action-resolve"
155
173
  onClick={onResolve}
156
174
  />
157
175
  )}
@@ -170,6 +188,7 @@ export function CommentView({
170
188
  formatDate(new Date(comment.resolvedDate!)) +
171
189
  ")"
172
190
  }
191
+ data-testid="comment-resolved-indicator"
173
192
  >
174
193
  <Check
175
194
  className="h-3.5 w-3.5 text-green-500"
@@ -195,6 +214,7 @@ export function CommentView({
195
214
  <SimpleIconButton
196
215
  icon={<Sparkles className="h-3.5 w-3.5" strokeWidth={1} />}
197
216
  label="AI"
217
+ data-testid="comment-action-ai"
198
218
  onClick={onAiAction}
199
219
  />
200
220
  )}
@@ -205,6 +225,7 @@ export function CommentView({
205
225
  {/* Comment Text */}
206
226
  <div
207
227
  className={`${compact ? "text-sm" : "text-sm"} whitespace-pre-wrap text-gray-700`}
228
+ data-testid="comment-text"
208
229
  >
209
230
  {comment.text}
210
231
  </div>