@alpaca-editor/core 1.0.4064 → 1.0.4066

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/editor/ContextMenu.js +0 -2
  2. package/dist/editor/ContextMenu.js.map +1 -1
  3. package/dist/editor/ImageEditButton.js +10 -3
  4. package/dist/editor/ImageEditButton.js.map +1 -1
  5. package/dist/editor/ai/AgentTerminal.d.ts +3 -2
  6. package/dist/editor/ai/AgentTerminal.js +386 -94
  7. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  8. package/dist/editor/ai/Agents.js +67 -25
  9. package/dist/editor/ai/Agents.js.map +1 -1
  10. package/dist/editor/ai/AiResponseMessage.d.ts +6 -1
  11. package/dist/editor/ai/AiResponseMessage.js +63 -3
  12. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  13. package/dist/editor/ai/AiTerminal.js +27 -2
  14. package/dist/editor/ai/AiTerminal.js.map +1 -1
  15. package/dist/editor/client/EditorClient.js +32 -19
  16. package/dist/editor/client/EditorClient.js.map +1 -1
  17. package/dist/editor/client/editContext.d.ts +4 -2
  18. package/dist/editor/client/editContext.js.map +1 -1
  19. package/dist/editor/client/operations.js +9 -6
  20. package/dist/editor/client/operations.js.map +1 -1
  21. package/dist/editor/commands/componentCommands.js +57 -7
  22. package/dist/editor/commands/componentCommands.js.map +1 -1
  23. package/dist/editor/field-types/richtext/contextMenuFactory.js +0 -3
  24. package/dist/editor/field-types/richtext/contextMenuFactory.js.map +1 -1
  25. package/dist/editor/menubar/ToolbarFactory.js +5 -2
  26. package/dist/editor/menubar/ToolbarFactory.js.map +1 -1
  27. package/dist/editor/menubar/toolbar-sections/UtilityControls.js +1 -1
  28. package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
  29. package/dist/editor/page-editor-chrome/CommentHighlighting.js +6 -4
  30. package/dist/editor/page-editor-chrome/CommentHighlighting.js.map +1 -1
  31. package/dist/editor/page-editor-chrome/CommentHighlightings.js +1 -1
  32. package/dist/editor/page-editor-chrome/CommentHighlightings.js.map +1 -1
  33. package/dist/editor/page-editor-chrome/FrameMenu.js +6 -8
  34. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  35. package/dist/editor/page-viewer/PageViewerFrame.js +70 -4
  36. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  37. package/dist/editor/reviews/Comment.js +3 -58
  38. package/dist/editor/reviews/Comment.js.map +1 -1
  39. package/dist/editor/reviews/CommentDisplayPopover.js +2 -3
  40. package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
  41. package/dist/editor/reviews/CommentEditor.js +2 -2
  42. package/dist/editor/reviews/CommentEditor.js.map +1 -1
  43. package/dist/editor/reviews/Comments.js +4 -0
  44. package/dist/editor/reviews/Comments.js.map +1 -1
  45. package/dist/editor/reviews/Reviews.js +2 -2
  46. package/dist/editor/reviews/Reviews.js.map +1 -1
  47. package/dist/editor/reviews/commentAi.d.ts +7 -0
  48. package/dist/editor/reviews/commentAi.js +86 -0
  49. package/dist/editor/reviews/commentAi.js.map +1 -0
  50. package/dist/editor/sidebar/ComponentTree.js +157 -49
  51. package/dist/editor/sidebar/ComponentTree.js.map +1 -1
  52. package/dist/editor/sidebar/Debug.js +1 -1
  53. package/dist/editor/sidebar/Debug.js.map +1 -1
  54. package/dist/revision.d.ts +2 -2
  55. package/dist/revision.js +2 -2
  56. package/dist/styles.css +15 -4
  57. package/dist/types.d.ts +1 -1
  58. package/package.json +1 -1
  59. package/src/editor/ContextMenu.tsx +0 -2
  60. package/src/editor/ImageEditButton.tsx +36 -8
  61. package/src/editor/ai/AgentTerminal.tsx +436 -65
  62. package/src/editor/ai/Agents.tsx +217 -117
  63. package/src/editor/ai/AiResponseMessage.tsx +106 -2
  64. package/src/editor/ai/AiTerminal.tsx +27 -0
  65. package/src/editor/client/EditorClient.tsx +41 -20
  66. package/src/editor/client/editContext.ts +4 -2
  67. package/src/editor/client/operations.ts +9 -8
  68. package/src/editor/commands/componentCommands.tsx +61 -13
  69. package/src/editor/field-types/richtext/components/EditorDropdown.css +1 -0
  70. package/src/editor/field-types/richtext/contextMenuFactory.tsx +0 -4
  71. package/src/editor/menubar/ToolbarFactory.tsx +6 -2
  72. package/src/editor/menubar/toolbar-sections/UtilityControls.tsx +23 -19
  73. package/src/editor/page-editor-chrome/CommentHighlighting.tsx +6 -4
  74. package/src/editor/page-editor-chrome/CommentHighlightings.tsx +3 -1
  75. package/src/editor/page-editor-chrome/FrameMenu.tsx +6 -8
  76. package/src/editor/page-viewer/PageViewerFrame.tsx +80 -4
  77. package/src/editor/reviews/Comment.tsx +4 -66
  78. package/src/editor/reviews/CommentDisplayPopover.tsx +2 -3
  79. package/src/editor/reviews/CommentEditor.tsx +2 -2
  80. package/src/editor/reviews/Comments.tsx +12 -0
  81. package/src/editor/reviews/Reviews.tsx +2 -0
  82. package/src/editor/reviews/commentAi.ts +106 -0
  83. package/src/editor/sidebar/ComponentTree.tsx +223 -69
  84. package/src/editor/sidebar/Debug.tsx +1 -1
  85. package/src/revision.ts +2 -2
  86. package/src/types.ts +1 -1
  87. package/styles.css +0 -5
  88. package/dist/editor/ai/AiPromptPopover.d.ts +0 -7
  89. package/dist/editor/ai/AiPromptPopover.js +0 -111
  90. package/dist/editor/ai/AiPromptPopover.js.map +0 -1
  91. package/src/editor/ai/AiPromptPopover.tsx +0 -206
