@alpaca-editor/core 1.0.4103 → 1.0.4104
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/components/ui/context-menu.d.ts +1 -1
- package/dist/components/ui/context-menu.js +8 -8
- package/dist/components/ui/context-menu.js.map +1 -1
- package/dist/config/config.js +8 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +1 -0
- package/dist/editor/ContentTree.js +15 -16
- package/dist/editor/ContentTree.js.map +1 -1
- package/dist/editor/ContextMenu.js +37 -6
- package/dist/editor/ContextMenu.js.map +1 -1
- package/dist/editor/MainLayout.js +1 -1
- package/dist/editor/MainLayout.js.map +1 -1
- package/dist/editor/ai/AgentHistory.js +1 -1
- package/dist/editor/ai/AgentTerminal.js +187 -13
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/AiResponseMessage.d.ts +1 -1
- package/dist/editor/ai/types.d.ts +25 -0
- package/dist/editor/ai/types.js +2 -0
- package/dist/editor/ai/types.js.map +1 -0
- package/dist/editor/client/EditorShell.js +29 -0
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +3 -0
- package/dist/editor/client/editContext.js.map +1 -1
- package/dist/editor/commands/agentCommands.d.ts +9 -0
- package/dist/editor/commands/agentCommands.js +30 -0
- package/dist/editor/commands/agentCommands.js.map +1 -0
- package/dist/editor/commands/itemCommands.js +14 -14
- package/dist/editor/commands/itemCommands.js.map +1 -1
- package/dist/editor/component-designer/aiContext.d.ts +1 -1
- package/dist/editor/context-menu/InsertMenu.d.ts +2 -1
- package/dist/editor/context-menu/InsertMenu.js +20 -15
- package/dist/editor/context-menu/InsertMenu.js.map +1 -1
- package/dist/editor/field-types/NameValueListEditor.d.ts +7 -0
- package/dist/editor/field-types/NameValueListEditor.js +99 -0
- package/dist/editor/field-types/NameValueListEditor.js.map +1 -0
- package/dist/editor/fieldTypes.d.ts +1 -1
- package/dist/editor/page-editor-chrome/FrameMenu.js +47 -10
- package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.d.ts +3 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +30 -4
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZones.js +44 -5
- package/dist/editor/page-editor-chrome/PlaceholderDropZones.js.map +1 -1
- package/dist/editor/services/agentService.d.ts +22 -2
- package/dist/editor/services/agentService.js +26 -1
- package/dist/editor/services/agentService.js.map +1 -1
- package/dist/editor/services/aiService.d.ts +2 -1
- package/dist/editor/services/aiService.js.map +1 -1
- package/dist/editor/sidebar/GraphQL.js +47 -90
- package/dist/editor/sidebar/GraphQL.js.map +1 -1
- package/dist/editor/ui/DragPreview.d.ts +13 -0
- package/dist/editor/ui/DragPreview.js +35 -0
- package/dist/editor/ui/DragPreview.js.map +1 -0
- package/dist/editor/ui/ItemNameDialogNew.js +2 -2
- package/dist/editor/ui/ItemNameDialogNew.js.map +1 -1
- package/dist/editor/ui/PerfectTree.js +3 -15
- package/dist/editor/ui/PerfectTree.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +37 -9
- package/dist/tour/default-tour.js +34 -38
- package/dist/tour/default-tour.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/context-menu.tsx +31 -19
- package/src/config/config.tsx +9 -1
- package/src/config/types.ts +1 -0
- package/src/editor/ContentTree.tsx +13 -18
- package/src/editor/ContextMenu.tsx +112 -19
- package/src/editor/MainLayout.tsx +1 -1
- package/src/editor/ai/AgentHistory.tsx +2 -2
- package/src/editor/ai/AgentTerminal.tsx +226 -15
- package/src/editor/ai/AiResponseMessage.tsx +1 -1
- package/src/editor/ai/types.ts +27 -0
- package/src/editor/client/EditorShell.tsx +31 -0
- package/src/editor/client/editContext.ts +12 -1
- package/src/editor/commands/agentCommands.tsx +49 -0
- package/src/editor/commands/itemCommands.tsx +26 -14
- package/src/editor/component-designer/aiContext.ts +1 -1
- package/src/editor/context-menu/InsertMenu.tsx +64 -39
- package/src/editor/field-types/NameValueListEditor.tsx +197 -0
- package/src/editor/fieldTypes.ts +1 -1
- package/src/editor/page-editor-chrome/FrameMenu.tsx +61 -13
- package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +82 -20
- package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +77 -24
- package/src/editor/services/agentService.ts +53 -2
- package/src/editor/services/aiService.ts +3 -1
- package/src/editor/sidebar/GraphQL.tsx +50 -99
- package/src/editor/ui/DragPreview.tsx +44 -0
- package/src/editor/ui/ItemNameDialogNew.tsx +7 -3
- package/src/editor/ui/PerfectTree.tsx +2 -17
- package/src/revision.ts +2 -2
- package/src/tour/default-tour.tsx +34 -46
- package/dist/editor/ai/AiTerminal.d.ts +0 -47
- package/dist/editor/ai/AiTerminal.js +0 -300
- package/dist/editor/ai/AiTerminal.js.map +0 -1
- package/src/editor/ai/AiTerminal.tsx +0 -570
- package/src/editor/component-designer/ComponentDesignerAiTerminal.tsx_ +0 -11
package/src/config/types.ts
CHANGED
|
@@ -5,8 +5,6 @@ import { useEditContext, useEditContextRef } from "./client/editContext";
|
|
|
5
5
|
import { useCallback, useEffect, useRef, useState, useMemo, memo } from "react";
|
|
6
6
|
|
|
7
7
|
import { ItemTreeNodeData, getChildren } from "./services/contentService";
|
|
8
|
-
import { ContextMenu } from "primereact/contextmenu";
|
|
9
|
-
import { MenuItem } from "primereact/menuitem";
|
|
10
8
|
|
|
11
9
|
import { getAbsoluteIconUrl } from "./utils";
|
|
12
10
|
import { FullItem, ItemDescriptor } from "./pageModel";
|
|
@@ -94,8 +92,6 @@ export default function ContentTree({
|
|
|
94
92
|
const editContext = useEditContext();
|
|
95
93
|
|
|
96
94
|
const nodeDictionary = useRef<{ [key: string]: CustomTreeNode }>({});
|
|
97
|
-
const cm = useRef<ContextMenu>(null);
|
|
98
|
-
const [menu, setMenu] = useState<MenuItem[]>([]);
|
|
99
95
|
|
|
100
96
|
const editContextRef = useEditContextRef();
|
|
101
97
|
const lastSelectedItemId = useRef<string | undefined>(undefined);
|
|
@@ -275,10 +271,7 @@ export default function ContentTree({
|
|
|
275
271
|
);
|
|
276
272
|
|
|
277
273
|
const refreshNode = useCallback(
|
|
278
|
-
async (
|
|
279
|
-
node: TreeNode,
|
|
280
|
-
options?: { reloadChildren?: boolean },
|
|
281
|
-
) => {
|
|
274
|
+
async (node: TreeNode, options?: { reloadChildren?: boolean }) => {
|
|
282
275
|
const item = await editContext?.itemsRepository.getItem(
|
|
283
276
|
node.data as ItemDescriptor,
|
|
284
277
|
);
|
|
@@ -348,7 +341,10 @@ export default function ContentTree({
|
|
|
348
341
|
const unsubscribe = editContext.itemsRepository.subscribeItemsChanged(
|
|
349
342
|
async (changes: any[]) => {
|
|
350
343
|
// Aggregate target nodes and whether their children should be reloaded
|
|
351
|
-
const targetMap = new Map<
|
|
344
|
+
const targetMap = new Map<
|
|
345
|
+
string,
|
|
346
|
+
{ node: CustomTreeNode; reloadChildren: boolean }
|
|
347
|
+
>();
|
|
352
348
|
|
|
353
349
|
for (const change of changes) {
|
|
354
350
|
const isDelete = change?.action === "delete";
|
|
@@ -363,13 +359,17 @@ export default function ContentTree({
|
|
|
363
359
|
// Default behavior: if flags are absent, keep previous behavior (reload children)
|
|
364
360
|
const reloadChildrenFlag = isDelete
|
|
365
361
|
? true
|
|
366
|
-
: change?.changed?.children ?? true;
|
|
362
|
+
: (change?.changed?.children ?? true);
|
|
367
363
|
|
|
368
364
|
const existing = targetMap.get(targetKey);
|
|
369
365
|
if (existing) {
|
|
370
|
-
existing.reloadChildren =
|
|
366
|
+
existing.reloadChildren =
|
|
367
|
+
existing.reloadChildren || reloadChildrenFlag;
|
|
371
368
|
} else {
|
|
372
|
-
targetMap.set(targetKey, {
|
|
369
|
+
targetMap.set(targetKey, {
|
|
370
|
+
node,
|
|
371
|
+
reloadChildren: reloadChildrenFlag,
|
|
372
|
+
});
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
@@ -616,7 +616,6 @@ export default function ContentTree({
|
|
|
616
616
|
items: items,
|
|
617
617
|
editContext: editContext,
|
|
618
618
|
commandCallback: (command: ItemCommand, result: any) => {
|
|
619
|
-
cm.current?.hide(originalEvent);
|
|
620
619
|
if (command.id === "insertItem") {
|
|
621
620
|
const item = result as ItemDescriptor;
|
|
622
621
|
if (item) {
|
|
@@ -627,10 +626,7 @@ export default function ContentTree({
|
|
|
627
626
|
},
|
|
628
627
|
},
|
|
629
628
|
);
|
|
630
|
-
|
|
631
|
-
if (menuItems) setMenu(menuItems);
|
|
632
|
-
|
|
633
|
-
cm.current?.show(originalEvent);
|
|
629
|
+
if (menuItems) editContext?.showContextMenu(originalEvent, menuItems);
|
|
634
630
|
},
|
|
635
631
|
[
|
|
636
632
|
itemCommands,
|
|
@@ -759,7 +755,6 @@ export default function ContentTree({
|
|
|
759
755
|
|
|
760
756
|
return (
|
|
761
757
|
<>
|
|
762
|
-
<ContextMenu model={menu} ref={cm} className="text-sm" />
|
|
763
758
|
<div
|
|
764
759
|
className={cn(className, "text-dark font-light")}
|
|
765
760
|
ref={treeContainerRef}
|
|
@@ -228,6 +228,18 @@ export const EditContextMenu = forwardRef<
|
|
|
228
228
|
return <ContextMenuSeparator key={index} />;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// Render custom template item as-is (used for Insert overlay template)
|
|
232
|
+
if (item.template) {
|
|
233
|
+
return (
|
|
234
|
+
<div
|
|
235
|
+
key={index}
|
|
236
|
+
className={cn("px-2 py-1.5 hover:bg-gray-100", item.className)}
|
|
237
|
+
>
|
|
238
|
+
{item.template}
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
231
243
|
// Render disabled items without command as a label (useful for section headers)
|
|
232
244
|
if (item.disabled && !item.command && !item.items?.length) {
|
|
233
245
|
return (
|
|
@@ -245,29 +257,110 @@ export const EditContextMenu = forwardRef<
|
|
|
245
257
|
if (item.items && item.items.length > 0) {
|
|
246
258
|
return (
|
|
247
259
|
<ContextMenuSub key={index}>
|
|
248
|
-
<ContextMenuSubTrigger
|
|
260
|
+
<ContextMenuSubTrigger
|
|
261
|
+
disabled={item.disabled}
|
|
262
|
+
className={item.className}
|
|
263
|
+
>
|
|
249
264
|
{item.icon}
|
|
250
265
|
{item.label}
|
|
251
266
|
</ContextMenuSubTrigger>
|
|
252
267
|
<ContextMenuSubContent>
|
|
253
|
-
{item.items.map((subItem, subIndex) =>
|
|
254
|
-
|
|
255
|
-
key={subIndex}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
268
|
+
{item.items.map((subItem, subIndex) => {
|
|
269
|
+
if (subItem.separator) {
|
|
270
|
+
return <ContextMenuSeparator key={subIndex} />;
|
|
271
|
+
}
|
|
272
|
+
if (subItem.template) {
|
|
273
|
+
return (
|
|
274
|
+
<div
|
|
275
|
+
key={subIndex}
|
|
276
|
+
className={cn("px-2 py-1.5", subItem.className)}
|
|
277
|
+
onClick={(e) => {
|
|
278
|
+
// prevent closing
|
|
279
|
+
e.stopPropagation();
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
{subItem.template}
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (
|
|
288
|
+
subItem.disabled &&
|
|
289
|
+
!subItem.command &&
|
|
290
|
+
!subItem.items?.length
|
|
291
|
+
) {
|
|
292
|
+
return (
|
|
293
|
+
<ContextMenuLabel
|
|
294
|
+
key={subIndex}
|
|
295
|
+
className={cn(
|
|
296
|
+
"flex items-center gap-2",
|
|
297
|
+
subItem.className,
|
|
298
|
+
)}
|
|
299
|
+
inset={false}
|
|
300
|
+
>
|
|
301
|
+
{subItem.icon}
|
|
302
|
+
{subItem.label}
|
|
303
|
+
</ContextMenuLabel>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
if (subItem.items && subItem.items.length > 0) {
|
|
307
|
+
// Nested submenu support if needed
|
|
308
|
+
return (
|
|
309
|
+
<ContextMenuSub key={subIndex}>
|
|
310
|
+
<ContextMenuSubTrigger
|
|
311
|
+
disabled={subItem.disabled}
|
|
312
|
+
className={subItem.className}
|
|
313
|
+
>
|
|
314
|
+
{subItem.icon}
|
|
315
|
+
{subItem.label}
|
|
316
|
+
</ContextMenuSubTrigger>
|
|
317
|
+
<ContextMenuSubContent>
|
|
318
|
+
{(subItem.items || []).map(
|
|
319
|
+
(nested, nestedIndex) => (
|
|
320
|
+
<ContextMenuItem
|
|
321
|
+
key={nestedIndex}
|
|
322
|
+
onClick={(e) => {
|
|
323
|
+
if (nested.command) nested.command(e);
|
|
324
|
+
canvasRef.current?.dispatchEvent(
|
|
325
|
+
new KeyboardEvent("keydown", {
|
|
326
|
+
key: "Escape",
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
}}
|
|
330
|
+
disabled={nested.disabled}
|
|
331
|
+
className={cn(
|
|
332
|
+
"cursor-pointer",
|
|
333
|
+
nested.className,
|
|
334
|
+
)}
|
|
335
|
+
>
|
|
336
|
+
{nested.icon}
|
|
337
|
+
{nested.label}
|
|
338
|
+
</ContextMenuItem>
|
|
339
|
+
),
|
|
340
|
+
)}
|
|
341
|
+
</ContextMenuSubContent>
|
|
342
|
+
</ContextMenuSub>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return (
|
|
346
|
+
<ContextMenuItem
|
|
347
|
+
key={subIndex}
|
|
348
|
+
onClick={(e) => {
|
|
349
|
+
if (subItem.command) {
|
|
350
|
+
subItem.command(e);
|
|
351
|
+
}
|
|
352
|
+
canvasRef.current?.dispatchEvent(
|
|
353
|
+
new KeyboardEvent("keydown", { key: "Escape" }),
|
|
354
|
+
);
|
|
355
|
+
}}
|
|
356
|
+
disabled={subItem.disabled}
|
|
357
|
+
className={cn("cursor-pointer", subItem.className)}
|
|
358
|
+
>
|
|
359
|
+
{subItem.icon}
|
|
360
|
+
{subItem.label}
|
|
361
|
+
</ContextMenuItem>
|
|
362
|
+
);
|
|
363
|
+
})}
|
|
271
364
|
</ContextMenuSubContent>
|
|
272
365
|
</ContextMenuSub>
|
|
273
366
|
);
|
|
@@ -84,7 +84,7 @@ export default function MainLayout(props: MainLayoutProps) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
return (
|
|
87
|
-
<div className={classNames("flex select-none", className)}>
|
|
87
|
+
<div className={classNames("flex font-light select-none", className)}>
|
|
88
88
|
<div className="flex flex-1">
|
|
89
89
|
{!props.view.hideViewSelector && <LeftToolbar />}
|
|
90
90
|
<div className="flex flex-1 flex-col">
|
|
@@ -58,9 +58,9 @@ export function AgentHistory({
|
|
|
58
58
|
onClick={() => onOpenAgent(agent)}
|
|
59
59
|
>
|
|
60
60
|
<div className="min-w-0 flex-1">
|
|
61
|
-
<div className="truncate font-
|
|
61
|
+
<div className="truncate font-normal text-gray-900">
|
|
62
62
|
{agent.name}
|
|
63
|
-
</div>
|
|
63
|
+
</div>
|
|
64
64
|
<div className="text-xs text-gray-400">
|
|
65
65
|
{formatDateToLocalTime(agent.updatedDate)}
|
|
66
66
|
</div>
|
|
@@ -33,13 +33,15 @@ import {
|
|
|
33
33
|
AgentDetails,
|
|
34
34
|
updateAgentMetadata,
|
|
35
35
|
AgentMetadata,
|
|
36
|
+
updateAgentSettings,
|
|
37
|
+
updateAgentCostLimit,
|
|
36
38
|
} from "../services/agentService";
|
|
37
39
|
import { useEditContext, useFieldsEditContext } from "../client/editContext";
|
|
38
40
|
import { Textarea } from "../../components/ui/textarea";
|
|
39
41
|
import { Button } from "../../components/ui/button";
|
|
40
42
|
import { AiResponseMessage } from "./AiResponseMessage";
|
|
41
43
|
import { AgentCostDisplay } from "./AgentCostDisplay";
|
|
42
|
-
import { Message } from "./
|
|
44
|
+
import { Message } from "./types";
|
|
43
45
|
import { ContextInfoBar } from "./ContextInfoBar";
|
|
44
46
|
import { getComponentById } from "../componentTreeHelper";
|
|
45
47
|
import { Comment } from "../../types";
|
|
@@ -316,6 +318,11 @@ export function AgentTerminal({
|
|
|
316
318
|
}, [messages]);
|
|
317
319
|
|
|
318
320
|
const [error, setError] = useState<string | null>(null);
|
|
321
|
+
const [costLimitExceeded, setCostLimitExceeded] = useState<{
|
|
322
|
+
totalCost: number;
|
|
323
|
+
costLimit: number;
|
|
324
|
+
initialCostLimit: number;
|
|
325
|
+
} | null>(null);
|
|
319
326
|
|
|
320
327
|
// Flag to track when we should create a new message
|
|
321
328
|
const shouldCreateNewMessage = useRef(false);
|
|
@@ -329,6 +336,12 @@ export function AgentTerminal({
|
|
|
329
336
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
|
330
337
|
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
|
|
331
338
|
|
|
339
|
+
// Cache mode/model changes made while the agent is still "new" (not yet persisted)
|
|
340
|
+
const pendingSettingsRef = useRef<{
|
|
341
|
+
modelName?: string | null;
|
|
342
|
+
mode?: "agent" | "ask";
|
|
343
|
+
} | null>(null);
|
|
344
|
+
|
|
332
345
|
// Auto-scroll to bottom when new messages arrive
|
|
333
346
|
const scrollToBottom = useCallback(() => {
|
|
334
347
|
const container = messagesContainerRef.current;
|
|
@@ -850,6 +863,24 @@ export function AgentTerminal({
|
|
|
850
863
|
break;
|
|
851
864
|
|
|
852
865
|
case "error":
|
|
866
|
+
// Detect cost limit exceeded
|
|
867
|
+
try {
|
|
868
|
+
const data: any = (message as any).data;
|
|
869
|
+
if (
|
|
870
|
+
message.error === "COST_LIMIT_EXCEEDED" ||
|
|
871
|
+
(data && data.kind === "costLimitExceeded")
|
|
872
|
+
) {
|
|
873
|
+
setCostLimitExceeded({
|
|
874
|
+
totalCost: Number(data?.totalCost) || 0,
|
|
875
|
+
costLimit: Number(data?.costLimit) || 0,
|
|
876
|
+
initialCostLimit: Number(data?.initialCostLimit) || 0,
|
|
877
|
+
});
|
|
878
|
+
setIsWaitingForResponse(false);
|
|
879
|
+
shouldCreateNewMessage.current = false;
|
|
880
|
+
resetDotsTimer();
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
} catch {}
|
|
853
884
|
console.error("❌ Stream error:", message.error);
|
|
854
885
|
setError(message.error || "Stream error occurred");
|
|
855
886
|
setIsWaitingForResponse(false);
|
|
@@ -1188,17 +1219,23 @@ export function AgentTerminal({
|
|
|
1188
1219
|
}
|
|
1189
1220
|
}, [profiles, agent?.profileId]);
|
|
1190
1221
|
|
|
1191
|
-
// Update selected model when the active profile changes
|
|
1222
|
+
// Update selected model when the active profile or agent model changes
|
|
1192
1223
|
useEffect(() => {
|
|
1193
1224
|
if (!activeProfile) return;
|
|
1194
|
-
const
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1225
|
+
const agentModelName = agent?.model; // persisted as model NAME on server
|
|
1226
|
+
const models = activeProfile.models || [];
|
|
1227
|
+
let nextModelId: string | undefined = undefined;
|
|
1228
|
+
if (agentModelName) {
|
|
1229
|
+
const match = models.find(
|
|
1230
|
+
(m) => (m.name || "").toLowerCase() === agentModelName.toLowerCase(),
|
|
1231
|
+
);
|
|
1232
|
+
if (match) nextModelId = match.id;
|
|
1233
|
+
}
|
|
1234
|
+
if (!nextModelId) {
|
|
1235
|
+
nextModelId = activeProfile.defaultModelId || models[0]?.id;
|
|
1236
|
+
}
|
|
1200
1237
|
setSelectedModelId(nextModelId || undefined);
|
|
1201
|
-
}, [activeProfile?.id]);
|
|
1238
|
+
}, [activeProfile?.id, agent?.model]);
|
|
1202
1239
|
|
|
1203
1240
|
// Cleanup stream connection when component unmounts or agent changes
|
|
1204
1241
|
useEffect(() => {
|
|
@@ -1210,7 +1247,7 @@ export function AgentTerminal({
|
|
|
1210
1247
|
};
|
|
1211
1248
|
}, [agent?.id]);
|
|
1212
1249
|
|
|
1213
|
-
// Initialize mode from metadata
|
|
1250
|
+
// Initialize mode from metadata; fall back to agent.Mode from server
|
|
1214
1251
|
useEffect(() => {
|
|
1215
1252
|
try {
|
|
1216
1253
|
const metaMode = (agentMetadata as any)?.mode as
|
|
@@ -1219,9 +1256,16 @@ export function AgentTerminal({
|
|
|
1219
1256
|
| undefined;
|
|
1220
1257
|
if (metaMode === "agent" || metaMode === "ask") {
|
|
1221
1258
|
setMode(metaMode);
|
|
1259
|
+
return;
|
|
1222
1260
|
}
|
|
1223
1261
|
} catch {}
|
|
1224
|
-
|
|
1262
|
+
try {
|
|
1263
|
+
const serverMode = (agent as any)?.mode as string | undefined;
|
|
1264
|
+
if (serverMode === "agent" || serverMode === "ask") {
|
|
1265
|
+
setMode(serverMode);
|
|
1266
|
+
}
|
|
1267
|
+
} catch {}
|
|
1268
|
+
}, [agentMetadata, (agent as any)?.mode]);
|
|
1225
1269
|
|
|
1226
1270
|
const updateMode = useCallback(
|
|
1227
1271
|
async (nextMode: "agent" | "ask") => {
|
|
@@ -1266,6 +1310,26 @@ export function AgentTerminal({
|
|
|
1266
1310
|
}
|
|
1267
1311
|
}, [showDots, shouldAutoScroll, scrollToBottom]);
|
|
1268
1312
|
|
|
1313
|
+
// Persist any pending settings (mode/model) once an agent exists server-side
|
|
1314
|
+
const persistPendingSettingsIfNeeded = useCallback(async () => {
|
|
1315
|
+
try {
|
|
1316
|
+
if (!agent?.id) return;
|
|
1317
|
+
const pending = pendingSettingsRef.current;
|
|
1318
|
+
if (!pending) return;
|
|
1319
|
+
const payload: {
|
|
1320
|
+
model?: string | null;
|
|
1321
|
+
mode?: "agent" | "ask" | string | null;
|
|
1322
|
+
} = {};
|
|
1323
|
+
if (pending.modelName) payload.model = pending.modelName;
|
|
1324
|
+
if (pending.mode) payload.mode = pending.mode;
|
|
1325
|
+
if (Object.keys(payload).length === 0) return;
|
|
1326
|
+
await updateAgentSettings(agent.id, payload);
|
|
1327
|
+
pendingSettingsRef.current = null;
|
|
1328
|
+
} catch (e) {
|
|
1329
|
+
console.error("Failed to persist pending settings", e);
|
|
1330
|
+
}
|
|
1331
|
+
}, [agent?.id]);
|
|
1332
|
+
|
|
1269
1333
|
const handleSubmit = async () => {
|
|
1270
1334
|
if (!prompt.trim() || isSubmitting || !editContext) return;
|
|
1271
1335
|
|
|
@@ -1276,6 +1340,26 @@ export function AgentTerminal({
|
|
|
1276
1340
|
|
|
1277
1341
|
if (!agentId) return;
|
|
1278
1342
|
|
|
1343
|
+
// Optional context factory: invoke if configured and available, otherwise continue
|
|
1344
|
+
const factoryName = (agentMetadata as any)?.additionalData
|
|
1345
|
+
?.contextFactory as string | undefined;
|
|
1346
|
+
if (factoryName) {
|
|
1347
|
+
const factory = editContext.getContextFactory?.(factoryName);
|
|
1348
|
+
if (factory) {
|
|
1349
|
+
try {
|
|
1350
|
+
await Promise.resolve(factory());
|
|
1351
|
+
} catch (e: any) {
|
|
1352
|
+
console.warn(
|
|
1353
|
+
`Context factory '${factoryName}' failed: ${e?.message || String(e)}`,
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
} else {
|
|
1357
|
+
console.warn(
|
|
1358
|
+
`Context factory not found: ${factoryName}. Proceeding without it.`,
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1279
1363
|
// Add user message to local state immediately for better UX
|
|
1280
1364
|
const userMessage: AgentChatMessage = {
|
|
1281
1365
|
id: `user-${Date.now()}`,
|
|
@@ -1351,6 +1435,9 @@ export function AgentTerminal({
|
|
|
1351
1435
|
|
|
1352
1436
|
await startAgent(request);
|
|
1353
1437
|
|
|
1438
|
+
// If user changed mode/model while the agent was new, persist them now
|
|
1439
|
+
await persistPendingSettingsIfNeeded();
|
|
1440
|
+
|
|
1354
1441
|
// Save prompt to history
|
|
1355
1442
|
if (prompt.trim()) {
|
|
1356
1443
|
setPromptHistory((prev) => [
|
|
@@ -1433,6 +1520,26 @@ export function AgentTerminal({
|
|
|
1433
1520
|
const agentId = agent?.id;
|
|
1434
1521
|
if (!agentId) return;
|
|
1435
1522
|
|
|
1523
|
+
// Optional context factory: invoke if configured and available, otherwise continue
|
|
1524
|
+
const factoryName = (agentMetadata as any)?.additionalData
|
|
1525
|
+
?.contextFactory as string | undefined;
|
|
1526
|
+
if (factoryName) {
|
|
1527
|
+
const factory = editContext.getContextFactory?.(factoryName);
|
|
1528
|
+
if (factory) {
|
|
1529
|
+
try {
|
|
1530
|
+
await Promise.resolve(factory());
|
|
1531
|
+
} catch (e: any) {
|
|
1532
|
+
console.warn(
|
|
1533
|
+
`Context factory '${factoryName}' failed: ${e?.message || String(e)}`,
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
} else {
|
|
1537
|
+
console.warn(
|
|
1538
|
+
`Context factory not found: ${factoryName}. Proceeding without it.`,
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1436
1543
|
const userMessage: AgentChatMessage = {
|
|
1437
1544
|
id: `user-${Date.now()}`,
|
|
1438
1545
|
agentId,
|
|
@@ -1499,6 +1606,9 @@ export function AgentTerminal({
|
|
|
1499
1606
|
|
|
1500
1607
|
await startAgent(request);
|
|
1501
1608
|
|
|
1609
|
+
// If user changed mode/model while the agent was new, persist them now
|
|
1610
|
+
await persistPendingSettingsIfNeeded();
|
|
1611
|
+
|
|
1502
1612
|
await connectToStream();
|
|
1503
1613
|
} catch (err) {
|
|
1504
1614
|
console.error("Failed to submit quick message:", err);
|
|
@@ -2046,6 +2156,56 @@ export function AgentTerminal({
|
|
|
2046
2156
|
/>
|
|
2047
2157
|
);
|
|
2048
2158
|
|
|
2159
|
+
const renderCostLimitBanner = () => {
|
|
2160
|
+
if (!costLimitExceeded) return null;
|
|
2161
|
+
const { totalCost, costLimit, initialCostLimit } = costLimitExceeded;
|
|
2162
|
+
return (
|
|
2163
|
+
<div className="m-3 rounded border border-amber-300 bg-amber-50 p-3 text-xs text-amber-900">
|
|
2164
|
+
<div className="mb-2 flex items-center gap-2">
|
|
2165
|
+
<AlertCircle className="h-4 w-4 text-amber-500" strokeWidth={1} />
|
|
2166
|
+
<span>
|
|
2167
|
+
Cost limit exceeded. Spent ${totalCost.toFixed(4)} / $
|
|
2168
|
+
{costLimit.toFixed(4)}.
|
|
2169
|
+
</span>
|
|
2170
|
+
</div>
|
|
2171
|
+
<div className="flex gap-2">
|
|
2172
|
+
<button
|
|
2173
|
+
className="rounded border border-amber-300 bg-white px-2 py-1 hover:bg-amber-100"
|
|
2174
|
+
onClick={async () => {
|
|
2175
|
+
if (!agent?.id) return;
|
|
2176
|
+
try {
|
|
2177
|
+
await updateAgentCostLimit(agent.id, "remove");
|
|
2178
|
+
setCostLimitExceeded(null);
|
|
2179
|
+
// Reconnect to stream for next actions
|
|
2180
|
+
await connectToStream(agent);
|
|
2181
|
+
} catch (e) {
|
|
2182
|
+
console.error("Failed to remove cost limit", e);
|
|
2183
|
+
}
|
|
2184
|
+
}}
|
|
2185
|
+
>
|
|
2186
|
+
Continue and remove limit
|
|
2187
|
+
</button>
|
|
2188
|
+
<button
|
|
2189
|
+
className="rounded border border-amber-300 bg-white px-2 py-1 hover:bg-amber-100"
|
|
2190
|
+
onClick={async () => {
|
|
2191
|
+
if (!agent?.id) return;
|
|
2192
|
+
try {
|
|
2193
|
+
// Extend by initial cost limit amount
|
|
2194
|
+
await updateAgentCostLimit(agent.id, "extend");
|
|
2195
|
+
setCostLimitExceeded(null);
|
|
2196
|
+
await connectToStream(agent);
|
|
2197
|
+
} catch (e) {
|
|
2198
|
+
console.error("Failed to extend cost limit", e);
|
|
2199
|
+
}
|
|
2200
|
+
}}
|
|
2201
|
+
>
|
|
2202
|
+
Continue and extend limit
|
|
2203
|
+
</button>
|
|
2204
|
+
</div>
|
|
2205
|
+
</div>
|
|
2206
|
+
);
|
|
2207
|
+
};
|
|
2208
|
+
|
|
2049
2209
|
return (
|
|
2050
2210
|
<div className="flex h-full flex-col">
|
|
2051
2211
|
{/* Messages */}
|
|
@@ -2054,6 +2214,7 @@ export function AgentTerminal({
|
|
|
2054
2214
|
className="flex-1 overflow-y-auto"
|
|
2055
2215
|
onScroll={handleScroll}
|
|
2056
2216
|
>
|
|
2217
|
+
{renderCostLimitBanner()}
|
|
2057
2218
|
{error && (
|
|
2058
2219
|
<div className="m-4 rounded-lg border-l-4 border-red-500 bg-red-50 p-3">
|
|
2059
2220
|
<div className="flex items-start">
|
|
@@ -2186,9 +2347,36 @@ export function AgentTerminal({
|
|
|
2186
2347
|
<select
|
|
2187
2348
|
className="h-5 rounded border px-1.5 text-[10px] text-gray-500"
|
|
2188
2349
|
value={mode}
|
|
2189
|
-
onChange={(e) =>
|
|
2190
|
-
|
|
2191
|
-
|
|
2350
|
+
onChange={async (e) => {
|
|
2351
|
+
const nextMode = (e.target.value as "agent" | "ask") || "agent";
|
|
2352
|
+
// Optimistic UI update
|
|
2353
|
+
setMode(nextMode);
|
|
2354
|
+
const current = agentMetadata || ({} as AgentMetadata);
|
|
2355
|
+
const nextMeta: AgentMetadata = {
|
|
2356
|
+
...current,
|
|
2357
|
+
mode: nextMode,
|
|
2358
|
+
} as AgentMetadata;
|
|
2359
|
+
try {
|
|
2360
|
+
if (!agent?.id || agent.status === "new") {
|
|
2361
|
+
setAgentMetadata(nextMeta);
|
|
2362
|
+
// Cache until first start when agent is persisted
|
|
2363
|
+
pendingSettingsRef.current = {
|
|
2364
|
+
...(pendingSettingsRef.current || {}),
|
|
2365
|
+
mode: nextMode,
|
|
2366
|
+
};
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
await updateAgentSettings(agent.id, { mode: nextMode });
|
|
2370
|
+
setAgentMetadata(nextMeta);
|
|
2371
|
+
setAgent((prev) =>
|
|
2372
|
+
prev
|
|
2373
|
+
? { ...prev, metadata: JSON.stringify(nextMeta) }
|
|
2374
|
+
: prev,
|
|
2375
|
+
);
|
|
2376
|
+
} catch (e2) {
|
|
2377
|
+
console.error("Failed to persist mode change", e2);
|
|
2378
|
+
}
|
|
2379
|
+
}}
|
|
2192
2380
|
title="Mode"
|
|
2193
2381
|
aria-label="Mode"
|
|
2194
2382
|
data-testid="agent-mode-select"
|
|
@@ -2219,7 +2407,30 @@ export function AgentTerminal({
|
|
|
2219
2407
|
<select
|
|
2220
2408
|
className="h-5 rounded border px-1.5 text-[10px] text-gray-500"
|
|
2221
2409
|
value={selectedModelId || ""}
|
|
2222
|
-
onChange={(e) =>
|
|
2410
|
+
onChange={async (e) => {
|
|
2411
|
+
const nextId = e.target.value;
|
|
2412
|
+
setSelectedModelId(nextId);
|
|
2413
|
+
const modelName =
|
|
2414
|
+
activeProfile?.models?.find((m) => m.id === nextId)?.name ||
|
|
2415
|
+
"";
|
|
2416
|
+
// Update local agent state immediately for UX and to reflect in streaming stub
|
|
2417
|
+
setAgent((prev) =>
|
|
2418
|
+
prev ? { ...prev, model: modelName } : prev,
|
|
2419
|
+
);
|
|
2420
|
+
// Persist only for existing agents; otherwise cache until first start
|
|
2421
|
+
try {
|
|
2422
|
+
if (agent?.id && agent.status !== "new") {
|
|
2423
|
+
await updateAgentSettings(agent.id, { model: modelName });
|
|
2424
|
+
} else {
|
|
2425
|
+
pendingSettingsRef.current = {
|
|
2426
|
+
...(pendingSettingsRef.current || {}),
|
|
2427
|
+
modelName,
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
} catch (err) {
|
|
2431
|
+
console.error("Failed to persist agent model", err);
|
|
2432
|
+
}
|
|
2433
|
+
}}
|
|
2223
2434
|
title="Model"
|
|
2224
2435
|
aria-label="Model"
|
|
2225
2436
|
data-testid="agent-model-select"
|
|
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo } from "react";
|
|
|
2
2
|
|
|
3
3
|
import { useEditContext } from "../client/editContext";
|
|
4
4
|
import { EditOperation } from "../../types";
|
|
5
|
-
import { Message, ToolCall } from "./
|
|
5
|
+
import { Message, ToolCall } from "./types";
|
|
6
6
|
import { ToolCallDisplay } from "./ToolCallDisplay";
|
|
7
7
|
|
|
8
8
|
import { X, Bot, Loader2 } from "lucide-react";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type ToolCall = {
|
|
2
|
+
id: string;
|
|
3
|
+
displayName?: string;
|
|
4
|
+
function: {
|
|
5
|
+
name: string;
|
|
6
|
+
arguments: string;
|
|
7
|
+
result?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type Message = {
|
|
13
|
+
id: string;
|
|
14
|
+
content: string;
|
|
15
|
+
formattedContent?: string;
|
|
16
|
+
name: string;
|
|
17
|
+
role: string;
|
|
18
|
+
tool_calls?: ToolCall[];
|
|
19
|
+
tool_call_id?: string;
|
|
20
|
+
createdDate?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type AiContext = {
|
|
24
|
+
promptData: any;
|
|
25
|
+
endpoint?: string;
|
|
26
|
+
callback?: (response: any) => void;
|
|
27
|
+
};
|