@alpaca-editor/core 1.0.4187 → 1.0.4190

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 (102) hide show
  1. package/dist/agents-view/AgentCard.js +1 -1
  2. package/dist/agents-view/AgentCard.js.map +1 -1
  3. package/dist/agents-view/AgentsView.js +7 -5
  4. package/dist/agents-view/AgentsView.js.map +1 -1
  5. package/dist/components/ui/PlaceholderInput.d.ts +41 -0
  6. package/dist/components/ui/PlaceholderInput.js +160 -0
  7. package/dist/components/ui/PlaceholderInput.js.map +1 -0
  8. package/dist/components/ui/PlaceholderInputTypes.d.ts +41 -0
  9. package/dist/components/ui/PlaceholderInputTypes.js +48 -0
  10. package/dist/components/ui/PlaceholderInputTypes.js.map +1 -0
  11. package/dist/components/ui/PlaceholderItemSelector.d.ts +7 -0
  12. package/dist/components/ui/PlaceholderItemSelector.js +154 -0
  13. package/dist/components/ui/PlaceholderItemSelector.js.map +1 -0
  14. package/dist/config/config.js +7 -14
  15. package/dist/config/config.js.map +1 -1
  16. package/dist/editor/ItemInfo.js +3 -3
  17. package/dist/editor/ItemInfo.js.map +1 -1
  18. package/dist/editor/QuickItemSwitcher.js +1 -1
  19. package/dist/editor/QuickItemSwitcher.js.map +1 -1
  20. package/dist/editor/ai/AgentTerminal.d.ts +7 -1
  21. package/dist/editor/ai/AgentTerminal.js +256 -382
  22. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  23. package/dist/editor/ai/Agents.js +198 -84
  24. package/dist/editor/ai/Agents.js.map +1 -1
  25. package/dist/editor/ai/AiResponseMessage.d.ts +3 -1
  26. package/dist/editor/ai/AiResponseMessage.js +63 -12
  27. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  28. package/dist/editor/ai/ToolCallDisplay.d.ts +2 -1
  29. package/dist/editor/ai/ToolCallDisplay.js +13 -5
  30. package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
  31. package/dist/editor/client/EditorShell.js +6 -5
  32. package/dist/editor/client/EditorShell.js.map +1 -1
  33. package/dist/editor/client/hooks/useSocketMessageHandler.js +6 -4
  34. package/dist/editor/client/hooks/useSocketMessageHandler.js.map +1 -1
  35. package/dist/editor/commands/componentCommands.js +2 -0
  36. package/dist/editor/commands/componentCommands.js.map +1 -1
  37. package/dist/editor/control-center/About.js +1 -1
  38. package/dist/editor/control-center/About.js.map +1 -1
  39. package/dist/editor/control-center/AllAgentsPanel.js +1 -1
  40. package/dist/editor/field-types/MultiLineText.js +1 -1
  41. package/dist/editor/field-types/MultiLineText.js.map +1 -1
  42. package/dist/editor/services/aiService.d.ts +1 -0
  43. package/dist/editor/services/aiService.js.map +1 -1
  44. package/dist/editor/sidebar/Validation.js +1 -1
  45. package/dist/editor/sidebar/Validation.js.map +1 -1
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.js +4 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/page-wizard/PageWizard.js +3 -3
  50. package/dist/page-wizard/PageWizard.js.map +1 -1
  51. package/dist/page-wizard/WizardSteps.js +1 -1
  52. package/dist/page-wizard/WizardSteps.js.map +1 -1
  53. package/dist/revision.d.ts +2 -2
  54. package/dist/revision.js +2 -2
  55. package/dist/splash-screen/ModernSplashScreen.d.ts +8 -0
  56. package/dist/splash-screen/ModernSplashScreen.js +36 -0
  57. package/dist/splash-screen/ModernSplashScreen.js.map +1 -0
  58. package/dist/splash-screen/OpenPage.js +10 -6
  59. package/dist/splash-screen/OpenPage.js.map +1 -1
  60. package/dist/splash-screen/ParheliaAssistantChat.d.ts +8 -0
  61. package/dist/splash-screen/ParheliaAssistantChat.js +155 -0
  62. package/dist/splash-screen/ParheliaAssistantChat.js.map +1 -0
  63. package/dist/splash-screen/RecentAgents.d.ts +7 -0
  64. package/dist/splash-screen/RecentAgents.js +76 -0
  65. package/dist/splash-screen/RecentAgents.js.map +1 -0
  66. package/dist/splash-screen/RecentPages.js +2 -2
  67. package/dist/splash-screen/RecentPages.js.map +1 -1
  68. package/dist/splash-screen/SplashScreen.js +1 -1
  69. package/dist/splash-screen/SplashScreen.js.map +1 -1
  70. package/dist/styles.css +241 -12
  71. package/package.json +1 -1
  72. package/src/agents-view/AgentCard.tsx +1 -6
  73. package/src/agents-view/AgentsView.tsx +18 -30
  74. package/src/components/ui/PlaceholderInput.tsx +290 -0
  75. package/src/components/ui/PlaceholderInputTypes.tsx +97 -0
  76. package/src/components/ui/PlaceholderItemSelector.tsx +253 -0
  77. package/src/config/config.tsx +8 -17
  78. package/src/editor/ItemInfo.tsx +3 -2
  79. package/src/editor/QuickItemSwitcher.tsx +1 -1
  80. package/src/editor/ai/AgentTerminal.tsx +544 -649
  81. package/src/editor/ai/Agents.tsx +464 -250
  82. package/src/editor/ai/AiResponseMessage.tsx +154 -29
  83. package/src/editor/ai/ToolCallDisplay.tsx +18 -4
  84. package/src/editor/client/EditorShell.tsx +9 -6
  85. package/src/editor/client/hooks/useSocketMessageHandler.ts +6 -7
  86. package/src/editor/commands/componentCommands.tsx +1 -0
  87. package/src/editor/control-center/About.tsx +2 -2
  88. package/src/editor/control-center/AllAgentsPanel.tsx +1 -1
  89. package/src/editor/field-types/MultiLineText.tsx +1 -1
  90. package/src/editor/services/aiService.ts +2 -0
  91. package/src/editor/sidebar/Validation.tsx +1 -1
  92. package/src/index.ts +5 -0
  93. package/src/page-wizard/PageWizard.tsx +3 -3
  94. package/src/page-wizard/WizardSteps.tsx +1 -1
  95. package/src/revision.ts +2 -2
  96. package/src/splash-screen/ModernSplashScreen.tsx +158 -0
  97. package/src/splash-screen/OpenPage.tsx +12 -4
  98. package/src/splash-screen/ParheliaAssistantChat.tsx +273 -0
  99. package/src/splash-screen/RecentAgents.tsx +151 -0
  100. package/src/splash-screen/RecentPages.tsx +58 -61
  101. package/src/splash-screen/SplashScreen.tsx +1 -1
  102. package/styles.css +20 -0
@@ -5,6 +5,7 @@ import React, {
5
5
  useCallback,
6
6
  useLayoutEffect,
7
7
  useMemo,
8
+ ViewTransition,
8
9
  } from "react";