@@ -11,6 +11,8 @@ import {
11
11
  Type,
12
12
  Plus,
13
13
  MessageSquare,
14
+ Wand2,
15
+ Square,
14
16
  } from "lucide-react";
15
17
  import { DancingDots } from "./DancingDots";
16
18
  import {
@@ -33,6 +35,12 @@ import { AgentCostDisplay } from "./AgentCostDisplay";
33
35
  import { Message } from "./AiTerminal";
34
36
  import { getComponentById } from "../componentTreeHelper";
35
37
  import { Comment } from "../../types";
38
+ import { AiProfile, loadAiProfiles } from "../services/aiService";
39
+ import {
40
+ Popover,
41
+ PopoverContent,
42
+ PopoverTrigger,
43
+ } from "../../components/ui/popover";
36
44
 
37
45
  // Simple user message component
38
46
  const UserMessage = ({ message }: { message: AgentChatMessage }) => {
@@ -168,7 +176,13 @@ const convertAgentMessagesToAiFormat = (
168
176
  // agentStub: Agent;
169
177
  // }
170
178
 
171
- export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
179
+ export function AgentTerminal({
180
+ agentStub,
181
+ initialMetadata,
182
+ }: {
183
+ agentStub: Agent;
184
+ initialMetadata?: AgentMetadata;
185
+ }) {
172
186
  const editContext = useEditContext();
173
187
  const [agent, setAgent] = useState<AgentDetails | undefined>(undefined);
174
188
  const [messages, setMessages] = useState<AgentChatMessage[]>([]);
@@ -180,6 +194,37 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
180
194
  null,
181
195
  );
182
196
  const [isWaitingForResponse, setIsWaitingForResponse] = useState(false);
197
+ const isWaitingRef = useRef<boolean>(false);
198
+ useEffect(() => {
199
+ isWaitingRef.current = isWaitingForResponse;
200
+ }, [isWaitingForResponse]);
201
+
202
+ // Dots visibility controlled by 1s idle timer since last incremental update
203
+ const [showDots, setShowDots] = useState(false);
204
+ const dotsTimeoutRef = useRef<any>(null);
205
+ const hasActiveStreaming = useCallback(() => {
206
+ const current = messagesRef.current || [];
207
+ return current.some((m) => !m.isCompleted && m.messageType === "streaming");
208
+ }, []);
209
+ const resetDotsTimer = useCallback(() => {
210
+ if (dotsTimeoutRef.current) {
211
+ clearTimeout(dotsTimeoutRef.current);
212
+ dotsTimeoutRef.current = null;
213
+ }
214
+ const waiting = isWaitingRef.current;
215
+ const streaming = hasActiveStreaming();
216
+ if (!waiting && !streaming) {
217
+ setShowDots(false);
218
+ return;
219
+ }
220
+ setShowDots(false);
221
+ dotsTimeoutRef.current = setTimeout(() => {
222
+ // Re-check conditions after 1s of inactivity
223
+ const stillWaiting = isWaitingRef.current;
224
+ const stillStreaming = hasActiveStreaming();
225
+ setShowDots(stillWaiting || stillStreaming);
226
+ }, 1000);
227
+ }, [hasActiveStreaming]);
183
228
  const [resolvedPageName, setResolvedPageName] = useState<string | undefined>(
184
229
  undefined,
185
230
  );
@@ -198,6 +243,14 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
198
243
  return [];
199
244
  });
200
245
  const [currentHistoryIndex, setCurrentHistoryIndex] = useState<number>(-1);
246
+ const [showPredefined, setShowPredefined] = useState(false);
247
+ const [profiles, setProfiles] = useState<AiProfile[]>([]);
248
+ const [activeProfile, setActiveProfile] = useState<AiProfile | undefined>(
249
+ undefined,
250
+ );
251
+ const [selectedModelId, setSelectedModelId] = useState<string | undefined>(
252
+ undefined,
253
+ );
201
254
 
202
255
  useEffect(() => {
203
256
  localStorage.setItem(
@@ -206,6 +259,21 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
206
259
  );
207
260
  }, [promptHistory]);
208
261
 
262
+ // Clear idle timer on unmount
263
+ useEffect(() => {
264
+ return () => {
265
+ if (dotsTimeoutRef.current) {
266
+ clearTimeout(dotsTimeoutRef.current);
267
+ dotsTimeoutRef.current = null;
268
+ }
269
+ };
270
+ }, []);
271
+
272
+ // Whenever waiting state changes, restart idle timer logic
273
+ useEffect(() => {
274
+ resetDotsTimer();
275
+ }, [isWaitingForResponse, resetDotsTimer]);
276
+
209
277
  useEffect(() => {
210
278
  // Keep messagesRef synchronized with messages state
211
279
  messagesRef.current = messages;
@@ -303,6 +371,8 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
303
371
 
304
372
  // Clear waiting state when first content chunk arrives
305
373
  setIsWaitingForResponse(false);
374
+ // Any content chunk is an incremental update -> reset idle timer
375
+ resetDotsTimer();
306
376
 
307
377
  // Always call setMessages and handle all logic in the callback with latest messages
308
378
  setMessages((prev) => {
@@ -471,6 +541,8 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
471
541
  messagesRef.current = updated;
472
542
  return updated;
473
543
  });
544
+ // Tool call activity counts as activity; keep dots hidden for 1s
545
+ resetDotsTimer();
474
546
  }
475
547
  },
476
548
  [createNewStreamMessage],
@@ -572,8 +644,10 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
572
644
  messagesRef.current = updated;
573
645
  return updated;
574
646
  });
