@alpaca-editor/core 1.0.4065 → 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 (57) hide show
  1. package/dist/editor/ImageEditButton.js +10 -3
  2. package/dist/editor/ImageEditButton.js.map +1 -1
  3. package/dist/editor/ai/AgentTerminal.d.ts +3 -2
  4. package/dist/editor/ai/AgentTerminal.js +386 -94
  5. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  6. package/dist/editor/ai/Agents.js +36 -19
  7. package/dist/editor/ai/Agents.js.map +1 -1
  8. package/dist/editor/ai/AiResponseMessage.d.ts +6 -1
  9. package/dist/editor/ai/AiResponseMessage.js +63 -3
  10. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  11. package/dist/editor/ai/AiTerminal.js +27 -2
  12. package/dist/editor/ai/AiTerminal.js.map +1 -1
  13. package/dist/editor/client/EditorClient.js +15 -0
  14. package/dist/editor/client/EditorClient.js.map +1 -1
  15. package/dist/editor/client/editContext.d.ts +2 -0
  16. package/dist/editor/client/editContext.js.map +1 -1
  17. package/dist/editor/commands/componentCommands.js +56 -6
  18. package/dist/editor/commands/componentCommands.js.map +1 -1
  19. package/dist/editor/page-editor-chrome/CommentHighlighting.js +6 -4
  20. package/dist/editor/page-editor-chrome/CommentHighlighting.js.map +1 -1
  21. package/dist/editor/page-editor-chrome/CommentHighlightings.js +1 -1
  22. package/dist/editor/page-editor-chrome/CommentHighlightings.js.map +1 -1
  23. package/dist/editor/page-editor-chrome/FrameMenu.js +6 -8
  24. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  25. package/dist/editor/reviews/Comment.js +3 -58
  26. package/dist/editor/reviews/Comment.js.map +1 -1
  27. package/dist/editor/reviews/CommentDisplayPopover.js +2 -3
  28. package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
  29. package/dist/editor/reviews/Comments.js +4 -0
  30. package/dist/editor/reviews/Comments.js.map +1 -1
  31. package/dist/editor/reviews/commentAi.d.ts +7 -0
  32. package/dist/editor/reviews/commentAi.js +86 -0
  33. package/dist/editor/reviews/commentAi.js.map +1 -0
  34. package/dist/revision.d.ts +2 -2
  35. package/dist/revision.js +2 -2
  36. package/dist/styles.css +10 -0
  37. package/package.json +1 -1
  38. package/src/editor/ImageEditButton.tsx +36 -8
  39. package/src/editor/ai/AgentTerminal.tsx +436 -65
  40. package/src/editor/ai/Agents.tsx +193 -140
  41. package/src/editor/ai/AiResponseMessage.tsx +106 -2
  42. package/src/editor/ai/AiTerminal.tsx +27 -0
  43. package/src/editor/client/EditorClient.tsx +23 -0
  44. package/src/editor/client/editContext.ts +2 -0
  45. package/src/editor/commands/componentCommands.tsx +60 -12
  46. package/src/editor/page-editor-chrome/CommentHighlighting.tsx +6 -4
  47. package/src/editor/page-editor-chrome/CommentHighlightings.tsx +3 -1
  48. package/src/editor/page-editor-chrome/FrameMenu.tsx +6 -8
  49. package/src/editor/reviews/Comment.tsx +4 -66
  50. package/src/editor/reviews/CommentDisplayPopover.tsx +2 -3
  51. package/src/editor/reviews/Comments.tsx +12 -0
  52. package/src/editor/reviews/commentAi.ts +106 -0
  53. package/src/revision.ts +2 -2
  54. package/dist/editor/ai/AiPromptPopover.d.ts +0 -7
  55. package/dist/editor/ai/AiPromptPopover.js +0 -111
  56. package/dist/editor/ai/AiPromptPopover.js.map +0 -1
  57. package/src/editor/ai/AiPromptPopover.tsx +0 -206
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState, useRef, useCallback } from "react";
3
- import { Send, Bot, AlertCircle, Loader2, User, X, FileText, Puzzle, Type, Plus, } from "lucide-react";
3
+ import { Send, Bot, AlertCircle, Loader2, User, X, FileText, Puzzle, Type, Plus, Wand2, Square, } from "lucide-react";
4
4
  import { DancingDots } from "./DancingDots";
5
5
  import { getAgent, startAgent, connectToAgentStream, updateAgentMetadata, } from "../services/agentService";
6
6
  import { useEditContext } from "../client/editContext";
@@ -9,6 +9,8 @@ import { Button } from "../../components/ui/button";
9
9
  import { AiResponseMessage } from "./AiResponseMessage";
10
10
  import { AgentCostDisplay } from "./AgentCostDisplay";
11
11
  import { getComponentById } from "../componentTreeHelper";
12
+ import { loadAiProfiles } from "../services/aiService";
13
+ import { Popover, PopoverContent, PopoverTrigger, } from "../../components/ui/popover";
12
14
  // Simple user message component
