@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.
- package/dist/editor/ContextMenu.js +0 -2
- package/dist/editor/ContextMenu.js.map +1 -1
- package/dist/editor/ImageEditButton.js +10 -3
- package/dist/editor/ImageEditButton.js.map +1 -1
- package/dist/editor/ai/AgentTerminal.d.ts +3 -2
- package/dist/editor/ai/AgentTerminal.js +386 -94
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/Agents.js +67 -25
- package/dist/editor/ai/Agents.js.map +1 -1
- package/dist/editor/ai/AiResponseMessage.d.ts +6 -1
- package/dist/editor/ai/AiResponseMessage.js +63 -3
- package/dist/editor/ai/AiResponseMessage.js.map +1 -1
- package/dist/editor/ai/AiTerminal.js +27 -2
- package/dist/editor/ai/AiTerminal.js.map +1 -1
- package/dist/editor/client/EditorClient.js +32 -19
- package/dist/editor/client/EditorClient.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +4 -2
- package/dist/editor/client/editContext.js.map +1 -1
- package/dist/editor/client/operations.js +9 -6
- package/dist/editor/client/operations.js.map +1 -1
- package/dist/editor/commands/componentCommands.js +57 -7
- package/dist/editor/commands/componentCommands.js.map +1 -1
- package/dist/editor/field-types/richtext/contextMenuFactory.js +0 -3
- package/dist/editor/field-types/richtext/contextMenuFactory.js.map +1 -1
- package/dist/editor/menubar/ToolbarFactory.js +5 -2
- package/dist/editor/menubar/ToolbarFactory.js.map +1 -1
- package/dist/editor/menubar/toolbar-sections/UtilityControls.js +1 -1
- package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
- package/dist/editor/page-editor-chrome/CommentHighlighting.js +6 -4
- package/dist/editor/page-editor-chrome/CommentHighlighting.js.map +1 -1
- package/dist/editor/page-editor-chrome/CommentHighlightings.js +1 -1
- package/dist/editor/page-editor-chrome/CommentHighlightings.js.map +1 -1
- package/dist/editor/page-editor-chrome/FrameMenu.js +6 -8
- package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
- package/dist/editor/page-viewer/PageViewerFrame.js +70 -4
- package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
- package/dist/editor/reviews/Comment.js +3 -58
- package/dist/editor/reviews/Comment.js.map +1 -1
- package/dist/editor/reviews/CommentDisplayPopover.js +2 -3
- package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
- package/dist/editor/reviews/CommentEditor.js +2 -2
- package/dist/editor/reviews/CommentEditor.js.map +1 -1
- package/dist/editor/reviews/Comments.js +4 -0
- package/dist/editor/reviews/Comments.js.map +1 -1
- package/dist/editor/reviews/Reviews.js +2 -2
- package/dist/editor/reviews/Reviews.js.map +1 -1
- package/dist/editor/reviews/commentAi.d.ts +7 -0
- package/dist/editor/reviews/commentAi.js +86 -0
- package/dist/editor/reviews/commentAi.js.map +1 -0
- package/dist/editor/sidebar/ComponentTree.js +157 -49
- package/dist/editor/sidebar/ComponentTree.js.map +1 -1
- package/dist/editor/sidebar/Debug.js +1 -1
- package/dist/editor/sidebar/Debug.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +15 -4
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/editor/ContextMenu.tsx +0 -2
- package/src/editor/ImageEditButton.tsx +36 -8
- package/src/editor/ai/AgentTerminal.tsx +436 -65
- package/src/editor/ai/Agents.tsx +217 -117
- package/src/editor/ai/AiResponseMessage.tsx +106 -2
- package/src/editor/ai/AiTerminal.tsx +27 -0
- package/src/editor/client/EditorClient.tsx +41 -20
- package/src/editor/client/editContext.ts +4 -2
- package/src/editor/client/operations.ts +9 -8
- package/src/editor/commands/componentCommands.tsx +61 -13
- package/src/editor/field-types/richtext/components/EditorDropdown.css +1 -0
- package/src/editor/field-types/richtext/contextMenuFactory.tsx +0 -4
- package/src/editor/menubar/ToolbarFactory.tsx +6 -2
- package/src/editor/menubar/toolbar-sections/UtilityControls.tsx +23 -19
- package/src/editor/page-editor-chrome/CommentHighlighting.tsx +6 -4
- package/src/editor/page-editor-chrome/CommentHighlightings.tsx +3 -1
- package/src/editor/page-editor-chrome/FrameMenu.tsx +6 -8
- package/src/editor/page-viewer/PageViewerFrame.tsx +80 -4
- package/src/editor/reviews/Comment.tsx +4 -66
- package/src/editor/reviews/CommentDisplayPopover.tsx +2 -3
- package/src/editor/reviews/CommentEditor.tsx +2 -2
- package/src/editor/reviews/Comments.tsx +12 -0
- package/src/editor/reviews/Reviews.tsx +2 -0
- package/src/editor/reviews/commentAi.ts +106 -0
- package/src/editor/sidebar/ComponentTree.tsx +223 -69
- package/src/editor/sidebar/Debug.tsx +1 -1
- package/src/revision.ts +2 -2
- package/src/types.ts +1 -1
- package/styles.css +0 -5
- package/dist/editor/ai/AiPromptPopover.d.ts +0 -7
- package/dist/editor/ai/AiPromptPopover.js +0 -111
- package/dist/editor/ai/AiPromptPopover.js.map +0 -1
- 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({
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
793
|
-
|
|
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",
|
|
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()
|
|
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
|
-
{
|
|
1634
|
-
<
|
|
2004
|
+
{isExecuting ? (
|
|
2005
|
+
<Square className="size-3" strokeWidth={1} />
|
|
1635
2006
|
) : (
|
|
1636
|
-
<Send className="
|
|
2007
|
+
<Send className="size-3" strokeWidth={1} />
|
|
1637
2008
|
)}
|
|
1638
2009
|
</Button>
|
|
1639
2010
|
</div>
|