647
+ // Tool result activity; reset idle timer
648
+ resetDotsTimer();
575
649
  },
576
- [],
650
+ [resetDotsTimer],
577
651
  );
578
652
 
579
653
  // Connect to agent stream for real-time updates
@@ -625,8 +699,8 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
625
699
 
626
700
  // Mark the specific message as completed by messageId
627
701
  if (completedMessageId) {
628
- setMessages((prev) =>
629
- prev.map((msg) => {
702
+ setMessages((prev) => {
703
+ const updated = prev.map((msg) => {
630
704
  if (msg.id === completedMessageId) {
631
705
  const updatedMessage = {
632
706
  ...msg,
@@ -669,15 +743,17 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
669
743
  return updatedMessage;
670
744
  }
671
745
  return msg;
672
- }),
673
- );
746
+ });
747
+ messagesRef.current = updated;
748
+ return updated;
749
+ });
674
750
  } else {
675
751
  // Fallback: Mark any streaming messages as completed (old behavior)
676
752
  console.warn(
677
753
  "⚠️ No messageId in completed event, falling back to marking all streaming messages as completed",
678
754
  );
679
- setMessages((prev) =>
680
- prev.map((msg) =>
755
+ setMessages((prev) => {
756
+ const updated = prev.map((msg) =>
681
757
  !msg.isCompleted && msg.messageType === "streaming"
682
758
  ? {
683
759
  ...msg,
@@ -685,10 +761,16 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
685
761
  messageType: "completed",
686
762
  }
687
763
  : msg,
688
- ),
689
- );
764
+ );
765
+ messagesRef.current = updated;
766
+ return updated;
767
+ });
690
768
  }
769
+ // Ensure waiting state is cleared when stream completes
770
+ setIsWaitingForResponse(false);
691
771
  shouldCreateNewMessage.current = false;
772
+ // Streaming finished; update indicator
773
+ resetDotsTimer();
692
774
  break;
693
775
 
694
776
  case "error":
@@ -696,6 +778,8 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
696
778
  setError(message.error || "Stream error occurred");
697
779
  setIsWaitingForResponse(false);
698
780
  shouldCreateNewMessage.current = false;
781
+ // Error ends streaming; update indicator
782
+ resetDotsTimer();
699
783
  break;
700
784
 
701
785
  default:
@@ -720,6 +804,8 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
720
804
  }
