@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.
Files changed (97) hide show
  1. package/dist/components/ui/context-menu.d.ts +1 -1
  2. package/dist/components/ui/context-menu.js +8 -8
  3. package/dist/components/ui/context-menu.js.map +1 -1
  4. package/dist/config/config.js +8 -1
  5. package/dist/config/config.js.map +1 -1
  6. package/dist/config/types.d.ts +1 -0
  7. package/dist/editor/ContentTree.js +15 -16
  8. package/dist/editor/ContentTree.js.map +1 -1
  9. package/dist/editor/ContextMenu.js +37 -6
  10. package/dist/editor/ContextMenu.js.map +1 -1
  11. package/dist/editor/MainLayout.js +1 -1
  12. package/dist/editor/MainLayout.js.map +1 -1
  13. package/dist/editor/ai/AgentHistory.js +1 -1
  14. package/dist/editor/ai/AgentTerminal.js +187 -13
  15. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  16. package/dist/editor/ai/AiResponseMessage.d.ts +1 -1
  17. package/dist/editor/ai/types.d.ts +25 -0
  18. package/dist/editor/ai/types.js +2 -0
  19. package/dist/editor/ai/types.js.map +1 -0
  20. package/dist/editor/client/EditorShell.js +29 -0
  21. package/dist/editor/client/EditorShell.js.map +1 -1
  22. package/dist/editor/client/editContext.d.ts +3 -0
  23. package/dist/editor/client/editContext.js.map +1 -1
  24. package/dist/editor/commands/agentCommands.d.ts +9 -0
  25. package/dist/editor/commands/agentCommands.js +30 -0
  26. package/dist/editor/commands/agentCommands.js.map +1 -0
  27. package/dist/editor/commands/itemCommands.js +14 -14
  28. package/dist/editor/commands/itemCommands.js.map +1 -1
  29. package/dist/editor/component-designer/aiContext.d.ts +1 -1
  30. package/dist/editor/context-menu/InsertMenu.d.ts +2 -1
  31. package/dist/editor/context-menu/InsertMenu.js +20 -15
  32. package/dist/editor/context-menu/InsertMenu.js.map +1 -1
  33. package/dist/editor/field-types/NameValueListEditor.d.ts +7 -0
  34. package/dist/editor/field-types/NameValueListEditor.js +99 -0
  35. package/dist/editor/field-types/NameValueListEditor.js.map +1 -0
  36. package/dist/editor/fieldTypes.d.ts +1 -1
  37. package/dist/editor/page-editor-chrome/FrameMenu.js +47 -10
  38. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  39. package/dist/editor/page-editor-chrome/PlaceholderDropZone.d.ts +3 -1
  40. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +30 -4
  41. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
  42. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js +44 -5
  43. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js.map +1 -1
  44. package/dist/editor/services/agentService.d.ts +22 -2
  45. package/dist/editor/services/agentService.js +26 -1
  46. package/dist/editor/services/agentService.js.map +1 -1
  47. package/dist/editor/services/aiService.d.ts +2 -1
  48. package/dist/editor/services/aiService.js.map +1 -1
  49. package/dist/editor/sidebar/GraphQL.js +47 -90
  50. package/dist/editor/sidebar/GraphQL.js.map +1 -1
  51. package/dist/editor/ui/DragPreview.d.ts +13 -0
  52. package/dist/editor/ui/DragPreview.js +35 -0
  53. package/dist/editor/ui/DragPreview.js.map +1 -0
  54. package/dist/editor/ui/ItemNameDialogNew.js +2 -2
  55. package/dist/editor/ui/ItemNameDialogNew.js.map +1 -1
  56. package/dist/editor/ui/PerfectTree.js +3 -15
  57. package/dist/editor/ui/PerfectTree.js.map +1 -1
  58. package/dist/revision.d.ts +2 -2
  59. package/dist/revision.js +2 -2
  60. package/dist/styles.css +37 -9
  61. package/dist/tour/default-tour.js +34 -38
  62. package/dist/tour/default-tour.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/components/ui/context-menu.tsx +31 -19
  65. package/src/config/config.tsx +9 -1
  66. package/src/config/types.ts +1 -0
  67. package/src/editor/ContentTree.tsx +13 -18
  68. package/src/editor/ContextMenu.tsx +112 -19
  69. package/src/editor/MainLayout.tsx +1 -1
  70. package/src/editor/ai/AgentHistory.tsx +2 -2
  71. package/src/editor/ai/AgentTerminal.tsx +226 -15
  72. package/src/editor/ai/AiResponseMessage.tsx +1 -1
  73. package/src/editor/ai/types.ts +27 -0
  74. package/src/editor/client/EditorShell.tsx +31 -0
  75. package/src/editor/client/editContext.ts +12 -1
  76. package/src/editor/commands/agentCommands.tsx +49 -0
  77. package/src/editor/commands/itemCommands.tsx +26 -14
  78. package/src/editor/component-designer/aiContext.ts +1 -1
  79. package/src/editor/context-menu/InsertMenu.tsx +64 -39
  80. package/src/editor/field-types/NameValueListEditor.tsx +197 -0
  81. package/src/editor/fieldTypes.ts +1 -1
  82. package/src/editor/page-editor-chrome/FrameMenu.tsx +61 -13
  83. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +82 -20
  84. package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +77 -24
  85. package/src/editor/services/agentService.ts +53 -2
  86. package/src/editor/services/aiService.ts +3 -1
  87. package/src/editor/sidebar/GraphQL.tsx +50 -99
  88. package/src/editor/ui/DragPreview.tsx +44 -0
  89. package/src/editor/ui/ItemNameDialogNew.tsx +7 -3
  90. package/src/editor/ui/PerfectTree.tsx +2 -17
  91. package/src/revision.ts +2 -2
  92. package/src/tour/default-tour.tsx +34 -46
  93. package/dist/editor/ai/AiTerminal.d.ts +0 -47
  94. package/dist/editor/ai/AiTerminal.js +0 -300
  95. package/dist/editor/ai/AiTerminal.js.map +0 -1
  96. package/src/editor/ai/AiTerminal.tsx +0 -570
  97. package/src/editor/component-designer/ComponentDesignerAiTerminal.tsx_ +0 -11