9
10
  import {
10
11
  Send,
@@ -28,7 +29,6 @@ import {
28
29
  StartAgentRequest,
29
30
  Agent,
30
31
  AgentDetails,
31
- updateAgentContext,
32
32
  AgentMetadata,
33
33
  updateAgentSettings,
34
34
  updateAgentCostLimit,
@@ -38,21 +38,22 @@ import {
38
38
  import { useEditContext, useFieldsEditContext } from "../client/editContext";
39
39
  import { Textarea } from "../../components/ui/textarea";
40
40
  import { Button } from "../../components/ui/button";
41
+ import { PlaceholderInput } from "../../components/ui/PlaceholderInput";
41
42
  import { AiResponseMessage } from "./AiResponseMessage";
42
43
  import { AgentCostDisplay } from "./AgentCostDisplay";
43
44
  import { Message } from "./types";
44
45
  import { ContextInfoBar } from "./ContextInfoBar";
45
46
  import { getComponentById } from "../componentTreeHelper";
46
- import { Comment } from "../../types";
47
+
47
48
  import { AiProfile } from "../services/aiService";
48
- import { ItemDescriptor } from "../pageModel";
49
+
49
50
  import {
50
51
  Popover,
51
52
  PopoverContent,
52
53
  PopoverTrigger,
53
54
  } from "../../components/ui/popover";
54
55
  import { SecretAgentIcon } from "../ui/Icons";
55
- import { formatTime, formatDateTime } from "../utils";
56
+ import { formatTime } from "../utils";
56
57
  import {
57
58
  Tooltip,
58
59
  TooltipTrigger,
@@ -662,6 +663,12 @@ const convertAgentMessagesToAiFormat = (
662
663
  id: agentMessage.id,
663
664
  content: agentMessage.content,
664
665
  formattedContent: agentMessage.content
666
+ ?.replace(/^######\s+(.+)$/gm, "<h6>$1</h6>")
667
+ ?.replace(/^#####\s+(.+)$/gm, "<h5>$1</h5>")
668
+ ?.replace(/^####\s+(.+)$/gm, "<h4>$1</h4>")
669
+ ?.replace(/^###\s+(.+)$/gm, "<h3>$1</h3>")
670
+ ?.replace(/^##\s+(.+)$/gm, "<h2>$1</h2>")
671
+ ?.replace(/^#\s+(.+)$/gm, "<h1>$1</h1>")
665
672
  ?.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
666
673
  ?.replace(/\n/g, "<br/>"),
667
674
  name: agentMessage.name,
@@ -700,11 +707,23 @@ export function AgentTerminal({
700
707
  initialMetadata,
701
708
  profiles,
702
709
  isActive = true,
710
+ compact = false,
711
+ hideContext = false,
712
+ hideBottomControls = false,
713
+ hideGreeting = false,
714
+ className,
715
+ initialPrompt,
703
716
  }: {
704
717
  agentStub: Agent;
705
718
  initialMetadata?: AgentMetadata;
706
719
  profiles: AiProfile[];
707
720
  isActive?: boolean;
721
+ compact?: boolean;
722
+ hideContext?: boolean;
723
+ hideBottomControls?: boolean;
724
+ hideGreeting?: boolean;
725
+ className?: string;
726
+ initialPrompt?: string;
708
727
  }) {
709
728
  const editContext = useEditContext();
710
729
  const fieldsContext = useFieldsEditContext();
@@ -717,6 +736,10 @@ export function AgentTerminal({
717
736
  const [isLoading, setIsLoading] = useState(false);
718
737
  const [isConnecting, setIsConnecting] = useState(false);
719
738
  const [isSubmitting, setIsSubmitting] = useState(false);
739
+ const [activePlaceholderInput, setActivePlaceholderInput] = useState<{
740
+ text: string;
741
+ behavior?: "compose" | "submit";
742
+ } | null>(null);
720
743
  const [agentMetadata, setAgentMetadata] = useState<AgentMetadata | null>(
721
744
  null,
722
745
  );
@@ -945,6 +968,13 @@ export function AgentTerminal({
945
968
  }
946
969
  }, []);
947
970
 
971
+ // Auto-focus terminal input on mount
972
+ useEffect(() => {
973
+ if (textareaRef.current) {
974
+ textareaRef.current.focus();
975
+ }
976
+ }, []);
977
+
948
978
  // Start voice recognition
949
979
  const startVoice = useCallback(() => {
950
980
  try {
@@ -1570,9 +1600,6 @@ export function AgentTerminal({
1570
1600
  const loadAgent = useCallback(async () => {
1571
1601
  try {
1572
1602
  if (agentStub.status === "new") {
1573
- // Set agent ID immediately for new agents
1574
- (window as any).currentAgentId = agentStub.id;
1575
- // Set currentAgentId for new agent
1576
1603
  // Derive initial profile from provided metadata if present
1577
1604
  const initialProfileIdFromMeta = (() => {
1578
1605
  try {
@@ -1790,9 +1817,6 @@ export function AgentTerminal({
1790
1817
  return merged;
1791
1818
  });
1792
1819
 
1793
- // Set agent ID for existing agents too
1794
- (window as any).currentAgentId = agentData.id;
1795
-
1796
1820
  // Check if cost limit was exceeded (detect from existing messages)
1797
1821
  try {
1798
1822
  const costLimitMessage = (agentData.messages || []).find(
@@ -2390,17 +2414,20 @@ export function AgentTerminal({
2390
2414
 
2391
2415
  // Profiles are provided by parent component (Agents). No local loading here.
2392
2416
 
2393
- // Select active profile based on agent.profileId or default to first
2417
+ // Select active profile based on agent.profileId or agentStub.profileId
2394
2418
  useEffect(() => {
2395
2419
  if (!profiles || profiles.length === 0) return;
2396
- const candidate = agent?.profileId
2397
- ? (profiles.find((p) => p.id === (agent?.profileId as any)) ??
2398
- profiles[0])
2420
+ // For new agents, use agentStub.profileId; for loaded agents, use agent.profileId
2421
+ const profileIdToUse = agent?.profileId || agentStub.profileId;
2422
+
2423
+ const candidate = profileIdToUse
2424
+ ? (profiles.find((p) => p.id === profileIdToUse) ?? profiles[0])
2399
2425
  : profiles[0];
2426
+
2400
2427
  if (candidate && (!activeProfile || activeProfile.id !== candidate.id)) {
2401
2428
  setActiveProfile(candidate);
2402
2429
  }
2403
- }, [profiles, agent?.profileId]);
2430
+ }, [profiles, agent?.profileId, agentStub.profileId]);
2404
2431
 
2405
2432
  // Update selected model when the active profile or agent model changes
2406
2433
  useEffect(() => {
@@ -2486,12 +2513,13 @@ export function AgentTerminal({
2486
2513
  }, [agent?.id]);
2487
2514
 
2488
2515
  const handleSubmit = async () => {
2489
- if (!prompt.trim() || isSubmitting || !editContext) return;
2516
+ if (isSubmitting || !editContext) return;
2490
2517
 
2491
2518
  try {
2492
2519
  setIsSubmitting(true);
2493
2520
  setError(null);
2494
- const agentId = agent?.id;
2521
+ // For new agents, use agentStub.id; for existing agents, use agent.id
2522
+ const agentId = agent?.id || agentStub.id;
2495
2523
 
2496
2524
  if (!agentId) return;
2497
2525
 
@@ -2520,7 +2548,7 @@ export function AgentTerminal({
2520
2548
  // This ensures all tabs (including the sending tab) have the same messageId from the database
2521
2549
 
2522
2550
  const request: StartAgentRequest = {
2523
- agentId: agent.id,
2551
+ agentId: agentId,
2524
2552
  message: prompt.trim(),
2525
2553
  sessionId: editContext.sessionId,
2526
2554
  profileId: activeProfile?.id || profiles[0]?.id || "",
@@ -2698,276 +2726,37 @@ export function AgentTerminal({
2698
2726
  }
2699
2727
  };
2700
2728
 
2701
- // Context info bar helpers - must be declared before any early returns to keep hooks order stable
2702
- const removeContextKey = useCallback(
2703
- async (
2704
- key: "items" | "componentIds" | "field" | "comment",
2705
- index?: number,
2706
- ) => {
2707
- if (!agent?.id) return;
2708
- const current = agentMetadata || {};
2709
- // Exclude top-level context to avoid duplicate keys when spreading
2710
- const currentWithoutContext = { ...current };
2711
- delete (currentWithoutContext as any).context;
2712
- const next: AgentMetadata = {
2713
- ...currentWithoutContext,
2714
- additionalData: {
2715
- ...(current.additionalData || {}),
2716
- context: {
2717
- ...((current.additionalData &&
2718
- (current.additionalData as any).context) ||
2719
- {}),
2720
- },
2721
- },
2722
- } as AgentMetadata;
2723
-
2724
- if (next.additionalData && (next.additionalData as any).context) {
2725
- const ctx = (next.additionalData as any).context;
2726
- if (key === "items" && typeof index === "number" && ctx.items) {
2727
- ctx.items.splice(index, 1);
2728
- if (ctx.items.length === 0) delete ctx.items;
2729
- } else if (
2730
- key === "componentIds" &&
2731
- typeof index === "number" &&
2732
- ctx.componentIds
2733
- ) {
2734
- ctx.componentIds.splice(index, 1);
2735
- if (ctx.componentIds.length === 0) delete ctx.componentIds;
2736
- } else if (key === "field") {
2737
- delete ctx.field;
2738
- } else if (key === "comment") {
2739
- delete ctx.comment;
2740
- }
2741
- }
2742
-
2743
- try {
2744
- // For new (not yet persisted) agents, only update local state
2745
- if (agent.status === "new") {
2746
- setAgentMetadata(next);
2747
- return;
2748
- }
2749
-
2750
- // Persisted agents: update server and local cache
2751
- await updateAgentContext(agent.id, next);
2752
- setAgentMetadata(next);
2753
- setAgent((prev) =>
2754
- prev ? { ...prev, metadata: JSON.stringify(next) } : prev,
2755
- );
2756
- } catch (e) {
2757
- console.error("Failed to update agent metadata", e);
2758
- }
2759
- },
2760
- [agent?.id, agentMetadata],
2761
- );
2762
-
2763
- const addPagesToContext = async () => {
2764
- if (!agent?.id || !editContext?.currentItemDescriptor) return;
2765
-
2766
- // Add the current page (currentItemDescriptor represents the current page)
2767
- // Note: Currently only supports adding the current page. To support multiple pages,
2768
- // we'd need a page selection mechanism in the UI (e.g., a page picker dialog)
2769
- const item = editContext.currentItemDescriptor;
2770
- const pageToAdd = {
2771
- id: item.id,
2772
- language: item.language,
2773
- version: item.version,
2774
- name: editContext.contentEditorItem?.name,
2775
- path: undefined,
2776
- };
2777
-
2778
- const current = agentMetadata || {};
2779
- const currentPages =
2780
- ((current.additionalData as any)?.context?.items as Array<any>) || [];
2781
-
2782
- // Check if this page is already in context
2783
- const existingPageIds = new Set(
2784
- currentPages.map((p: any) => `${p.id}-${p.language}-${p.version}`),
2785
- );
2786
-
2729
+ // Auto-send initial prompt if provided and agent has no messages yet
2730
+ const initialPromptSentRef = useRef(false);
2731
+ useEffect(() => {
2787
2732
  if (
2788
- existingPageIds.has(
2789
- `${pageToAdd.id}-${pageToAdd.language}-${pageToAdd.version}`,
2790
- )
2733
+ initialPrompt !== undefined &&
2734
+ !isLoading &&
2735
+ !initialPromptSentRef.current &&
2736
+ messages.length === 0 &&
2737
+ agentStub.id &&
2738
+ editContext &&
2739
+ activeProfile && // MUST have activeProfile set, not just profiles.length
2740
+ profiles.length > 0
2791
2741
  ) {
2792
- return; // Page already exists
2793
- }
2794
-
2795
- // Exclude top-level context to avoid duplicate keys when spreading
2796
- const currentWithoutContext = { ...current };
2797
- delete (currentWithoutContext as any).context;
2798
- const next: AgentMetadata = {
2799
- ...currentWithoutContext,
2800
- additionalData: {
2801
- ...(current.additionalData || {}),
2802
- context: {
2803
- ...((current.additionalData &&
2804
- (current.additionalData as any).context) ||
2805
- {}),
2806
- items: [...currentPages, pageToAdd],
2807
- },
2808
- },
2809
- } as AgentMetadata;
2810
-
2811
- try {
2812
- if (agent.status === "new") {
2813
- setAgentMetadata(next);
2814
- return;
2815
- }
2816
- await updateAgentContext(agent.id, next);
2817
- setAgentMetadata(next);
2818
- setAgent((prev) =>
2819
- prev ? { ...prev, metadata: JSON.stringify(next) } : prev,
2820
- );
2821
- } catch (e) {
2822
- console.error("Failed to update agent metadata (add pages)", e);
2823
- }
2824
- };
2825
-
2826
- const addSelectedComponentsToContext = async () => {
2827
- if (!agent?.id || !editContext?.selection?.length) return;
2828
-
2829
- const current = agentMetadata || {};
2830
- const currentComponentIds =
2831
- ((current.additionalData as any)?.context?.componentIds as string[]) ||
2832
- [];
2833
-
2834
- // Merge with existing components, avoiding duplicates
2835
- const existingIds = new Set(currentComponentIds);
2836
- const newComponentIds = editContext.selection.filter(
2837
- (id) => !existingIds.has(id),
2838
- );
2839
-
2840
- if (newComponentIds.length === 0) return; // No new components to add
2841
-
2842
- // Exclude top-level context to avoid duplicate keys when spreading
2843
- const currentWithoutContext = { ...current };
2844
- delete (currentWithoutContext as any).context;
2845
- const next: AgentMetadata = {
2846
- ...currentWithoutContext,
2847
- additionalData: {
2848
- ...(current.additionalData || {}),
2849
- context: {
2850
- ...((current.additionalData &&
2851
- (current.additionalData as any).context) ||
2852
- {}),
2853
- componentIds: [...currentComponentIds, ...newComponentIds],
2854
- },
2855
- },
2856
- } as AgentMetadata;
2857
-
2858
- try {
2859
- if (agent.status === "new") {
2860
- setAgentMetadata(next);
2861
- return;
2862
- }
2863
- await updateAgentContext(agent.id, next);
2864
- setAgentMetadata(next);
2865
- setAgent((prev) =>
2866
- prev ? { ...prev, metadata: JSON.stringify(next) } : prev,
2867
- );
2868
- } catch (e) {
2869
- console.error("Failed to update agent metadata (add components)", e);
2870
- }
2871
- };
2872
-
2873
- const addComponentIdsToContext = async (ids: string[]) => {
2874
- if (!agent?.id || !ids?.length) return;
2875
-
2876
- const current = agentMetadata || {};
2877
- const currentComponentIds =
2878
- ((current.additionalData as any)?.context?.componentIds as string[]) ||
2879
- [];
2880
-
2881
- // Merge with existing components, avoiding duplicates
2882
- const existingIds = new Set(currentComponentIds);
2883
- const newComponentIds = ids.filter((id) => !!id && !existingIds.has(id));
2884
- if (newComponentIds.length === 0) return;
2885
-
2886
- // Exclude top-level context to avoid duplicate keys when spreading
2887
- const currentWithoutContext = { ...current };
2888
- delete (currentWithoutContext as any).context;
2889
- const next: AgentMetadata = {
2890
- ...currentWithoutContext,
2891
- additionalData: {
2892
- ...(current.additionalData || {}),
2893
- context: {
2894
- ...((current.additionalData &&
2895
- (current.additionalData as any).context) ||
2896
- {}),
2897
- componentIds: [...currentComponentIds, ...newComponentIds],
2898
- },
2899
- },
2900
- } as AgentMetadata;
2901
-
2902
- try {
2903
- if (agent.status === "new") {
2904
- setAgentMetadata(next);
2905
- return;
2906
- }
2907
- await updateAgentContext(agent.id, next);
2908
- setAgentMetadata(next);
2909
- setAgent((prev) =>
2910
- prev ? { ...prev, metadata: JSON.stringify(next) } : prev,
2911
- );
2912
- } catch (e) {
2913
- console.error("Failed to update agent metadata (add components)", e);
2742
+ initialPromptSentRef.current = true;
2743
+ // Delay slightly to ensure all state is ready
2744
+ setTimeout(() => {
2745
+ setPrompt(initialPrompt);
2746
+ // Trigger submit programmatically
2747
+ handleSubmit();
2748
+ }, 200);
2914
2749
  }
2915
- };
2916
-
2917
- const addPagesToContextFromItems = async (items: ItemDescriptor[]) => {
2918
- if (!agent?.id || !items?.length) return;
2919
-
2920
- const current = agentMetadata || {};
2921
- const currentPages =
2922
- ((current.additionalData as any)?.context?.items as Array<any>) || [];
2923
-
2924
- const existingPageIds = new Set(
2925
- currentPages.map((p: any) => `${p.id}-${p.language}-${p.version}`),
2926
- );
2927
-
2928
- const pagesToAdd = items
2929
- .filter((it) => !!it?.id)
2930
- .map((it) => ({
2931
- id: it.id,
2932
- language: it.language,
2933
- version: it.version,
2934
- }))
2935
- .filter(
2936
- (p) => !existingPageIds.has(`${p.id}-${p.language}-${p.version}`),
2937
- );
2938
-
2939
- if (pagesToAdd.length === 0) return;
2940
-
2941
- // Exclude top-level context to avoid duplicate keys when spreading
2942
- const currentWithoutContext = { ...current };
2943
- delete (currentWithoutContext as any).context;
2944
- const next: AgentMetadata = {
2945
- ...currentWithoutContext,
2946
- additionalData: {
2947
- ...(current.additionalData || {}),
2948
- context: {
2949
- ...((current.additionalData &&
2950
- (current.additionalData as any).context) ||
2951
- {}),
2952
- items: [...currentPages, ...pagesToAdd],
2953
- },
2954
- },
2955
- } as AgentMetadata;
2956
-
2957
- try {
2958
- if (agent.status === "new") {
2959
- setAgentMetadata(next);
2960
- return;
2961
- }
2962
- await updateAgentContext(agent.id, next);
2963
- setAgentMetadata(next);
2964
- setAgent((prev) =>
2965
- prev ? { ...prev, metadata: JSON.stringify(next) } : prev,
2966
- );
2967
- } catch (e) {
2968
- console.error("Failed to update agent metadata (add items/pages)", e);
2969
- }
2970
- };
2750
+ }, [
2751
+ initialPrompt,
2752
+ isLoading,
2753
+ messages.length,
2754
+ agentStub.id,
2755
+ editContext,
2756
+ activeProfile,
2757
+ profiles.length,
2758
+ handleSubmit,
2759
+ ]);
2971
2760
 
2972
2761
  // Resolve display names when metadata or editor state changes
2973
2762
  useEffect(() => {
@@ -3215,9 +3004,12 @@ export function AgentTerminal({
3215
3004
  );
3216
3005
  };
3217
3006
 
3007
+
3008
+
3218
3009
  return (
3219
- <div className="flex h-full flex-col">
3010
+ <div className={`flex h-full flex-col ${className || ""}`}>
3220
3011
  {/* Messages */}
3012
+
3221
3013
  <div
3222
3014
  ref={messagesContainerRef}
3223
3015
  className="flex-1 overflow-y-auto"
@@ -3238,42 +3030,50 @@ export function AgentTerminal({
3238
3030
  </div>
3239
3031
  )}
3240
3032
 
3241
- {messages.length === 0 && !error && (
3033
+ {messages.length === 0 && !error && !hideGreeting && (
3242
3034
  <div className="flex h-full items-center justify-center p-8">
3243
3035
  <div className="max-w-prose text-center">
3244
- {activeProfile?.svgIcon ? (
3245
- <div
3246
- className="mx-auto mb-4 flex h-24 w-24 items-center justify-center text-gray-400 [&>svg]:h-full [&>svg]:w-full"
3247
- dangerouslySetInnerHTML={{
3248
- __html: activeProfile.svgIcon,
3249
- }}
3250
- />
3251
- ) : (
3252
- <SecretAgentIcon
3253
- size={96}
3254
- strokeWidth={1}
3255
- className="mx-auto mb-4 text-gray-400"
3256
- />
3257
- )}
3258
- {activeProfile?.greetingMessage ? (
3259
- <div
3260
- className="prose prose-sm mx-auto text-center"
3261
- dangerouslySetInnerHTML={{
3262
- __html: activeProfile.greetingMessage,
3263
- }}
3264
- />
3036
+ {!activeProfile ? (
3037
+ <Loader2 className="h-8 w-8 animate-spin text-gray-400 mx-auto" />
3265
3038
  ) : (
3266
3039
  <>
3267
- <h3 className="mb-2 text-lg font-medium text-gray-900">
3268
- Start a conversation
3269
- </h3>
3270
- <p className="mb-4 text-sm text-gray-500">
3271
- Send a message to begin working with your AI agent.
3272
- </p>
3273
- <div className="text-xs text-gray-400">
3274
- Your agent can help with content editing, research, and
3275
- automation tasks.
3276
- </div>
3040
+ {activeProfile.svgIcon ? (
3041
+ <div
3042
+ className="mx-auto mb-4 flex h-24 w-24 items-center justify-center text-gray-400 [&>svg]:h-full [&>svg]:w-full"
3043
+ dangerouslySetInnerHTML={{
3044
+ __html: activeProfile.svgIcon,
3045
+ }}
3046
+ />
3047
+ ) : (
3048
+ <SecretAgentIcon
3049
+ size={96}
3050
+ strokeWidth={1}
3051
+ className="mx-auto mb-4 text-gray-400"
3052
+ />
3053
+ )}
3054
+ {activeProfile.greetingMessage ? (
3055
+ <ViewTransition>
3056
+ <div
3057
+ className="prose prose-sm mx-auto text-center"
3058
+ dangerouslySetInnerHTML={{
3059
+ __html: activeProfile.greetingMessage,
3060
+ }}
3061
+ />
3062
+ </ViewTransition>
3063
+ ) : (
3064
+ <>
3065
+ <h3 className="mb-2 text-lg font-medium text-gray-900">
3066
+ Start a conversation
3067
+ </h3>
3068
+ <p className="mb-4 text-sm text-gray-500">
3069
+ Send a message to begin working with your AI agent.
3070
+ </p>
3071
+ <div className="text-xs text-gray-400">
3072
+ Your agent can help with content editing, research, and
3073
+ automation tasks.
3074
+ </div>
3075
+ </>
3076
+ )}
3277
3077
  </>
3278
3078
  )}
3279
3079
  </div>
@@ -3319,6 +3119,8 @@ export function AgentTerminal({
3319
3119
  editOperations={[]}
3320
3120
  error={error || undefined}
3321
3121
  profileSvgIcon={activeProfile?.svgIcon}
3122
+ agentId={agent?.id || agentStub.id}
3123
+ agentName={activeProfile?.name}
3322
3124
  onQuickAction={(action) => {
3323
3125
  const text = (
3324
3126
  action.prompt ||
@@ -3327,6 +3129,19 @@ export function AgentTerminal({
3327
3129
  ""
3328
3130
  ).trim();
3329
3131
  if (!text) return;
3132
+
3133
+ // Check if text contains placeholders ({placeholder} or <placeholder>)
3134
+ const hasPlaceholders = /\{([^}]+)\}|<([^>]+)>/.test(text);
3135
+
3136
+ if (hasPlaceholders) {
3137
+ // Show placeholder input
3138
+ setActivePlaceholderInput({
3139
+ text,
3140
+ behavior: action.behavior,
3141
+ });
3142
+ return;
3143
+ }
3144
+
3330
3145
  if (action.behavior === "compose") {
3331
3146
  setPrompt(text);
3332
3147
  setInputPlaceholder(
@@ -3365,361 +3180,441 @@ export function AgentTerminal({
3365
3180
  </div>
3366
3181
 
3367
3182
  {/* Context Info Bar */}
3368
- {renderContextInfoBar()}
3183
+ {!hideContext && renderContextInfoBar()}
3369
3184
 
3370
3185
  {/* Todo List Panel */}
3371
- <TodoListPanel messages={messages} agentMetadata={agentMetadata} />
3186
+ {!hideContext && (
3187
+ <TodoListPanel messages={messages} agentMetadata={agentMetadata} />
3188
+ )}
3372
3189
 
3373
3190
  {/* Input */}
3374
3191
  <div className="border-t border-gray-200 p-4">
3375
- <div className="flex items-stretch gap-2">
3376
- <Textarea
3377
- ref={textareaRef}
3378
- value={prompt}
3379
- onChange={(e) => {
3380
- setPrompt(e.target.value);
3381
- // Reset history index when user starts typing
3382
- if (currentHistoryIndex !== -1) {
3383
- setCurrentHistoryIndex(-1);
3192
+ {activePlaceholderInput ? (
3193
+ // Placeholder Input (from quick actions)
3194
+ <PlaceholderInput
3195
+ text={activePlaceholderInput.text}
3196
+ showButtons={false}
3197
+ onComplete={(filledText) => {
3198
+ setActivePlaceholderInput(null);
3199
+
3200
+ if (activePlaceholderInput.behavior === "compose") {
3201
+ setPrompt(filledText);
3202
+ setInputPlaceholder("Review and edit, then press Enter to send");
3203
+ if (textareaRef.current) {
3204
+ try {
3205
+ textareaRef.current.focus();
3206
+ const v = textareaRef.current.value || "";
3207
+ textareaRef.current.selectionStart = v.length;
3208
+ textareaRef.current.selectionEnd = v.length;
3209
+ } catch {}
3210
+ }
3211
+ } else {
3212
+ // Submit behavior or default
3213
+ if (isExecuting) {
3214
+ try {
3215
+ handleStop();
3216
+ } catch {}
3217
+ }
3218
+ sendQuickMessage(filledText);
3384
3219
  }
3385
3220
  }}
3386
- onKeyDown={handleKeyPress}
3387
- placeholder={inputPlaceholder}
3388
- className="h-[80px] flex-1 resize-none overflow-y-auto text-xs"
3389
- data-testid="agent-terminal-prompt"
3390
- disabled={isSubmitting}
3221
+ onCancel={() => {
3222
+ setActivePlaceholderInput(null);
3223
+ }}
3391
3224
  />
3392
- </div>
3393
- <div className="flex items-stretch justify-between gap-2">
3394
- {/* Profile/Model selectors below the input */}
3395
- <div className="mt-2 flex flex-wrap items-center justify-start gap-2">
3396
- <Tooltip delayDuration={400}>
3397
- <TooltipTrigger asChild>
3398
- <select
3399
- className={`h-5 rounded border px-1.5 text-[10px] ${
3400
- mode === "read-only"
3401
- ? "border-green-300 bg-green-50 text-green-700"
3402
- : mode === "supervised"
3403
- ? "border-amber-300 bg-amber-50 text-amber-700"
3404
- : "border-red-300 bg-red-50 text-red-700"
3405
- }`}
3406
- value={mode}
3407
- onChange={async (e) => {
3408
- const nextMode =
3409
- (e.target.value as AgentMode) || "supervised";
3410
- // Optimistic UI update
3411
- setMode(nextMode);
3412
- const current = agentMetadata || ({} as AgentMetadata);
3413
- const nextMeta: AgentMetadata = {
3414
- ...current,
3415
- mode: nextMode,
3416
- } as AgentMetadata;
3417
- try {
3418
- if (!agent?.id || agent.status === "new") {
3419
- setAgentMetadata(nextMeta);
3420
- // Cache until first start when agent is persisted
3421
- pendingSettingsRef.current = {
3422
- ...(pendingSettingsRef.current || {}),
3423
- mode: nextMode,
3424
- };
3425
- return;
3426
- }
3427
- await updateAgentSettings(agent.id, { mode: nextMode });
3428
- setAgentMetadata(nextMeta);
3429
- setAgent((prev) =>
3430
- prev
3431
- ? { ...prev, metadata: JSON.stringify(nextMeta) }
3432
- : prev,
3433
- );
3434
- } catch (e2) {
3435
- console.error("Failed to persist mode change", e2);
3436
- }
3437
- }}
3438
- title="Mode"
3439
- aria-label="Mode"
3440
- data-testid="agent-mode-select"
3441
- >
3442
- <option value="supervised">Supervised</option>
3443
- <option value="autonomous">Autonomous</option>
3444
- <option value="read-only">Read-Only</option>
3445
- </select>
3446
- </TooltipTrigger>
3447
- <TooltipContent side="top" sideOffset={6}>
3448
- <div className="max-w-[320px] space-y-1">
3449
- <div>
3450
- <span className="font-semibold text-green-500">
3451
- Read-Only
3452
- </span>
3453
- : Limited tool access as configured by the profile (Ask Mode
3454
- Tools).
3455
- </div>
3456
- <div>
3457
- <span className="font-semibold text-amber-500">
3458
- Supervised
3459
- </span>
3460
- : Full tool access, but writes are limited to pages/items in
3461
- the current context. Creating new items or updating existing
3462
- items outside the current context requires explicit
3463
- approval.
3464
- </div>
3465
- <div>
3466
- <span className="font-semibold text-red-500">
3467
- Autonomous
3468
- </span>
3469
- : Full tool access; can write across the site/project only
3470
- limited by user permissions.
3471
- </div>
3472
- </div>
3473
- </TooltipContent>
3474
- </Tooltip>
3475
- {profiles?.length > 0 && (
3476
- <select
3477
- className="h-5 rounded border px-1.5 text-[10px] text-gray-500"
3478
- value={activeProfile?.id || ""}
3479
- onChange={async (e) => {
3480
- const nextProfile = profiles.find(
3481
- (x) => x.id === e.target.value,
3482
- );
3483
- if (!nextProfile) return;
3484
- setActiveProfile(nextProfile);
3225
+ ) : prompt && /\{([^}]+)\}|<([^>]+)>/.test(prompt) ? (
3226
+ // Template mode: show PlaceholderInput when prompt contains placeholders
3227
+ <PlaceholderInput
3228
+ text={prompt}
3229
+ showButtons={false}
3230
+ onComplete={(filledText) => {
3231
+ setPrompt(filledText);
3232
+ // Auto-submit after filling placeholders
3233
+ if (filledText.trim()) {
3234
+ if (isExecuting) {
3485
3235
  try {
3486
- if (agent?.id && agent.status !== "new") {
3487
- await updateAgentSettings(agent.id, {
3488
- profileId: nextProfile.id,
3489
- profileName: nextProfile.name,
3490
- });
3491
- } else {
3492
- // cache until first start
3493
- pendingSettingsRef.current = {
3494
- ...(pendingSettingsRef.current || {}),
3495
- // we cache profile by updating local metadata
3496
- };
3497
- setAgentMetadata((current) => {
3498
- const next = { ...(current || {}) } as any;
3499
- next.profile = nextProfile.name;
3500
- next.additionalData = {
3501
- ...(next.additionalData || {}),
3502
- profileId: nextProfile.id,
3503
- profileName: nextProfile.name,
3504
- };
3505
- return next;
3506
- });
3507
- }
3508
- // reflect in local agent stub so tabs and titles can use it if needed
3509
- setAgent((prev) =>
3510
- prev
3511
- ? {
3512
- ...prev,
3513
- metadata: JSON.stringify({
3514
- ...(agentMetadata || {}),
3515
- profile: nextProfile.name,
3516
- additionalData: {
3517
- ...((agentMetadata as any)?.additionalData ||
3518
- {}),
3519
- profileId: nextProfile.id,
3520
- profileName: nextProfile.name,
3521
- },
3522
- }),
3523
- }
3524
- : prev,
3525
- );
3526
- } catch (err) {
3527
- console.error("Failed to persist agent profile", err);
3528
- }
3529
- }}
3530
- title="Profile"
3531
- aria-label="Profile"
3532
- data-testid="agent-profile-select"
3533
- >
3534
- {profiles.map((p) => (
3535
- <option key={p.id} value={p.id}>
3536
- {p.name}
3537
- </option>
3538
- ))}
3539
- </select>
3540
- )}
3541
- {activeProfile?.models?.length ? (
3542
- <select
3543
- className="h-5 rounded border px-1.5 text-[10px] text-gray-500"
3544
- value={selectedModelId || ""}
3545
- onChange={async (e) => {
3546
- const nextId = e.target.value;
3547
- setSelectedModelId(nextId);
3548
- const modelName =
3549
- activeProfile?.models?.find((m) => m.id === nextId)?.name ||
3550
- "";
3551
- // Update local agent state immediately for UX and to reflect in streaming stub
3552
- setAgent((prev) =>
3553
- prev ? { ...prev, model: modelName } : prev,
3554
- );
3555
- // Persist only for existing agents; otherwise cache until first start
3556
- try {
3557
- if (agent?.id && agent.status !== "new") {
3558
- await updateAgentSettings(agent.id, { model: modelName });
3559
- } else {
3560
- pendingSettingsRef.current = {
3561
- ...(pendingSettingsRef.current || {}),
3562
- modelName,
3563
- };
3564
- }
3565
- } catch (err) {
3566
- console.error("Failed to persist agent model", err);
3567
- }
3568
- }}
3569
- title="Model"
3570
- aria-label="Model"
3571
- data-testid="agent-model-select"
3572
- >
3573
- {activeProfile.models.map((m) => (
3574
- <option key={m.id} value={m.id}>
3575
- {m.name}
3576
- </option>
3577
- ))}
3578
- </select>
3579
- ) : null}
3580
- {activeProfile?.prompts?.length ? (
3581
- <Popover open={showPredefined} onOpenChange={setShowPredefined}>
3582
- <PopoverTrigger asChild>
3583
- <button
3584
- className="rounded p-1 hover:bg-gray-100"
3585
- onClick={() => {}}
3586
- title="Predefined prompts"
3587
- aria-label="Predefined prompts"
3588
- >
3589
- <Wand2 className="h-3 w-3" strokeWidth={1} />
3590
- </button>
3591
- </PopoverTrigger>
3592
- <PopoverContent className="w-64 p-0" align="start">
3593
- <div className="max-h-56 overflow-y-auto p-2">
3594
- {activeProfile.prompts.map((p, index) => (
3595
- <div
3596
- key={index}
3597
- className="cursor-pointer rounded p-1.5 text-xs text-gray-700 hover:bg-gray-100"
3598
- onClick={() => {
3599
- setPrompt(p.prompt);
3600
- setShowPredefined(false);
3601
- if (textareaRef.current) textareaRef.current.focus();
3602
- }}
3603
- >
3604
- {p.title}
3605
- </div>
3606
- ))}
3607
- </div>
3608
- </PopoverContent>
3609
- </Popover>
3610
- ) : null}
3611
- </div>
3612
- <div className="flex items-center gap-1 self-end">
3613
- {isVoiceSupported ? (
3614
- <Button
3615
- onClick={toggleVoice}
3616
- size="sm"
3617
- className="h-5.5 w-5.5 cursor-pointer rounded-full"
3618
- title={isListening ? "Stop voice input" : "Start voice input"}
3619
- aria-label={
3620
- isListening ? "Stop voice input" : "Start voice input"
3236
+ handleStop();
3237
+ } catch {}
3621
3238
  }
3622
- aria-pressed={isListening}
3623
- >
3624
- {isListening ? (
3625
- <MicOff className="size-3" strokeWidth={1} />
3626
- ) : (
3627
- <Mic className="size-3" strokeWidth={1} />
3628
- )}
3629
- </Button>
3630
- ) : null}
3631
- <Button
3632
- onClick={isExecuting ? handleStop : handleSubmit}
3633
- disabled={!isExecuting && !prompt.trim()}
3634
- size="sm"
3635
- className="h-5.5 w-5.5 cursor-pointer rounded-full"
3636
- title={isExecuting ? "Stop" : "Send"}
3637
- aria-label={isExecuting ? "Stop" : "Send"}
3638
- data-testid="agent-send-stop-button"
3639
- data-executing={isExecuting ? "true" : "false"}
3640
- >
3641
- {isExecuting ? (
3642
- <Square className="size-3" strokeWidth={1} />
3643
- ) : (
3644
- <Send className="size-3" strokeWidth={1} />
3645
- )}
3646
- </Button>
3647
- </div>
3648
- </div>
3649
- <div className="mt-1 flex items-center gap-2 text-[10px] text-gray-500">
3650
- <AgentCostDisplay
3651
- totalTokens={
3652
- liveTotals
3653
- ? {
3654
- input: liveTotals.input,
3655
- output: liveTotals.output,
3656
- cached: liveTotals.cached,
3657
- cacheWrite: liveTotals.cacheWrite ?? 0,
3658
- inputCost: liveTotals.inputCost,
3659
- outputCost: liveTotals.outputCost,
3660
- cachedCost: liveTotals.cachedCost,
3661
- cacheWriteCost: liveTotals.cacheWriteCost ?? 0,
3662
- totalCost: liveTotals.totalCost,
3663
- }
3664
- : totalTokens
3665
- }
3666
- costLimit={effectiveCostLimit}
3239
+ sendQuickMessage(filledText);
3240
+ }
3241
+ }}
3242
+ onCancel={() => {
3243
+ setPrompt("");
3244
+ setInputPlaceholder(
3245
+ "Type your message... (Enter to send, Shift+Enter or Ctrl+Enter for new line)",
3246
+ );
3247
+ }}
3667
3248
  />
3668
- {(() => {
3669
- try {
3670
- const s = (window as any).__agentContextWindowStatus;
3671
- if (!s || !s.contextWindowTokens) return null;
3672
- const pct =
3673
- typeof s.contextUsedPercent === "number"
3674
- ? `${s.contextUsedPercent.toFixed(1)}%`
3675
- : undefined;
3676
-
3677
- // Helper function to format tokens as "k"
3678
- const formatTokens = (tokens: number) => {
3679
- if (tokens >= 1000) {
3680
- return `${(tokens / 1000).toFixed(1)}k`;
3249
+ ) : (
3250
+ <div className="flex items-stretch gap-2">
3251
+ <Textarea
3252
+ ref={textareaRef}
3253
+ value={prompt}
3254
+ onChange={(e) => {
3255
+ setPrompt(e.target.value);
3256
+ // Reset history index when user starts typing
3257
+ if (currentHistoryIndex !== -1) {
3258
+ setCurrentHistoryIndex(-1);
3681
3259
  }
3682
- return tokens.toString();
3683
- };
3684
-
3685
- if (!pct) return null;
3686
-
3687
- return (
3688
- <Tooltip>
3260
+ }}
3261
+ onKeyDown={handleKeyPress}
3262
+ placeholder={inputPlaceholder}
3263
+ className="h-[80px] flex-1 resize-none overflow-y-auto text-xs"
3264
+ data-testid="agent-terminal-prompt"
3265
+ disabled={isSubmitting}
3266
+ />
3267
+ </div>
3268
+ )}
3269
+ {!hideBottomControls && (
3270
+ <>
3271
+ <div className="flex items-stretch justify-between gap-2">
3272
+ {/* Profile/Model selectors below the input */}
3273
+ <div className="mt-2 flex flex-wrap items-center justify-start gap-2">
3274
+ <Tooltip delayDuration={400}>
3689
3275
  <TooltipTrigger asChild>
3690
- <div className="cursor-help rounded border border-gray-200 bg-gray-50 px-2 py-0.5">
3691
- Context: {pct}
3692
- </div>
3276
+ <select
3277
+ className={`h-5 rounded border px-1.5 text-[10px] ${
3278
+ mode === "read-only"
3279
+ ? "border-green-300 bg-green-50 text-green-700"
3280
+ : mode === "supervised"
3281
+ ? "border-amber-300 bg-amber-50 text-amber-700"
3282
+ : "border-red-300 bg-red-50 text-red-700"
3283
+ }`}
3284
+ value={mode}
3285
+ onChange={async (e) => {
3286
+ const nextMode =
3287
+ (e.target.value as AgentMode) || "supervised";
3288
+ // Optimistic UI update
3289
+ setMode(nextMode);
3290
+ const current = agentMetadata || ({} as AgentMetadata);
3291
+ const nextMeta: AgentMetadata = {
3292
+ ...current,
3293
+ mode: nextMode,
3294
+ } as AgentMetadata;
3295
+ try {
3296
+ if (!agent?.id || agent.status === "new") {
3297
+ setAgentMetadata(nextMeta);
3298
+ // Cache until first start when agent is persisted
3299
+ pendingSettingsRef.current = {
3300
+ ...(pendingSettingsRef.current || {}),
3301
+ mode: nextMode,
3302
+ };
3303
+ return;
3304
+ }
3305
+ await updateAgentSettings(agent.id, {
3306
+ mode: nextMode,
3307
+ });
3308
+ setAgentMetadata(nextMeta);
3309
+ setAgent((prev) =>
3310
+ prev
3311
+ ? { ...prev, metadata: JSON.stringify(nextMeta) }
3312
+ : prev,
3313
+ );
3314
+ } catch (e2) {
3315
+ console.error("Failed to persist mode change", e2);
3316
+ }
3317
+ }}
3318
+ title="Mode"
3319
+ aria-label="Mode"
3320
+ data-testid="agent-mode-select"
3321
+ >
3322
+ <option value="supervised">Supervised</option>
3323
+ <option value="autonomous">Autonomous</option>
3324
+ <option value="read-only">Read-Only</option>
3325
+ </select>
3693
3326
  </TooltipTrigger>
3694
3327
  <TooltipContent side="top" sideOffset={6}>
3695
- <div className="max-w-[320px] space-y-1 text-xs">
3328
+ <div className="max-w-[320px] space-y-1">
3696
3329
  <div>
3697
- <span className="font-semibold">Model:</span> {s.model}
3698
- {s.normalizedModel && ` (${s.normalizedModel})`}
3330
+ <span className="font-semibold text-green-500">
3331
+ Read-Only
3332
+ </span>
3333
+ : Limited tool access as configured by the profile (Ask
3334
+ Mode Tools).
3699
3335
  </div>
3700
3336
  <div>
3701
- <span className="font-semibold">Context window:</span>{" "}
3702
- {formatTokens(s.estimatedInputTokens || 0)} /{" "}
3703
- {formatTokens(s.contextWindowTokens)} tokens
3337
+ <span className="font-semibold text-amber-500">
3338
+ Supervised
3339
+ </span>
3340
+ : Full tool access, but writes are limited to
3341
+ pages/items in the current context. Creating new items
3342
+ or updating existing items outside the current context
3343
+ requires explicit approval.
3704
3344
  </div>
3705
- {typeof s.maxCompletionTokens === "number" && (
3706
- <div>
3707
- <span className="font-semibold">Max completion:</span>{" "}
3708
- {formatTokens(s.maxCompletionTokens)} tokens
3709
- </div>
3710
- )}
3711
3345
  <div>
3712
- <span className="font-semibold">Used:</span> {pct}
3346
+ <span className="font-semibold text-red-500">
3347
+ Autonomous
3348
+ </span>
3349
+ : Full tool access; can write across the site/project
3350
+ only limited by user permissions.
3713
3351
  </div>
3714
3352
  </div>
3715
3353
  </TooltipContent>
3716
3354
  </Tooltip>
3717
- );
3718
- } catch {
3719
- return null;
3720
- }
3721
- })()}
3722
- </div>
3355
+ {profiles?.length > 0 && (
3356
+ <select
3357
+ className="h-5 rounded border px-1.5 text-[10px] text-gray-500"
3358
+ value={activeProfile?.id || ""}
3359
+ onChange={async (e) => {
3360
+ const nextProfile = profiles.find(
3361
+ (x) => x.id === e.target.value,
3362
+ );
3363
+ if (!nextProfile) return;
3364
+ setActiveProfile(nextProfile);
3365
+ try {
3366
+ if (agent?.id && agent.status !== "new") {
3367
+ await updateAgentSettings(agent.id, {
3368
+ profileId: nextProfile.id,
3369
+ profileName: nextProfile.name,
3370
+ });
3371
+ } else {
3372
+ // cache until first start
3373
+ pendingSettingsRef.current = {
3374
+ ...(pendingSettingsRef.current || {}),
3375
+ // we cache profile by updating local metadata
3376
+ };
3377
+ setAgentMetadata((current) => {
3378
+ const next = { ...(current || {}) } as any;
3379
+ next.profile = nextProfile.name;
3380
+ next.additionalData = {
3381
+ ...(next.additionalData || {}),
3382
+ profileId: nextProfile.id,
3383
+ profileName: nextProfile.name,
3384
+ };
3385
+ return next;
3386
+ });
3387
+ }
3388
+ // reflect in local agent stub so tabs and titles can use it if needed
3389
+ setAgent((prev) =>
3390
+ prev
3391
+ ? {
3392
+ ...prev,
3393
+ metadata: JSON.stringify({
3394
+ ...(agentMetadata || {}),
3395
+ profile: nextProfile.name,
3396
+ additionalData: {
3397
+ ...((agentMetadata as any)
3398
+ ?.additionalData || {}),
3399
+ profileId: nextProfile.id,
3400
+ profileName: nextProfile.name,
3401
+ },
3402
+ }),
3403
+ }
3404
+ : prev,
3405
+ );
3406
+ } catch (err) {
3407
+ console.error("Failed to persist agent profile", err);
3408
+ }
3409
+ }}
3410
+ title="Profile"
3411
+ aria-label="Profile"
3412
+ data-testid="agent-profile-select"
3413
+ >
3414
+ {profiles.map((p) => (
3415
+ <option key={p.id} value={p.id}>
3416
+ {p.name}
3417
+ </option>
3418
+ ))}
3419
+ </select>
3420
+ )}
3421
+ {activeProfile?.models?.length ? (
3422
+ <select
3423
+ className="h-5 rounded border px-1.5 text-[10px] text-gray-500"
3424
+ value={selectedModelId || ""}
3425
+ onChange={async (e) => {
3426
+ const nextId = e.target.value;
3427
+ setSelectedModelId(nextId);
3428
+ const modelName =
3429
+ activeProfile?.models?.find((m) => m.id === nextId)
3430
+ ?.name || "";
3431
+ // Update local agent state immediately for UX and to reflect in streaming stub
3432
+ setAgent((prev) =>
3433
+ prev ? { ...prev, model: modelName } : prev,
3434
+ );
3435
+ // Persist only for existing agents; otherwise cache until first start
3436
+ try {
3437
+ if (agent?.id && agent.status !== "new") {
3438
+ await updateAgentSettings(agent.id, {
3439
+ model: modelName,
3440
+ });
3441
+ } else {
3442
+ pendingSettingsRef.current = {
3443
+ ...(pendingSettingsRef.current || {}),
3444
+ modelName,
3445
+ };
3446
+ }
3447
+ } catch (err) {
3448
+ console.error("Failed to persist agent model", err);
3449
+ }
3450
+ }}
3451
+ title="Model"
3452
+ aria-label="Model"
3453
+ data-testid="agent-model-select"
3454
+ >
3455
+ {activeProfile.models.map((m) => (
3456
+ <option key={m.id} value={m.id}>
3457
+ {m.name}
3458
+ </option>
3459
+ ))}
3460
+ </select>
3461
+ ) : null}
3462
+ {activeProfile?.prompts?.length ? (
3463
+ <Popover
3464
+ open={showPredefined}
3465
+ onOpenChange={setShowPredefined}
3466
+ >
3467
+ <PopoverTrigger asChild>
3468
+ <button
3469
+ className="rounded p-1 hover:bg-gray-100"
3470
+ onClick={() => {}}
3471
+ title="Predefined prompts"
3472
+ aria-label="Predefined prompts"
3473
+ >
3474
+ <Wand2 className="h-3 w-3" strokeWidth={1} />
3475
+ </button>
3476
+ </PopoverTrigger>
3477
+ <PopoverContent className="w-64 p-0" align="start">
3478
+ <div className="max-h-56 overflow-y-auto p-2">
3479
+ {activeProfile.prompts.map((p, index) => (
3480
+ <div
3481
+ key={index}
3482
+ className="cursor-pointer rounded p-1.5 text-xs text-gray-700 hover:bg-gray-100"
3483
+ onClick={() => {
3484
+ setPrompt(p.prompt);
3485
+ setShowPredefined(false);
3486
+ if (textareaRef.current)
3487
+ textareaRef.current.focus();
3488
+ }}
3489
+ >
3490
+ {p.title}
3491
+ </div>
3492
+ ))}
3493
+ </div>
3494
+ </PopoverContent>
3495
+ </Popover>
3496
+ ) : null}
3497
+ </div>
3498
+ <div className="flex items-center gap-1 self-end">
3499
+ {isVoiceSupported ? (
3500
+ <Button
3501
+ onClick={toggleVoice}
3502
+ size="sm"
3503
+ className="h-5.5 w-5.5 cursor-pointer rounded-full"
3504
+ title={
3505
+ isListening ? "Stop voice input" : "Start voice input"
3506
+ }
3507
+ aria-label={
3508
+ isListening ? "Stop voice input" : "Start voice input"
3509
+ }
3510
+ aria-pressed={isListening}
3511
+ >
3512
+ {isListening ? (
3513
+ <MicOff className="size-3" strokeWidth={1} />
3514
+ ) : (
3515
+ <Mic className="size-3" strokeWidth={1} />
3516
+ )}
3517
+ </Button>
3518
+ ) : null}
3519
+ <Button
3520
+ onClick={isExecuting ? handleStop : handleSubmit}
3521
+ disabled={!isExecuting && !prompt.trim()}
3522
+ size="sm"
3523
+ className="h-5.5 w-5.5 cursor-pointer rounded-full"
3524
+ title={isExecuting ? "Stop" : "Send"}
3525
+ aria-label={isExecuting ? "Stop" : "Send"}
3526
+ data-testid="agent-send-stop-button"
3527
+ data-executing={isExecuting ? "true" : "false"}
3528
+ >
3529
+ {isExecuting ? (
3530
+ <Square className="size-3" strokeWidth={1} />
3531
+ ) : (
3532
+ <Send className="size-3" strokeWidth={1} />
3533
+ )}
3534
+ </Button>
3535
+ </div>
3536
+ </div>
3537
+ <div className="mt-1 flex items-center gap-2 text-[10px] text-gray-500">
3538
+ <AgentCostDisplay
3539
+ totalTokens={
3540
+ liveTotals
3541
+ ? {
3542
+ input: liveTotals.input,
3543
+ output: liveTotals.output,
3544
+ cached: liveTotals.cached,
3545
+ cacheWrite: liveTotals.cacheWrite ?? 0,
3546
+ inputCost: liveTotals.inputCost,
3547
+ outputCost: liveTotals.outputCost,
3548
+ cachedCost: liveTotals.cachedCost,
3549
+ cacheWriteCost: liveTotals.cacheWriteCost ?? 0,
3550
+ totalCost: liveTotals.totalCost,
3551
+ }
3552
+ : totalTokens
3553
+ }
3554
+ costLimit={effectiveCostLimit}
3555
+ />
3556
+ {(() => {
3557
+ try {
3558
+ const s = (window as any).__agentContextWindowStatus;
3559
+ if (!s || !s.contextWindowTokens) return null;
3560
+ const pct =
3561
+ typeof s.contextUsedPercent === "number"
3562
+ ? `${s.contextUsedPercent.toFixed(1)}%`
3563
+ : undefined;
3564
+
3565
+ // Helper function to format tokens as "k"
3566
+ const formatTokens = (tokens: number) => {
3567
+ if (tokens >= 1000) {
3568
+ return `${(tokens / 1000).toFixed(1)}k`;
3569
+ }
3570
+ return tokens.toString();
3571
+ };
3572
+
3573
+ if (!pct) return null;
3574
+
3575
+ return (
3576
+ <Tooltip>
3577
+ <TooltipTrigger asChild>
3578
+ <div className="cursor-help rounded border border-gray-200 bg-gray-50 px-2 py-0.5">
3579
+ Context: {pct}
3580
+ </div>
3581
+ </TooltipTrigger>
3582
+ <TooltipContent side="top" sideOffset={6}>
3583
+ <div className="max-w-[320px] space-y-1 text-xs">
3584
+ <div>
3585
+ <span className="font-semibold">Model:</span>{" "}
3586
+ {s.model}
3587
+ {s.normalizedModel && ` (${s.normalizedModel})`}
3588
+ </div>
3589
+ <div>
3590
+ <span className="font-semibold">
3591
+ Context window:
3592
+ </span>{" "}
3593
+ {formatTokens(s.estimatedInputTokens || 0)} /{" "}
3594
+ {formatTokens(s.contextWindowTokens)} tokens
3595
+ </div>
3596
+ {typeof s.maxCompletionTokens === "number" && (
3597
+ <div>
3598
+ <span className="font-semibold">
3599
+ Max completion:
3600
+ </span>{" "}
3601
+ {formatTokens(s.maxCompletionTokens)} tokens
3602
+ </div>
3603
+ )}
3604
+ <div>
3605
+ <span className="font-semibold">Used:</span> {pct}
3606
+ </div>
3607
+ </div>
3608
+ </TooltipContent>
3609
+ </Tooltip>
3610
+ );
3611
+ } catch {
3612
+ return null;
3613
+ }
3614
+ })()}
3615
+ </div>
3616
+ </>
3617
+ )}
3723
3618
  </div>
3724
3619
  </div>
3725
3620
  );