721
805
  } finally {
722
806
  setIsConnecting(false);
807
+ // Guard: clear waiting state if connection finished without content
808
+ setIsWaitingForResponse(false);
723
809
  }
724
810
  },
725
811
  [agent?.id, handleContentChunk, handleToolCall, handleToolResult],
@@ -762,35 +848,49 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
762
848
  setIsLoading(false);
763
849
 
764
850
  // Initialize local context for a brand-new agent (not yet persisted)
765
- const item = editContext?.currentItemDescriptor;
766
- const initialLocalContext: AgentMetadata | null = item
767
- ? {
768
- additionalData: {
769
- context: {
770
- pages: [
771
- {
772
- id: item.id,
773
- language: item.language,
774
- version: item.version,
775
- name: editContext?.contentEditorItem?.name,
776
- },
777
- ],
778
- componentIds: editContext?.selection?.length
779
- ? editContext.selection
780
- : undefined,
781
- field:
782
- editContext?.focusedField &&
783
- editContext.currentItemDescriptor
784
- ? {
785
- fieldId: editContext.focusedField.fieldId,
786
- itemId: editContext.focusedField.item.id,
787
- }
851
+ // Prefer explicitly provided metadata; otherwise seed with page, selected componentIds, and focused field if available
852
+ if (initialMetadata) {
853
+ setAgentMetadata(initialMetadata);
854
+ // If an initial prompt is provided via metadata, seed the input once
855
+ try {
856
+ const maybePrompt = (initialMetadata as any)?.additionalData
857
+ ?.initialPrompt;
858
+ if (typeof maybePrompt === "string" && maybePrompt.trim()) {
859
+ setPrompt(maybePrompt);
860
+ }
861
+ } catch {}
862
+ } else {
863
+ const item = editContext?.currentItemDescriptor;
864
+ const initialLocalContext: AgentMetadata | null = item
865
+ ? {
866
+ additionalData: {
867
+ context: {
868
+ pages: [
869
+ {
870
+ id: item.id,
871
+ language: item.language,
872
+ version: item.version,
873
+ name: editContext?.contentEditorItem?.name,
874
+ },
875
+ ],
876
+ componentIds: editContext?.selection?.length
877
+ ? editContext.selection
788
878
  : undefined,
879
+ field:
880
+ editContext?.focusedField?.fieldId &&
881
+ (editContext.focusedField as any)?.item?.id
882
+ ? {
883
+ fieldId: editContext.focusedField.fieldId,
884
+ itemId: (editContext.focusedField as any).item.id,
885
+ name: (editContext.focusedField as any).fieldName,
886
+ }
887
+ : undefined,
888
+ },
789
889
  },
790
- },
791
- }
792
- : null;
793
- if (initialLocalContext) setAgentMetadata(initialLocalContext);
890
+ }
891
+ : null;
892
+ if (initialLocalContext) setAgentMetadata(initialLocalContext);
893
+ }
794
894
  return;
795
895
  }