13
15
  const UserMessage = ({ message }) => {
14
16
  return (_jsxs("div", { className: "flex gap-3 p-4", children: [_jsx("div", { className: "flex-shrink-0", children: _jsx(User, { className: "h-6 w-6 text-blue-600", strokeWidth: 1 }) }), _jsxs("div", { className: "min-w-0 flex-1 select-text", children: [_jsxs("div", { className: "mb-1 flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium text-gray-900", children: "You" }), message.createdDate && (_jsx("span", { className: "text-xs text-gray-400", children: new Date(message.createdDate).toLocaleTimeString() }))] }), _jsx("div", { className: "prose prose-sm max-w-none text-sm text-gray-700 select-text", children: message.content })] })] }));
@@ -100,7 +102,7 @@ const convertAgentMessagesToAiFormat = (agentMessages) => {
100
102
  // interface AgentTerminalProps {
101
103
  // agentStub: Agent;
102
104
  // }
103
- export function AgentTerminal({ agentStub }) {
105
+ export function AgentTerminal({ agentStub, initialMetadata, }) {
104
106
  const editContext = useEditContext();
105
107
  const [agent, setAgent] = useState(undefined);
106
108
  const [messages, setMessages] = useState([]);
@@ -110,6 +112,36 @@ export function AgentTerminal({ agentStub }) {
110
112
  const [isSubmitting, setIsSubmitting] = useState(false);
111
113
  const [agentMetadata, setAgentMetadata] = useState(null);
112
114
  const [isWaitingForResponse, setIsWaitingForResponse] = useState(false);
115
+ const isWaitingRef = useRef(false);
116
+ useEffect(() => {
117
+ isWaitingRef.current = isWaitingForResponse;
118
+ }, [isWaitingForResponse]);
119
+ // Dots visibility controlled by 1s idle timer since last incremental update
120
+ const [showDots, setShowDots] = useState(false);
121
+ const dotsTimeoutRef = useRef(null);
122
+ const hasActiveStreaming = useCallback(() => {
123
+ const current = messagesRef.current || [];
124
+ return current.some((m) => !m.isCompleted && m.messageType === "streaming");
125
+ }, []);
126
+ const resetDotsTimer = useCallback(() => {
127
+ if (dotsTimeoutRef.current) {
128
+ clearTimeout(dotsTimeoutRef.current);
129
+ dotsTimeoutRef.current = null;
130
+ }
131
+ const waiting = isWaitingRef.current;
132
+ const streaming = hasActiveStreaming();
133
+ if (!waiting && !streaming) {
134
+ setShowDots(false);
135
+ return;
136
+ }
137
+ setShowDots(false);
138
+ dotsTimeoutRef.current = setTimeout(() => {
139
+ // Re-check conditions after 1s of inactivity
140
+ const stillWaiting = isWaitingRef.current;
141
+ const stillStreaming = hasActiveStreaming();
142
+ setShowDots(stillWaiting || stillStreaming);
143
+ }, 1000);
144
+ }, [hasActiveStreaming]);
113
145
  const [resolvedPageName, setResolvedPageName] = useState(undefined);
114
146
  const [resolvedComponentName, setResolvedComponentName] = useState(undefined);
115
147
  const [resolvedFieldName, setResolvedFieldName] = useState(undefined);
@@ -120,9 +152,26 @@ export function AgentTerminal({ agentStub }) {
120
152
  return [];
121
153
  });
122
154
  const [currentHistoryIndex, setCurrentHistoryIndex] = useState(-1);
155
+ const [showPredefined, setShowPredefined] = useState(false);
156
+ const [profiles, setProfiles] = useState([]);
157
+ const [activeProfile, setActiveProfile] = useState(undefined);
158
+ const [selectedModelId, setSelectedModelId] = useState(undefined);
123
159
  useEffect(() => {
124
160
  localStorage.setItem("editor.agent.promptHistory", JSON.stringify(promptHistory));
125
161
  }, [promptHistory]);
162
+ // Clear idle timer on unmount
163
+ useEffect(() => {
164
+ return () => {
165
+ if (dotsTimeoutRef.current) {
166
+ clearTimeout(dotsTimeoutRef.current);
167
+ dotsTimeoutRef.current = null;
168
+ }
169
+ };
170
+ }, []);
171
+ // Whenever waiting state changes, restart idle timer logic
172
+ useEffect(() => {
173
+ resetDotsTimer();
174
+ }, [isWaitingForResponse, resetDotsTimer]);
126
175
  useEffect(() => {
127
176
  // Keep messagesRef synchronized with messages state
128
177
  messagesRef.current = messages;
@@ -200,6 +249,8 @@ export function AgentTerminal({ agentStub }) {
200
249
  }
201
250
  // Clear waiting state when first content chunk arrives
202
251
  setIsWaitingForResponse(false);
252
+ // Any content chunk is an incremental update -> reset idle timer
253
+ resetDotsTimer();
203
254
  // Always call setMessages and handle all logic in the callback with latest messages
204
255
  setMessages((prev) => {
205
256
  // Find existing message by messageId in the latest messages
@@ -316,6 +367,8 @@ export function AgentTerminal({ agentStub }) {
316
367
  messagesRef.current = updated;
317
368
  return updated;
318
369
  });
370
+ // Tool call activity counts as activity; keep dots hidden for 1s
371
+ resetDotsTimer();
319
372
  }
320
373
  }, [createNewStreamMessage]);
321
374
  const handleToolResult = useCallback((message, agentData) => {
@@ -391,7 +444,9 @@ export function AgentTerminal({ agentStub }) {
391
444
  messagesRef.current = updated;
392
445
  return updated;
393
446
  });
394
- }, []);
447
+ // Tool result activity; reset idle timer
448
+ resetDotsTimer();
449
+ }, [resetDotsTimer]);
395
450
  // Connect to agent stream for real-time updates