@@ -111,6 +111,7 @@ export type MenuItem = {
111
111
  disabled?: boolean;
112
112
  separator?: boolean;
113
113
  className?: string;
114
+ template?: ReactNode;
114
115
  };
115
116
 
116
117
  export type MenuItemGroup = {
@@ -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<string, { node: CustomTreeNode; reloadChildren: boolean }>();
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 = existing.reloadChildren || reloadChildrenFlag;
366
+ existing.reloadChildren =
367
+ existing.reloadChildren || reloadChildrenFlag;
371
368
  } else {
372
- targetMap.set(targetKey, { node, reloadChildren: reloadChildrenFlag });
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 disabled={item.disabled} className={item.className}>
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
- <ContextMenuItem
255
- key={subIndex}
256
- onClick={(e) => {
257
- if (subItem.command) {
258
- subItem.command(e);
259
- }
260
- canvasRef.current?.dispatchEvent(
261
- new KeyboardEvent("keydown", { key: "Escape" }),
262
- );
263
- }}
264
- disabled={subItem.disabled}
265
- className={cn("cursor-pointer", subItem.className)}
266
- >
267
- {subItem.icon}
268
- {subItem.label}
269
- </ContextMenuItem>
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-medium text-gray-900">
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 "./AiTerminal";
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 agentModelId = agent?.model;
1195
- const availableModelIds = (activeProfile.models || []).map((m) => m.id);
1196
- const nextModelId =
1197
- agentModelId && availableModelIds.includes(agentModelId)
1198
- ? agentModelId
1199
- : activeProfile.defaultModelId || activeProfile.models?.[0]?.id;
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 when available
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
- }, [agentMetadata]);
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
- updateMode((e.target.value as "agent" | "ask") || "agent")
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) => setSelectedModelId(e.target.value)}
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 "./AiTerminal";
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
+ };