796
896
 
@@ -867,6 +967,72 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
867
967
  loadAgent();
868
968
  }, [loadAgent]);
869
969
 
970
+ // Focus prompt when requested globally (from AI command)
971
+ useEffect(() => {
972
+ const focusHandler = () => {
973
+ try {
974
+ if (textareaRef.current) {
975
+ textareaRef.current.focus();
976
+ // Move caret to end
977
+ const value = textareaRef.current.value || "";
978
+ textareaRef.current.selectionStart = value.length;
979
+ textareaRef.current.selectionEnd = value.length;
980
+ }
981
+ } catch {}
982
+ };
983
+ window.addEventListener(
984
+ "editor:focusAgentPrompt",
985
+ focusHandler as EventListener,
986
+ );
987
+ return () =>
988
+ window.removeEventListener(
989
+ "editor:focusAgentPrompt",
990
+ focusHandler as EventListener,
991
+ );
992
+ }, []);
993
+
994
+ // Load AI profiles for predefined prompts
995
+ useEffect(() => {
996
+ let cancelled = false;
997
+ (async () => {
998
+ try {
999
+ if (!editContext?.currentItemDescriptor) return;
1000
+ const fetched = await loadAiProfiles(editContext.currentItemDescriptor);
1001
+ if (cancelled) return;
1002
+ setProfiles(fetched || []);
1003
+ } catch (e) {
1004
+ console.error("Failed to load AI profiles", e);
1005
+ }
1006
+ })();
1007
+ return () => {
1008
+ cancelled = true;
1009
+ };
1010
+ }, [editContext?.currentItemDescriptor]);
1011
+
1012
+ // Select active profile based on agent.profileId or default to first
1013
+ useEffect(() => {
1014
+ if (!profiles || profiles.length === 0) return;
1015
+ const candidate = agent?.profileId
1016
+ ? (profiles.find((p) => p.id === (agent?.profileId as any)) ??
1017
+ profiles[0])
1018
+ : profiles[0];
1019
+ if (candidate && (!activeProfile || activeProfile.id !== candidate.id)) {
1020
+ setActiveProfile(candidate);
1021
+ }
1022
+ }, [profiles, agent?.profileId]);
1023
+
1024
+ // Update selected model when the active profile changes
1025
+ useEffect(() => {
1026
+ if (!activeProfile) return;
1027
+ const agentModelId = agent?.model;
1028
+ const availableModelIds = (activeProfile.models || []).map((m) => m.id);
1029
+ const nextModelId =
1030
+ agentModelId && availableModelIds.includes(agentModelId)
1031
+ ? agentModelId
1032
+ : activeProfile.defaultModelId || activeProfile.models?.[0]?.id;
1033
+ setSelectedModelId(nextModelId || undefined);
1034
+ }, [activeProfile?.id]);
1035
+
870
1036
  // Cleanup stream connection when component unmounts or agent changes