396
451
  const connectToStream = useCallback(async (agentData) => {
397
452
  const currentAgent = agentData || agent;
@@ -424,67 +479,81 @@ export function AgentTerminal({ agentStub }) {
424
479
  console.log("💾 Stream completed for message:", completedMessageId, "messages count:", messages.length);
425
480
  // Mark the specific message as completed by messageId
426
481
  if (completedMessageId) {
427
- setMessages((prev) => prev.map((msg) => {
428
- if (msg.id === completedMessageId) {
429
- const updatedMessage = {
430
- ...msg,
431
- isCompleted: true,
432
- messageType: "completed",
433
- };
434
- // Update token usage data if provided in the completed event
435
- if (message.data) {
436
- const data = message.data;
437
- if (data.numInputTokens !== undefined) {
438
- updatedMessage.inputTokens = data.numInputTokens;
439
- }
440
- if (data.numOutputTokens !== undefined) {
441
- updatedMessage.outputTokens = data.numOutputTokens;
442
- }
443
- if (data.numCachedTokens !== undefined) {
444
- updatedMessage.cachedInputTokens =
445
- data.numCachedTokens;
446
- }
447
- // Update total tokens used
448
- updatedMessage.tokensUsed =
449
- (updatedMessage.inputTokens || 0) +
450
- (updatedMessage.outputTokens || 0);
451
- // Handle content that might only be sent in the completed event
452
- if (data.deltaContent && data.deltaContent.trim()) {
453
- if (!data.isIncremental) {
454
- // Non-incremental: replace the entire content
455
- updatedMessage.content = data.deltaContent;
482
+ setMessages((prev) => {
483
+ const updated = prev.map((msg) => {
484
+ if (msg.id === completedMessageId) {
485
+ const updatedMessage = {
486
+ ...msg,
487
+ isCompleted: true,
488
+ messageType: "completed",
489
+ };
490
+ // Update token usage data if provided in the completed event
491
+ if (message.data) {
492
+ const data = message.data;
493
+ if (data.numInputTokens !== undefined) {
494
+ updatedMessage.inputTokens = data.numInputTokens;
495
+ }
496
+ if (data.numOutputTokens !== undefined) {
497
+ updatedMessage.outputTokens = data.numOutputTokens;
456
498
  }
457
- else {
458
- // Incremental: append to existing content
459
- updatedMessage.content =
460
- (updatedMessage.content || "") +
461
- data.deltaContent;
499
+ if (data.numCachedTokens !== undefined) {
500
+ updatedMessage.cachedInputTokens =
501
+ data.numCachedTokens;
502
+ }
503
+ // Update total tokens used
504
+ updatedMessage.tokensUsed =
505
+ (updatedMessage.inputTokens || 0) +
506
+ (updatedMessage.outputTokens || 0);
507
+ // Handle content that might only be sent in the completed event
508
+ if (data.deltaContent && data.deltaContent.trim()) {
509
+ if (!data.isIncremental) {
510
+ // Non-incremental: replace the entire content
511
+ updatedMessage.content = data.deltaContent;
512
+ }
513
+ else {
514
+ // Incremental: append to existing content
515
+ updatedMessage.content =
516
+ (updatedMessage.content || "") +
517
+ data.deltaContent;
518
+ }
462
519
  }
463
520
  }
521
+ return updatedMessage;
464
522
  }
465
- return updatedMessage;
466
- }
467
- return msg;
468
- }));
523
+ return msg;
524
+ });
525
+ messagesRef.current = updated;
526
+ return updated;
527
+ });
469
528
  }
470
529
  else {
471
530
  // Fallback: Mark any streaming messages as completed (old behavior)
472
531
  console.warn("⚠️ No messageId in completed event, falling back to marking all streaming messages as completed");
473
- setMessages((prev) => prev.map((msg) => !msg.isCompleted && msg.messageType === "streaming"
474
- ? {
475
- ...msg,
476
- isCompleted: true,
477
- messageType: "completed",
478
- }
479
- : msg));
532
+ setMessages((prev) => {
533
+ const updated = prev.map((msg) => !msg.isCompleted && msg.messageType === "streaming"
534
+ ? {
535
+ ...msg,
536
+ isCompleted: true,
537
+ messageType: "completed",
538
+ }
539
+ : msg);
540
+ messagesRef.current = updated;
541
+ return updated;
542
+ });
480
543
  }
544
+ // Ensure waiting state is cleared when stream completes
545
+ setIsWaitingForResponse(false);
481
546
  shouldCreateNewMessage.current = false;
547
+ // Streaming finished; update indicator
548
+ resetDotsTimer();
482
549
  break;
483
550
  case "error":
484
551
  console.error("❌ Stream error:", message.error);
485
552
  setError(message.error || "Stream error occurred");
486
553
  setIsWaitingForResponse(false);
487
554
  shouldCreateNewMessage.current = false;
555
+ // Error ends streaming; update indicator
556
+ resetDotsTimer();
488
557
  break;
489
558
  default:
490
559
  console.warn("❓ Unhandled message type:", {
@@ -508,6 +577,8 @@ export function AgentTerminal({ agentStub }) {
508
577
  }
509
578
  finally {
510
579
  setIsConnecting(false);
580
+ // Guard: clear waiting state if connection finished without content
581
+ setIsWaitingForResponse(false);
511
582
  }
512
583
  }, [agent?.id, handleContentChunk, handleToolCall, handleToolResult]);
513
584
  // Load agent data and messages
@@ -545,35 +616,51 @@ export function AgentTerminal({ agentStub }) {
545
616
  setError(null);
546
617
  setIsLoading(false);
547
618
  // Initialize local context for a brand-new agent (not yet persisted)
548
- const item = editContext?.currentItemDescriptor;
549
- const initialLocalContext = item
550
- ? {
551
- additionalData: {
552
- context: {
553
- pages: [
554
- {
555
- id: item.id,
556
- language: item.language,
557
- version: item.version,
558
- name: editContext?.contentEditorItem?.name,
559
- },
560
- ],
561
- componentIds: editContext?.selection?.length
562
- ? editContext.selection
563
- : undefined,
564
- field: editContext?.focusedField &&
565
- editContext.currentItemDescriptor
566
- ? {
567
- fieldId: editContext.focusedField.fieldId,
568
- itemId: editContext.focusedField.item.id,
569
- }
570
- : undefined,
571
- },
572
- },
619
+ // Prefer explicitly provided metadata; otherwise seed with page, selected componentIds, and focused field if available
620
+ if (initialMetadata) {
621
+ setAgentMetadata(initialMetadata);
622
+ // If an initial prompt is provided via metadata, seed the input once
623
+ try {
624
+ const maybePrompt = initialMetadata?.additionalData
625
+ ?.initialPrompt;
626
+ if (typeof maybePrompt === "string" && maybePrompt.trim()) {
627
+ setPrompt(maybePrompt);
628
+ }
573
629
  }
574
- : null;
575
- if (initialLocalContext)
576
- setAgentMetadata(initialLocalContext);
630
+ catch { }
631
+ }
632
+ else {
633
+ const item = editContext?.currentItemDescriptor;
634
+ const initialLocalContext = item
635
+ ? {
636
+ additionalData: {
637
+ context: {
638
+ pages: [
639
+ {
640
+ id: item.id,
641
+ language: item.language,
642
+ version: item.version,
643
+ name: editContext?.contentEditorItem?.name,
644
+ },
645
+ ],
646
+ componentIds: editContext?.selection?.length
647
+ ? editContext.selection
648
+ : undefined,
649
+ field: editContext?.focusedField?.fieldId &&
650
+ editContext.focusedField?.item?.id
651
+ ? {
652
+ fieldId: editContext.focusedField.fieldId,
653
+ itemId: editContext.focusedField.item.id,
654
+ name: editContext.focusedField.fieldName,
655
+ }
656
+ : undefined,
657
+ },
658
+ },
659
+ }
660
+ : null;
661
+ if (initialLocalContext)
662
+ setAgentMetadata(initialLocalContext);
663
+ }
577
664
  return;
578
665
  }
579
666
  setIsLoading(true);
@@ -639,6 +726,66 @@ export function AgentTerminal({ agentStub }) {
639
726
  useEffect(() => {
640
727
  loadAgent();
641
728
  }, [loadAgent]);
729
+ // Focus prompt when requested globally (from AI command)
730
+ useEffect(() => {
731
+ const focusHandler = () => {
732
+ try {
733
+ if (textareaRef.current) {
734
+ textareaRef.current.focus();
735
+ // Move caret to end
736
+ const value = textareaRef.current.value || "";
737
+ textareaRef.current.selectionStart = value.length;
738
+ textareaRef.current.selectionEnd = value.length;
739
+ }
740
+ }
741
+ catch { }
742
+ };
743
+ window.addEventListener("editor:focusAgentPrompt", focusHandler);
744
+ return () => window.removeEventListener("editor:focusAgentPrompt", focusHandler);
745
+ }, []);
746
+ // Load AI profiles for predefined prompts
747
+ useEffect(() => {
748
+ let cancelled = false;
749
+ (async () => {
750
+ try {
751
+ if (!editContext?.currentItemDescriptor)
752
+ return;
753
+ const fetched = await loadAiProfiles(editContext.currentItemDescriptor);
754
+ if (cancelled)
755
+ return;
756
+ setProfiles(fetched || []);
757
+ }
758
+ catch (e) {
759
+ console.error("Failed to load AI profiles", e);
760
+ }
761
+ })();
762
+ return () => {
763
+ cancelled = true;
764
+ };
765
+ }, [editContext?.currentItemDescriptor]);
766
+ // Select active profile based on agent.profileId or default to first
767
+ useEffect(() => {
768
+ if (!profiles || profiles.length === 0)
769
+ return;
770
+ const candidate = agent?.profileId
771
+ ? (profiles.find((p) => p.id === agent?.profileId) ??
772
+ profiles[0])
773
+ : profiles[0];
774
+ if (candidate && (!activeProfile || activeProfile.id !== candidate.id)) {
775
+ setActiveProfile(candidate);
776
+ }
777
+ }, [profiles, agent?.profileId]);
778
+ // Update selected model when the active profile changes
779
+ useEffect(() => {
780
+ if (!activeProfile)
781
+ return;
782
+ const agentModelId = agent?.model;
783
+ const availableModelIds = (activeProfile.models || []).map((m) => m.id);
784
+ const nextModelId = agentModelId && availableModelIds.includes(agentModelId)
785
+ ? agentModelId
786
+ : activeProfile.defaultModelId || activeProfile.models?.[0]?.id;
787
+ setSelectedModelId(nextModelId || undefined);
788
+ }, [activeProfile?.id]);
642
789
  // Cleanup stream connection when component unmounts or agent changes
643
790
  useEffect(() => {
644
791
  return () => {
@@ -699,8 +846,9 @@ export function AgentTerminal({ agentStub }) {
699
846
  agentId: agent.id,
700
847
  message: prompt.trim(),
701
848
  sessionId: editContext.sessionId,
702
- profileId: "default", // TODO: Get from context or settings
703
- profile: "default",
849
+ profileId: activeProfile?.id || "default",
850
+ profile: activeProfile?.name || "default",
851
+ model: selectedModelId,
704
852
  itemid: editContext.currentItemDescriptor?.id || "",
705
853
  language: editContext.currentItemDescriptor?.language || "en",
706
854
  version: editContext.currentItemDescriptor?.version || 1,
@@ -713,6 +861,8 @@ export function AgentTerminal({ agentStub }) {
713
861
  console.log("Starting agent:", request);
714
862
  // Set waiting state to show dancing dots immediately
715
863
  setIsWaitingForResponse(true);
864
+ // Start idle timer; dots appear only if no chunks for >1s
865
+ resetDotsTimer();
716
866
  // Re-enable auto-scroll when user submits a new message
717
867
  setShouldAutoScroll(true);
718
868
  // No need to create a temporary message - the stream will create messages as needed
@@ -784,6 +934,79 @@ export function AgentTerminal({ agentStub }) {
784
934
  }
785
935
  }
786
936
  };
937
+ // Send a message programmatically (used by quick-action buttons)
938
+ const sendQuickMessage = async (text) => {
939
+ if (!text.trim() || isSubmitting || !editContext)
940
+ return;
941
+ try {
942
+ setIsSubmitting(true);
943
+ setError(null);
944
+ const agentId = agent?.id;
945
+ if (!agentId)
946
+ return;
947
+ const userMessage = {
948
+ id: `user-${Date.now()}`,
949
+ agentId,
950
+ messageIndex: messages.length,
951
+ role: "user",
952
+ content: text.trim(),
953
+ name: "user",
954
+ messageType: "user",
955
+ isCompleted: true,
956
+ model: "",
957
+ tokensUsed: 0,
958
+ inputTokens: 0,
959
+ outputTokens: 0,
960
+ cachedInputTokens: 0,
961
+ inputTokenCost: 0,
962
+ outputTokenCost: 0,
963
+ cachedInputTokenCost: 0,
964
+ totalCost: 0,
965
+ currency: "USD",
966
+ createdDate: new Date().toISOString(),
967
+ };
968
+ setMessages((prev) => [...prev, userMessage]);
969
+ const metaCtx = agentMetadata?.additionalData?.context;
970
+ const selectionFromCtx = metaCtx?.componentIds?.length
971
+ ? metaCtx.componentIds
972
+ : undefined;
973
+ const effectiveSelection = selectionFromCtx?.length
974
+ ? selectionFromCtx
975
+ : editContext.selection && editContext.selection.length > 0
976
+ ? editContext.selection
977
+ : undefined;
978
+ const request = {
979
+ agentId: agent.id,
980
+ message: text.trim(),
981
+ sessionId: editContext.sessionId,
982
+ profileId: activeProfile?.id || "default",
983
+ profile: activeProfile?.name || "default",
984
+ model: selectedModelId,
985
+ itemid: editContext.currentItemDescriptor?.id || "",
986
+ language: editContext.currentItemDescriptor?.language || "en",
987
+ version: editContext.currentItemDescriptor?.version || 1,
988
+ selection: effectiveSelection,
989
+ selectedText: editContext.selectedRange?.text || undefined,
990
+ addSelectedComponents: !!effectiveSelection?.length,
991
+ addContextContent: false,
992
+ addAllContent: false,
993
+ };
994
+ setIsWaitingForResponse(true);
995
+ resetDotsTimer();
996
+ setShouldAutoScroll(true);
997
+ await startAgent(request);
998
+ await connectToStream();
999
+ }
1000
+ catch (err) {
1001
+ console.error("Failed to submit quick message:", err);
1002
+ setError("Failed to submit prompt");
1003
+ setIsWaitingForResponse(false);
1004
+ setMessages((prev) => prev.slice(0, -1));
1005
+ }
1006
+ finally {
1007
+ setIsSubmitting(false);
1008
+ }
1009
+ };
787
1010
  // Context info bar helpers - must be declared before any early returns to keep hooks order stable
788
1011
  const removeContextKey = useCallback(async (key, index) => {
789
1012
  if (!agent?.id)
@@ -1046,11 +1269,58 @@ export function AgentTerminal({ agentStub }) {
1046
1269
  editContext?.currentItemDescriptor,
1047
1270
  editContext?.itemsRepository,
1048
1271
  ]);
1272
+ // Stop current execution/stream safely
1273
+ const handleStop = useCallback(() => {
1274
+ try {
1275
+ setIsWaitingForResponse(false);
1276
+ if (abortControllerRef.current) {
1277
+ abortControllerRef.current.abort();
1278
+ abortControllerRef.current = null;
1279
+ }
1280
+ setIsConnecting(false);
1281
+ setIsSubmitting(false);
1282
+ // Mark any in-progress streaming messages as completed in UI
1283
+ setMessages((prev) => {
1284
+ const updated = prev.map((msg) => !msg.isCompleted && msg.messageType === "streaming"
1285
+ ? { ...msg, isCompleted: true, messageType: "completed" }
1286
+ : msg);
1287
+ messagesRef.current = updated;
1288
+ return updated;
1289
+ });
1290
+ // Update indicator state
1291
+ resetDotsTimer();
1292
+ }
1293
+ catch (e) {
1294
+ console.error("Failed to stop agent execution", e);
1295
+ }
1296
+ }, [resetDotsTimer]);
1297
+ // Ensure waiting state resets when no active work remains
1298
+ useEffect(() => {
1299
+ if (isSubmitting || isConnecting)
1300
+ return;
1301
+ const streaming = hasActiveStreaming();
1302
+ if (!streaming && isWaitingForResponse) {
1303
+ setIsWaitingForResponse(false);
1304
+ resetDotsTimer();
1305
+ }
1306
+ }, [
1307
+ isSubmitting,
1308
+ isConnecting,
1309
+ messages,
1310
+ hasActiveStreaming,
1311
+ isWaitingForResponse,
1312
+ resetDotsTimer,
1313
+ ]);
1049
1314
  if (isLoading) {
1050
1315
  return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-500", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin", strokeWidth: 1 }), "Loading agent..."] }) }));
1051
1316
  }
1052
1317
  // Calculate total token usage for cost display
1053
1318
  const totalTokens = calculateTotalTokens(messages);
1319
+ // Determine if the agent is actively executing (submitting, connecting, waiting, or streaming)
1320
+ const isExecuting = isSubmitting ||
1321
+ isConnecting ||
1322
+ isWaitingForResponse ||
1323
+ hasActiveStreaming();
1054
1324
  const renderContextInfoBar = () => {
1055
1325
  const ctx = agentMetadata?.additionalData?.context;
1056
1326
  // Check if we have context chips or add buttons to show
@@ -1105,26 +1375,48 @@ export function AgentTerminal({ agentStub }) {
1105
1375
  }
1106
1376
  return (_jsx("div", { className: "border-t border-gray-100 px-4 py-2", children: _jsxs("div", { className: "flex items-center justify-between text-xs text-gray-600", children: [_jsx("div", { className: "flex flex-wrap items-center gap-2", children: chips }), _jsxs("div", { className: "ml-2 flex flex-wrap items-center gap-2", children: [canAddPage && (_jsxs(Button, { size: "sm", variant: "outline", onClick: addPagesToContext, children: [_jsx(Plus, { className: "mr-1 h-3 w-3", strokeWidth: 1 }), " Add current item"] })), canAddSelection && (_jsxs(Button, { size: "sm", variant: "outline", onClick: addSelectedComponentsToContext, children: [_jsx(Plus, { className: "mr-1 h-3 w-3", strokeWidth: 1 }), " Add selected components"] }))] })] }) }));
1107
1377
  };
1108
- return (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { ref: messagesContainerRef, className: "flex-1 overflow-y-auto", onScroll: handleScroll, children: [error && (_jsx("div", { className: "m-4 rounded-lg border-l-4 border-red-500 bg-red-50 p-3", children: _jsxs("div", { className: "flex items-start", children: [_jsx(AlertCircle, { className: "mt-0.5 h-5 w-5 text-red-400", strokeWidth: 1 }), _jsxs("div", { className: "ml-3", children: [_jsx("p", { className: "text-sm font-medium text-red-800", children: "Error" }), _jsx("p", { className: "mt-1 text-sm text-red-700", children: error })] })] }) })), messages.length === 0 && !error && (_jsx("div", { className: "flex h-full items-center justify-center p-8", children: _jsxs("div", { className: "text-center", children: [_jsx(Bot, { className: "mx-auto mb-4 h-12 w-12 text-gray-400", strokeWidth: 1 }), _jsx("h3", { className: "mb-2 text-lg font-medium text-gray-900", children: "Start a conversation" }), _jsx("p", { className: "mb-4 text-sm text-gray-500", children: "Send a message to begin working with your AI agent." }), _jsx("div", { className: "text-xs text-gray-400", children: "Your agent can help with content editing, research, and automation tasks." })] }) })), _jsxs("div", { className: "space-y-0 divide-y divide-gray-100 select-text", children: [groupConsecutiveMessages(messages).map((group, groupIndex) => {
1109
- if (group.type === "user" && group.messages[0]) {
1110
- // Render user message
1111
- return (_jsx(UserMessage, { message: group.messages[0] }, groupIndex));
1112
- }
1113
- else {
1114
- // Render bundled assistant messages
1115
- // Check if this group contains any streaming message
1116
- const hasStreamingMessage = group.messages.some((msg) => !msg.isCompleted && msg.messageType === "streaming");
1117
- const convertedMessages = convertAgentMessagesToAiFormat(group.messages);
1118
- return (_jsx(AiResponseMessage, { messages: convertedMessages, finished: !isSubmitting && !isConnecting, editOperations: [], error: error || undefined }, groupIndex));
1119
- }
1120
- }), (isWaitingForResponse ||
1121
- groupConsecutiveMessages(messages).some((group) => group.type === "assistant-group" &&
1122
- group.messages.some((msg) => !msg.isCompleted && msg.messageType === "streaming"))) && _jsx(DancingDots, {})] }), _jsx("div", { ref: messagesEndRef })] }), totalTokens.totalCost > 0 && (_jsx("div", { className: "border-t border-gray-100 px-4 py-2", children: _jsx(AgentCostDisplay, { totalTokens: totalTokens, className: "flex justify-end" }) })), renderContextInfoBar(), _jsx("div", { className: "border-t border-gray-200 p-4", children: _jsxs("div", { className: "flex items-stretch gap-2", children: [_jsx(Textarea, { ref: textareaRef, value: prompt, onChange: (e) => {
1378
+ return (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { ref: messagesContainerRef, className: "flex-1 overflow-y-auto", onScroll: handleScroll, children: [error && (_jsx("div", { className: "m-4 rounded-lg border-l-4 border-red-500 bg-red-50 p-3", children: _jsxs("div", { className: "flex items-start", children: [_jsx(AlertCircle, { className: "mt-0.5 h-5 w-5 text-red-400", strokeWidth: 1 }), _jsxs("div", { className: "ml-3", children: [_jsx("p", { className: "text-sm font-medium text-red-800", children: "Error" }), _jsx("p", { className: "mt-1 text-sm text-red-700", children: error })] })] }) })), messages.length === 0 && !error && (_jsx("div", { className: "flex h-full items-center justify-center p-8", children: _jsxs("div", { className: "text-center", children: [_jsx(Bot, { className: "mx-auto mb-4 h-12 w-12 text-gray-400", strokeWidth: 1 }), _jsx("h3", { className: "mb-2 text-lg font-medium text-gray-900", children: "Start a conversation" }), _jsx("p", { className: "mb-4 text-sm text-gray-500", children: "Send a message to begin working with your AI agent." }), _jsx("div", { className: "text-xs text-gray-400", children: "Your agent can help with content editing, research, and automation tasks." })] }) })), _jsx("div", { className: "space-y-0 divide-y divide-gray-100 select-text", children: groupConsecutiveMessages(messages).map((group, groupIndex) => {
1379
+ if (group.type === "user" && group.messages[0]) {
1380
+ // Render user message
1381
+ return (_jsx(UserMessage, { message: group.messages[0] }, groupIndex));
1382
+ }
1383
+ else {
1384
+ // Render bundled assistant messages
1385
+ // Check if this group contains any streaming message
1386
+ const hasStreamingMessage = group.messages.some((msg) => !msg.isCompleted && msg.messageType === "streaming");
1387
+ const convertedMessages = convertAgentMessagesToAiFormat(group.messages);
1388
+ return (_jsx(AiResponseMessage, { messages: convertedMessages, finished: !isSubmitting && !isConnecting, editOperations: [], error: error || undefined, onQuickAction: (action) => {
1389
+ const text = (action.prompt ||
1390
+ action.value ||
1391
+ action.label ||
1392
+ "").trim();
1393
+ if (!text)
1394
+ return;
1395
+ // Stop any current execution before sending the next message
1396
+ if (isExecuting) {
1397
+ try {
1398
+ handleStop();
1399
+ }
1400
+ catch { }
1401
+ }
1402
+ sendQuickMessage(text);
1403
+ } }, groupIndex));
1404
+ }
1405
+ }) }), showDots && _jsx(DancingDots, {}), _jsx("div", { ref: messagesEndRef })] }), renderContextInfoBar(), _jsxs("div", { className: "border-t border-gray-200 p-4", children: [_jsx("div", { className: "flex items-stretch gap-2", children: _jsx(Textarea, { ref: textareaRef, value: prompt, onChange: (e) => {
1123
1406
  setPrompt(e.target.value);
1124
1407
  // Reset history index when user starts typing
1125
1408
  if (currentHistoryIndex !== -1) {
1126
1409
  setCurrentHistoryIndex(-1);
1127
1410
  }
1128
- }, onKeyDown: handleKeyPress, placeholder: "Type your message... (Enter to send, Ctrl+Enter for new line)", className: "resize-nones min-h-[80px] flex-1 text-xs", disabled: isSubmitting }), _jsx(Button, { onClick: handleSubmit, disabled: !prompt.trim() || isSubmitting, size: "sm", className: "self-end", children: isSubmitting ? (_jsx(Loader2, { className: "h-4 w-4 animate-spin", strokeWidth: 1 })) : (_jsx(Send, { className: "h-4 w-4", strokeWidth: 1 })) })] }) })] }));
1411
+ }, onKeyDown: handleKeyPress, placeholder: "Type your message... (Enter to send, Ctrl+Enter for new line)", className: "resize-nones min-h-[80px] flex-1 text-xs", disabled: isSubmitting }) }), _jsxs("div", { className: "flex items-stretch justify-between gap-2", children: [_jsxs("div", { className: "mt-2 flex items-center justify-start gap-2", children: [profiles?.length > 0 && (_jsx("select", { className: "h-5 rounded border px-1.5 text-[10px] text-gray-500", value: activeProfile?.id || "", onChange: (e) => {
1412
+ const p = profiles.find((x) => x.id === e.target.value);
1413
+ if (p)
1414
+ setActiveProfile(p);
1415
+ }, title: "Profile", "aria-label": "Profile", children: profiles.map((p) => (_jsx("option", { value: p.id, children: p.name }, p.id))) })), activeProfile?.models?.length ? (_jsx("select", { className: "h-5 rounded border px-1.5 text-[10px] text-gray-500", value: selectedModelId || "", onChange: (e) => setSelectedModelId(e.target.value), title: "Model", "aria-label": "Model", children: activeProfile.models.map((m) => (_jsx("option", { value: m.id, children: m.name }, m.id))) })) : null, activeProfile?.prompts?.length ? (_jsxs(Popover, { open: showPredefined, onOpenChange: setShowPredefined, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { className: "rounded p-1 hover:bg-gray-100", onClick: () => { }, title: "Predefined prompts", "aria-label": "Predefined prompts", children: _jsx(Wand2, { className: "h-3 w-3", strokeWidth: 1 }) }) }), _jsx(PopoverContent, { className: "w-64 p-0", align: "start", children: _jsx("div", { className: "max-h-56 overflow-y-auto p-2", children: activeProfile.prompts.map((p, index) => (_jsx("div", { className: "cursor-pointer rounded p-1.5 text-xs text-gray-700 hover:bg-gray-100", onClick: () => {
1416
+ setPrompt(p.prompt);
1417
+ setShowPredefined(false);
1418
+ if (textareaRef.current)
1419
+ textareaRef.current.focus();
1420
+ }, children: p.title }, index))) }) })] })) : null, _jsx(AgentCostDisplay, { totalTokens: totalTokens })] }), _jsx(Button, { onClick: isExecuting ? handleStop : handleSubmit, disabled: !isExecuting && !prompt.trim(), size: "sm", className: "h-5.5 w-5.5 cursor-pointer self-end rounded-full", title: isExecuting ? "Stop" : "Send", "aria-label": isExecuting ? "Stop" : "Send", children: isExecuting ? (_jsx(Square, { className: "size-3", strokeWidth: 1 })) : (_jsx(Send, { className: "size-3", strokeWidth: 1 })) })] })] })] }));
1129
1421
  }
1130
1422
  //# sourceMappingURL=AgentTerminal.js.map