871
1037
  useEffect(() => {
872
1038
  return () => {
@@ -935,8 +1101,9 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
935
1101
  agentId: agent.id,
936
1102
  message: prompt.trim(),
937
1103
  sessionId: editContext.sessionId,
938
- profileId: "default", // TODO: Get from context or settings
939
- profile: "default",
1104
+ profileId: activeProfile?.id || "default",
1105
+ profile: activeProfile?.name || "default",
1106
+ model: selectedModelId,
940
1107
  itemid: editContext.currentItemDescriptor?.id || "",
941
1108
  language: editContext.currentItemDescriptor?.language || "en",
942
1109
  version: editContext.currentItemDescriptor?.version || 1,
@@ -951,6 +1118,8 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
951
1118
 
952
1119
  // Set waiting state to show dancing dots immediately
953
1120
  setIsWaitingForResponse(true);
1121
+ // Start idle timer; dots appear only if no chunks for >1s
1122
+ resetDotsTimer();
954
1123
 
955
1124
  // Re-enable auto-scroll when user submits a new message
956
1125
  setShouldAutoScroll(true);
@@ -1031,6 +1200,86 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
1031
1200
  }
1032
1201
  };
1033
1202
 
1203
+ // Send a message programmatically (used by quick-action buttons)
1204
+ const sendQuickMessage = async (text: string) => {
1205
+ if (!text.trim() || isSubmitting || !editContext) return;
1206
+
1207
+ try {
1208
+ setIsSubmitting(true);
1209
+ setError(null);
1210
+ const agentId = agent?.id;
1211
+ if (!agentId) return;
1212
+
1213
+ const userMessage: AgentChatMessage = {
1214
+ id: `user-${Date.now()}`,
1215
+ agentId,
1216
+ messageIndex: messages.length,
1217
+ role: "user",
1218
+ content: text.trim(),
1219
+ name: "user",
1220
+ messageType: "user",
1221
+ isCompleted: true,
1222
+ model: "",
1223
+ tokensUsed: 0,
1224
+ inputTokens: 0,
1225
+ outputTokens: 0,
1226
+ cachedInputTokens: 0,
1227
+ inputTokenCost: 0,
1228
+ outputTokenCost: 0,
1229
+ cachedInputTokenCost: 0,
1230
+ totalCost: 0,
1231
+ currency: "USD",
1232
+ createdDate: new Date().toISOString(),
1233
+ };
1234
+
1235
+ setMessages((prev) => [...prev, userMessage]);
1236
+
1237
+ const metaCtx = (agentMetadata as any)?.additionalData?.context as
1238
+ | { componentIds?: string[] }
1239
+ | undefined;
1240
+ const selectionFromCtx = metaCtx?.componentIds?.length
1241
+ ? metaCtx.componentIds
1242
+ : undefined;
1243
+ const effectiveSelection = selectionFromCtx?.length
1244
+ ? selectionFromCtx
1245
+ : editContext.selection && editContext.selection.length > 0
1246
+ ? editContext.selection
1247
+ : undefined;
1248
+
1249
+ const request: StartAgentRequest = {
1250
+ agentId: agent.id,
1251
+ message: text.trim(),
1252
+ sessionId: editContext.sessionId,
1253
+ profileId: activeProfile?.id || "default",
1254
+ profile: activeProfile?.name || "default",
1255
+ model: selectedModelId,
1256
+ itemid: editContext.currentItemDescriptor?.id || "",
1257
+ language: editContext.currentItemDescriptor?.language || "en",
1258
+ version: editContext.currentItemDescriptor?.version || 1,
1259
+ selection: effectiveSelection,
1260
+ selectedText: editContext.selectedRange?.text || undefined,
1261
+ addSelectedComponents: !!effectiveSelection?.length,
1262
+ addContextContent: false,
1263
+ addAllContent: false,
1264
+ };
1265
+
1266
+ setIsWaitingForResponse(true);
1267
+ resetDotsTimer();
1268
+ setShouldAutoScroll(true);
1269
+
1270
+ await startAgent(request);
1271
+
1272
+ await connectToStream();
1273
+ } catch (err) {
1274
+ console.error("Failed to submit quick message:", err);
1275
+ setError("Failed to submit prompt");
1276
+ setIsWaitingForResponse(false);
1277
+ setMessages((prev) => prev.slice(0, -1));
1278
+ } finally {
1279
+ setIsSubmitting(false);
1280
+ }
1281
+ };
1282
+
1034
1283
  // Context info bar helpers - must be declared before any early returns to keep hooks order stable
1035
1284
  const removeContextKey = useCallback(
1036
1285
  async (
@@ -1336,6 +1585,50 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
1336
1585
  editContext?.itemsRepository,
1337
1586
  ]);
1338
1587
 
1588
+ // Stop current execution/stream safely
1589
+ const handleStop = useCallback(() => {
1590
+ try {
1591
+ setIsWaitingForResponse(false);
1592
+ if (abortControllerRef.current) {
1593
+ abortControllerRef.current.abort();
1594
+ abortControllerRef.current = null;
1595
+ }
1596
+ setIsConnecting(false);
1597
+ setIsSubmitting(false);
1598
+ // Mark any in-progress streaming messages as completed in UI
1599
+ setMessages((prev) => {
1600
+ const updated = prev.map((msg) =>
1601
+ !msg.isCompleted && msg.messageType === "streaming"
1602
+ ? { ...msg, isCompleted: true, messageType: "completed" as const }
1603
+ : msg,
1604
+ );
1605
+ messagesRef.current = updated;
1606
+ return updated;
1607
+ });
1608
+ // Update indicator state
1609
+ resetDotsTimer();
1610
+ } catch (e) {
1611
+ console.error("Failed to stop agent execution", e);
1612
+ }
1613
+ }, [resetDotsTimer]);
1614
+
1615
+ // Ensure waiting state resets when no active work remains
1616
+ useEffect(() => {
1617
+ if (isSubmitting || isConnecting) return;
1618
+ const streaming = hasActiveStreaming();
1619
+ if (!streaming && isWaitingForResponse) {
1620
+ setIsWaitingForResponse(false);
1621
+ resetDotsTimer();
1622
+ }
1623
+ }, [
1624
+ isSubmitting,
1625
+ isConnecting,
1626
+ messages,
1627
+ hasActiveStreaming,
1628
+ isWaitingForResponse,
1629
+ resetDotsTimer,
1630
+ ]);
1631
+
1339
1632
  if (isLoading) {
1340
1633
  return (
1341
1634
  <div className="flex h-full items-center justify-center">
@@ -1350,6 +1643,13 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
1350
1643
  // Calculate total token usage for cost display
1351
1644
  const totalTokens = calculateTotalTokens(messages);
1352
1645
 
1646
+ // Determine if the agent is actively executing (submitting, connecting, waiting, or streaming)
1647
+ const isExecuting =
1648
+ isSubmitting ||
1649
+ isConnecting ||
1650
+ isWaitingForResponse ||
1651
+ hasActiveStreaming();
1652
+
1353
1653
  const renderContextInfoBar = () => {
1354
1654
  const ctx = (agentMetadata as any)?.additionalData?.context as
1355
1655
  | {
@@ -1574,35 +1874,34 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
1574
1874
  finished={!isSubmitting && !isConnecting}
1575
1875
  editOperations={[]}
1576
1876
  error={error || undefined}
1877
+ onQuickAction={(action) => {
1878
+ const text = (
1879
+ action.prompt ||
1880
+ action.value ||
1881
+ action.label ||
1882
+ ""
1883
+ ).trim();
1884
+ if (!text) return;
1885
+ // Stop any current execution before sending the next message
1886
+ if (isExecuting) {
1887
+ try {
1888
+ handleStop();
1889
+ } catch {}
1890
+ }
1891
+ sendQuickMessage(text);
1892
+ }}
1577
1893
  />
1578
1894
  );
1579
1895
  }
1580
1896
  })}
1581
-
1582
- {/* Show dancing dots when waiting for response or streaming */}
1583
- {(isWaitingForResponse ||
1584
- groupConsecutiveMessages(messages).some(
1585
- (group) =>
1586
- group.type === "assistant-group" &&
1587
- group.messages.some(
1588
- (msg) => !msg.isCompleted && msg.messageType === "streaming",
1589
- ),
1590
- )) && <DancingDots />}
1591
1897
  </div>
1592
1898
 
1899
+ {/* Show dancing dots only after 1s of inactivity */}
1900
+ {showDots && <DancingDots />}
1901
+
1593
1902
  <div ref={messagesEndRef} />
1594
1903
  </div>
1595
1904
 
1596
- {/* Cost Display */}
1597
- {totalTokens.totalCost > 0 && (
1598
- <div className="border-t border-gray-100 px-4 py-2">
1599
- <AgentCostDisplay
1600
- totalTokens={totalTokens}
1601
- className="flex justify-end"
1602
- />
1603
- </div>
1604
- )}
1605
-
1606
1905
  {/* Context Info Bar */}
1607
1906
  {renderContextInfoBar()}
1608
1907
 
@@ -1624,16 +1923,88 @@ export function AgentTerminal({ agentStub }: { agentStub: Agent }) {
1624
1923
  className="resize-nones min-h-[80px] flex-1 text-xs"
1625
1924
  disabled={isSubmitting}
1626
1925
  />
1926
+ </div>
1927
+ <div className="flex items-stretch justify-between gap-2">
1928
+ {/* Profile/Model selectors below the input */}
1929
+ <div className="mt-2 flex items-center justify-start gap-2">
1930
+ {profiles?.length > 0 && (
1931
+ <select
1932
+ className="h-5 rounded border px-1.5 text-[10px] text-gray-500"
1933
+ value={activeProfile?.id || ""}
1934
+ onChange={(e) => {
1935
+ const p = profiles.find((x) => x.id === e.target.value);
1936
+ if (p) setActiveProfile(p);
1937
+ }}
1938
+ title="Profile"
1939
+ aria-label="Profile"
1940
+ >
1941
+ {profiles.map((p) => (
1942
+ <option key={p.id} value={p.id}>
1943
+ {p.name}
1944
+ </option>
1945
+ ))}
1946
+ </select>
1947
+ )}
1948
+ {activeProfile?.models?.length ? (
1949
+ <select
1950
+ className="h-5 rounded border px-1.5 text-[10px] text-gray-500"
1951
+ value={selectedModelId || ""}
1952
+ onChange={(e) => setSelectedModelId(e.target.value)}
1953
+ title="Model"
1954
+ aria-label="Model"
1955
+ >
1956
+ {activeProfile.models.map((m) => (
1957
+ <option key={m.id} value={m.id}>
1958
+ {m.name}
1959
+ </option>
1960
+ ))}
1961
+ </select>
1962
+ ) : null}
1963
+ {activeProfile?.prompts?.length ? (
1964
+ <Popover open={showPredefined} onOpenChange={setShowPredefined}>
1965
+ <PopoverTrigger asChild>
1966
+ <button
1967
+ className="rounded p-1 hover:bg-gray-100"
1968
+ onClick={() => {}}
1969
+ title="Predefined prompts"
1970
+ aria-label="Predefined prompts"
1971
+ >
1972
+ <Wand2 className="h-3 w-3" strokeWidth={1} />
1973
+ </button>
1974
+ </PopoverTrigger>
1975
+ <PopoverContent className="w-64 p-0" align="start">
1976
+ <div className="max-h-56 overflow-y-auto p-2">
1977
+ {activeProfile.prompts.map((p, index) => (
1978
+ <div
1979
+ key={index}
1980
+ className="cursor-pointer rounded p-1.5 text-xs text-gray-700 hover:bg-gray-100"
1981
+ onClick={() => {
1982
+ setPrompt(p.prompt);
1983
+ setShowPredefined(false);
1984
+ if (textareaRef.current) textareaRef.current.focus();
1985
+ }}
1986
+ >
1987
+ {p.title}
1988
+ </div>
1989
+ ))}
1990
+ </div>
1991
+ </PopoverContent>
1992
+ </Popover>
1993
+ ) : null}
1994
+ <AgentCostDisplay totalTokens={totalTokens} />
1995
+ </div>
1627
1996
  <Button
1628
- onClick={handleSubmit}
1629
- disabled={!prompt.trim() || isSubmitting}
1997
+ onClick={isExecuting ? handleStop : handleSubmit}
1998
+ disabled={!isExecuting && !prompt.trim()}
1630
1999
  size="sm"
1631
- className="self-end"
2000
+ className="h-5.5 w-5.5 cursor-pointer self-end rounded-full"
2001
+ title={isExecuting ? "Stop" : "Send"}
2002
+ aria-label={isExecuting ? "Stop" : "Send"}
1632
2003
  >
1633
- {isSubmitting ? (
1634
- <Loader2 className="h-4 w-4 animate-spin" strokeWidth={1} />
2004
+ {isExecuting ? (
2005
+ <Square className="size-3" strokeWidth={1} />
1635
2006
  ) : (
1636
- <Send className="h-4 w-4" strokeWidth={1} />
2007
+ <Send className="size-3" strokeWidth={1} />
1637
2008
  )}
1638
2009
  </Button>
1639
2010
  </div>