@manta-eu/react 0.5.0 → 0.6.0

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 (105) hide show
  1. package/dist/components/chat-view/Chat.d.ts +8 -2
  2. package/dist/components/chat-view/Chat.d.ts.map +1 -1
  3. package/dist/components/chat-view/Chat.js +25 -3
  4. package/dist/components/chat-view/Chat.js.map +1 -1
  5. package/dist/components/chat-view/ToolApprovalPart.d.ts +12 -0
  6. package/dist/components/chat-view/ToolApprovalPart.d.ts.map +1 -0
  7. package/dist/components/chat-view/ToolApprovalPart.js +12 -0
  8. package/dist/components/chat-view/ToolApprovalPart.js.map +1 -0
  9. package/dist/components/chat-view/ToolMessagePart.d.ts.map +1 -1
  10. package/dist/components/chat-view/ToolMessagePart.js +7 -15
  11. package/dist/components/chat-view/ToolMessagePart.js.map +1 -1
  12. package/dist/components/chat-view/tool-part-rendering.js +1 -1
  13. package/dist/components/chat-view/tool-part-rendering.js.map +1 -1
  14. package/dist/components/layouts/Sidebar.d.ts.map +1 -1
  15. package/dist/components/layouts/Sidebar.js +4 -2
  16. package/dist/components/layouts/Sidebar.js.map +1 -1
  17. package/dist/components/planning/ActionPlanDetailView.d.ts +1 -0
  18. package/dist/components/planning/ActionPlanDetailView.d.ts.map +1 -1
  19. package/dist/components/planning/ActionPlanDetailView.js +2 -2
  20. package/dist/components/planning/ActionPlanDetailView.js.map +1 -1
  21. package/dist/components/planning/ConnectedPlanDetailView.d.ts +1 -0
  22. package/dist/components/planning/ConnectedPlanDetailView.d.ts.map +1 -1
  23. package/dist/components/planning/ConnectedPlanDetailView.js.map +1 -1
  24. package/dist/components/planning/TopBar.d.ts +1 -0
  25. package/dist/components/planning/TopBar.d.ts.map +1 -1
  26. package/dist/components/planning/TopBar.js +7 -4
  27. package/dist/components/planning/TopBar.js.map +1 -1
  28. package/dist/components/planning/plan-detail-utils.d.ts +1 -0
  29. package/dist/components/planning/plan-detail-utils.d.ts.map +1 -1
  30. package/dist/components/planning/plan-detail-utils.js +4 -1
  31. package/dist/components/planning/plan-detail-utils.js.map +1 -1
  32. package/dist/components/sidebar/nav-user.js +1 -1
  33. package/dist/components/sidebar/nav-user.js.map +1 -1
  34. package/dist/components/tool-invocation-card/ToolActions.d.ts +2 -4
  35. package/dist/components/tool-invocation-card/ToolActions.d.ts.map +1 -1
  36. package/dist/components/tool-invocation-card/ToolActions.js +2 -9
  37. package/dist/components/tool-invocation-card/ToolActions.js.map +1 -1
  38. package/dist/components/tool-invocation-card/ToolHeader.d.ts +0 -1
  39. package/dist/components/tool-invocation-card/ToolHeader.d.ts.map +1 -1
  40. package/dist/components/tool-invocation-card/ToolHeader.js +11 -4
  41. package/dist/components/tool-invocation-card/ToolHeader.js.map +1 -1
  42. package/dist/components/tool-invocation-card/ToolInvocationCard.d.ts +0 -7
  43. package/dist/components/tool-invocation-card/ToolInvocationCard.d.ts.map +1 -1
  44. package/dist/components/tool-invocation-card/ToolInvocationCard.js +6 -8
  45. package/dist/components/tool-invocation-card/ToolInvocationCard.js.map +1 -1
  46. package/dist/explain-fact/rule-presentation.d.ts.map +1 -1
  47. package/dist/explain-fact/rule-presentation.js +1 -6
  48. package/dist/explain-fact/rule-presentation.js.map +1 -1
  49. package/dist/explain-fact/tree-model.d.ts +2 -1
  50. package/dist/explain-fact/tree-model.d.ts.map +1 -1
  51. package/dist/explain-fact/tree-model.js +17 -2
  52. package/dist/explain-fact/tree-model.js.map +1 -1
  53. package/dist/hooks/use-connection-problems.d.ts.map +1 -1
  54. package/dist/hooks/use-manual-actions.js +1 -1
  55. package/dist/hooks/use-manual-actions.js.map +1 -1
  56. package/dist/hooks/useActionGraph.d.ts +7 -0
  57. package/dist/hooks/useActionGraph.d.ts.map +1 -0
  58. package/dist/hooks/useActionGraph.js +23 -0
  59. package/dist/hooks/useActionGraph.js.map +1 -0
  60. package/dist/hooks/useAuth.d.ts +1 -0
  61. package/dist/hooks/useAuth.d.ts.map +1 -1
  62. package/dist/hooks/useAuth.js +2 -1
  63. package/dist/hooks/useAuth.js.map +1 -1
  64. package/dist/hooks/useValidateActionPlan.d.ts.map +1 -1
  65. package/dist/lib/datastore-route-table.d.ts +1 -0
  66. package/dist/lib/datastore-route-table.d.ts.map +1 -1
  67. package/dist/lib/datastore-route-table.js +1 -0
  68. package/dist/lib/datastore-route-table.js.map +1 -1
  69. package/dist/lib/tool-icons.d.ts.map +1 -1
  70. package/dist/lib/tool-icons.js +1 -0
  71. package/dist/lib/tool-icons.js.map +1 -1
  72. package/dist/providers/AuthContext.d.ts +1 -0
  73. package/dist/providers/AuthContext.d.ts.map +1 -1
  74. package/dist/providers/AuthContext.js.map +1 -1
  75. package/dist/routes/plans/components/ActionPlansPage.d.ts.map +1 -1
  76. package/dist/routes/plans/components/ActionPlansPage.js +3 -3
  77. package/dist/routes/plans/components/ActionPlansPage.js.map +1 -1
  78. package/dist/routes/plans/components/PlanDetailRouteContent.d.ts.map +1 -1
  79. package/dist/routes/plans/components/PlanDetailRouteContent.js +3 -1
  80. package/dist/routes/plans/components/PlanDetailRouteContent.js.map +1 -1
  81. package/package.json +12 -7
  82. package/src/app-subpaths.json +1 -0
  83. package/src/components/chat-view/Chat.tsx +60 -5
  84. package/src/components/chat-view/ToolApprovalPart.tsx +27 -0
  85. package/src/components/chat-view/ToolMessagePart.tsx +8 -25
  86. package/src/components/chat-view/tool-part-rendering.ts +1 -1
  87. package/src/components/layouts/Sidebar.tsx +3 -2
  88. package/src/components/planning/ActionPlanDetailView.tsx +3 -0
  89. package/src/components/planning/ConnectedPlanDetailView.tsx +1 -0
  90. package/src/components/planning/TopBar.tsx +14 -3
  91. package/src/components/planning/plan-detail-utils.tsx +5 -1
  92. package/src/components/sidebar/nav-user.tsx +1 -1
  93. package/src/components/tool-invocation-card/ToolActions.tsx +5 -16
  94. package/src/components/tool-invocation-card/ToolHeader.tsx +13 -6
  95. package/src/components/tool-invocation-card/ToolInvocationCard.tsx +6 -23
  96. package/src/explain-fact/rule-presentation.ts +1 -6
  97. package/src/explain-fact/tree-model.ts +23 -1
  98. package/src/hooks/use-manual-actions.ts +1 -1
  99. package/src/hooks/useActionGraph.ts +41 -0
  100. package/src/hooks/useAuth.ts +3 -1
  101. package/src/lib/datastore-route-table.ts +2 -0
  102. package/src/lib/tool-icons.ts +1 -0
  103. package/src/providers/AuthContext.ts +1 -0
  104. package/src/routes/plans/components/ActionPlansPage.tsx +4 -4
  105. package/src/routes/plans/components/PlanDetailRouteContent.tsx +3 -0
@@ -16,7 +16,7 @@ import { humanCount } from "@manta-eu/lib/humanize";
16
16
  import { isPresent } from "@manta-eu/lib/present";
17
17
  import type { ChatStatus, FileUIPart } from "ai";
18
18
  import { ArrowUp } from "lucide-react";
19
- import type { ReactNode } from "react";
19
+ import type { ChangeEvent, ReactNode, RefObject } from "react";
20
20
  import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
21
21
  import { toast } from "sonner";
22
22
  import { match, P } from "ts-pattern";
@@ -138,9 +138,41 @@ export function useInitialPrompt(initialPrompt: string | undefined, submit: (pro
138
138
  }, [initialPrompt, isConversationLoading, activeConversation, submit]);
139
139
  }
140
140
 
141
+ export type ComposerCarryOver = { conversationId: string | undefined; text: string };
142
+
143
+ /** The unsent text a remounted composer starts with: its own chat's, or that of the blank chat it just became. */
144
+ export function carriedComposerText(
145
+ carryOver: ComposerCarryOver,
146
+ conversationId: string | undefined,
147
+ hasPendingMessage: boolean,
148
+ ): string {
149
+ if (carryOver.conversationId === conversationId) return carryOver.text;
150
+ return carryOver.conversationId === undefined && hasPendingMessage ? carryOver.text : "";
151
+ }
152
+
153
+ function useComposerCarryOver(
154
+ carryOverRef: RefObject<ComposerCarryOver>,
155
+ conversationId: string | undefined,
156
+ hasPendingMessage: boolean,
157
+ ) {
158
+ const [initialText] = useState(() => carriedComposerText(carryOverRef.current, conversationId, hasPendingMessage));
159
+ const rememberText = useCallback(
160
+ (text: string) => {
161
+ carryOverRef.current = { conversationId, text };
162
+ },
163
+ [carryOverRef, conversationId],
164
+ );
165
+
166
+ useEffect(() => rememberText(initialText), [rememberText, initialText]);
167
+
168
+ return { initialText: isPresent(initialText) ? initialText : undefined, rememberText };
169
+ }
170
+
141
171
  export function Chat(props: ChatProps) {
142
172
  const turnState = useChatTurnState();
143
- const content = <ChatContent {...props} turnState={turnState} />;
173
+ // ChatContent remounts when a blank chat becomes a conversation; the composer's unsent text outlives it here.
174
+ const composerCarryOver = useRef<ComposerCarryOver>({ conversationId: undefined, text: "" });
175
+ const content = <ChatContent {...props} turnState={turnState} composerCarryOver={composerCarryOver} />;
144
176
 
145
177
  return <ActiveChatProvider>{props.isEmbedded ? <Suspense>{content}</Suspense> : content}</ActiveChatProvider>;
146
178
  }
@@ -208,7 +240,12 @@ export function chatIsConnectingOrSyncing(args: ChatConnectingArgs): boolean {
208
240
  return !args.isBlankChat && !args.hasMessages && (!args.isSocketOpen || !args.isTranscriptSynced);
209
241
  }
210
242
 
211
- function ChatContent(props: ChatProps & { turnState: ChatTurnState }) {
243
+ type ChatContentProps = ChatProps & {
244
+ turnState: ChatTurnState;
245
+ composerCarryOver: RefObject<ComposerCarryOver>;
246
+ };
247
+
248
+ function ChatContent(props: ChatContentProps) {
212
249
  const {
213
250
  datastore,
214
251
  user,
@@ -225,6 +262,7 @@ function ChatContent(props: ChatProps & { turnState: ChatTurnState }) {
225
262
  mark,
226
263
  renderMenuItems,
227
264
  turnState,
265
+ composerCarryOver,
228
266
  } = props;
229
267
 
230
268
  const assistantName = useAssistantName();
@@ -377,6 +415,22 @@ function ChatContent(props: ChatProps & { turnState: ChatTurnState }) {
377
415
  );
378
416
 
379
417
  const composerDraft = useComposerDraft(initialDraft, handleAgentSubmit);
418
+ const { initialText: carriedText, rememberText } = useComposerCarryOver(
419
+ composerCarryOver,
420
+ activeConversation?.id,
421
+ hasPendingMessage,
422
+ );
423
+ const submitComposer = useCallback(
424
+ (value: string, files: File[]) => {
425
+ rememberText("");
426
+ return composerDraft.onSubmit(value, files);
427
+ },
428
+ [composerDraft, rememberText],
429
+ );
430
+ const rememberComposerText = useCallback(
431
+ (event: ChangeEvent<HTMLTextAreaElement>) => rememberText(event.target.value),
432
+ [rememberText],
433
+ );
380
434
 
381
435
  const handleConversationStarterSubmit = useCallback(
382
436
  (value: string) => {
@@ -584,9 +638,9 @@ function ChatContent(props: ChatProps & { turnState: ChatTurnState }) {
584
638
 
585
639
  <ChatInput
586
640
  key={composerDraft.key}
587
- initialValue={composerDraft.initialValue}
641
+ initialValue={composerDraft.initialValue ?? carriedText}
588
642
  dropTargetRef={dropZoneRef}
589
- onSubmit={isComposerBusy ? undefined : composerDraft.onSubmit}
643
+ onSubmit={isComposerBusy ? undefined : submitComposer}
590
644
  onRejected={handleFilesRejected}
591
645
  topContent={<ChatInputFilePreview progress={uploadProgress} />}
592
646
  >
@@ -596,6 +650,7 @@ function ChatContent(props: ChatProps & { turnState: ChatTurnState }) {
596
650
  className="pl-5"
597
651
  autoFocus={shouldAutoFocusInput}
598
652
  disabled={hasPendingQuestionnaire}
653
+ onChange={rememberComposerText}
599
654
  data-chat-input
600
655
  />
601
656
  <ChatInputSubmit
@@ -0,0 +1,27 @@
1
+ import { getToolUIPartLabel } from "@manta-eu/client/shared/tools";
2
+ import type { ToolUIPart } from "ai";
3
+ import { Card } from "#components/card/index";
4
+ import { ToolActions } from "#components/tool-invocation-card/ToolActions";
5
+
6
+ type ToolApprovalResponse = { id: string; approved: boolean };
7
+
8
+ type ToolApprovalPartProps = {
9
+ part: ToolUIPart;
10
+ onRespond: (response: ToolApprovalResponse) => void;
11
+ };
12
+
13
+ export function ToolApprovalPart(props: ToolApprovalPartProps) {
14
+ const { part, onRespond } = props;
15
+ const id = part.approval?.id;
16
+ if (id === undefined) return null;
17
+
18
+ return (
19
+ <Card padding="md" className="my-3 w-full max-w-2xl card-base gap-3">
20
+ <h4 className="typography-small-medium">{getToolUIPartLabel(part)}</h4>
21
+ <ToolActions
22
+ onApprove={() => onRespond({ id, approved: true })}
23
+ onReject={() => onRespond({ id, approved: false })}
24
+ />
25
+ </Card>
26
+ );
27
+ }
@@ -7,7 +7,9 @@ import { ActionPlanShowcaseCard } from "../tool-invocation-card/ActionPlanShowca
7
7
  import { ToolInvocationCard } from "../tool-invocation-card/ToolInvocationCard";
8
8
  import { isErrorToolResult, isShowcaseToolResult } from "../tool-invocation-card/types";
9
9
  import { RequestUserInputToolPart } from "./RequestUserInputToolPart";
10
+ import { ToolApprovalPart } from "./ToolApprovalPart";
10
11
  import { Banner } from "#components/banner/index";
12
+ import { useActiveChatConnection } from "#providers/ActiveChatProvider";
11
13
  import { useChatManagerConnection } from "#providers/ChatManagerProvider";
12
14
 
13
15
  type ActionPlanShowcaseToolPartProps = {
@@ -67,11 +69,16 @@ export function ToolMessagePart(props: ToolMessagePartProps) {
67
69
  const { part, toolsUsedInSession, onAddToolResult } = props;
68
70
  const toolCallId = part.toolCallId;
69
71
  const toolName = part.type.replace("tool-", "");
72
+ const chatData = useActiveChatConnection()?.chatData;
70
73
 
71
74
  if (toolName === "actionPlanShowcase") {
72
75
  return <ActionPlanShowcaseToolPart part={part} />;
73
76
  }
74
77
 
78
+ if (part.state === "approval-requested") {
79
+ return <ToolApprovalPart part={part} onRespond={(response) => chatData?.addToolApprovalResponse(response)} />;
80
+ }
81
+
75
82
  if (toolName === requestUserInputToolName) {
76
83
  return (
77
84
  <RequestUserInputToolPart
@@ -84,29 +91,5 @@ export function ToolMessagePart(props: ToolMessagePartProps) {
84
91
  );
85
92
  }
86
93
 
87
- return (
88
- <ToolInvocationCard
89
- toolUIPart={part}
90
- toolCallId={toolCallId}
91
- needsConfirmation={false}
92
- onSubmit={({ toolCallId, result }) => {
93
- toolsUsedInSession.current.add(toolName);
94
-
95
- onAddToolResult({
96
- tool: toolName,
97
- toolCallId,
98
- output: result,
99
- });
100
- }}
101
- addToolResult={(toolCallId, result) => {
102
- toolsUsedInSession.current.add(toolName);
103
-
104
- onAddToolResult({
105
- tool: toolName,
106
- toolCallId,
107
- output: result,
108
- });
109
- }}
110
- />
111
- );
94
+ return <ToolInvocationCard toolUIPart={part} />;
112
95
  }
@@ -17,7 +17,7 @@ function isFailedRequestUserInputPart(part: UIMessage["parts"][number]): boolean
17
17
  }
18
18
 
19
19
  export function isStandaloneToolPart(part: ToolUIPart): boolean {
20
- return standaloneToolPartTypes.has(part.type);
20
+ return standaloneToolPartTypes.has(part.type) || part.state === "approval-requested";
21
21
  }
22
22
 
23
23
  export function isPendingRequestUserInputPart(part: UIMessage["parts"][number]): boolean {
@@ -107,6 +107,7 @@ export function Sidebar(props: SidebarProps) {
107
107
  const canManageTeam = authContext?.canManageTeam ?? false;
108
108
  const canManageCustomApps = authContext?.canManageCustomApps ?? false;
109
109
  const canDisconnectCustomApps = authContext?.canDisconnectCustomApps ?? false;
110
+ const hasProfilePage = authContext?.hasProfilePage ?? false;
110
111
  const canManageStreams = authContext?.canManageStreams ?? isOperator ?? false;
111
112
 
112
113
  const { state: sidebarState, isMobile, toggleSidebar, setOpenMobile } = useSidebar();
@@ -467,11 +468,11 @@ export function Sidebar(props: SidebarProps) {
467
468
  }
468
469
  onCloseAutoFocus={handleProfileMenuCloseAutoFocus}
469
470
  onNavigateToAccount={() => {
470
- // TODO: Implement account navigation when account page is available
471
+ if (hasProfilePage) pendingMenuNavigateRef.current = "/profile";
471
472
  }}
472
473
  onLogout={onLogout}
473
474
  >
474
- <DropdownGroup className="p-2">
475
+ <DropdownGroup className="px-2 pb-2">
475
476
  <ThemeSelectorDropdown />
476
477
  {canManageDatastores && (
477
478
  <ManageDatastoresMenuItem onSelect={() => (pendingMenuNavigateRef.current = "/datastores")} />
@@ -25,6 +25,7 @@ export type ActionPlanDetailViewProps = {
25
25
  issueCount?: number;
26
26
  isValidating?: boolean;
27
27
  isDryRun?: boolean;
28
+ pendingApprovalCount?: number;
28
29
  hideTopBar?: boolean;
29
30
  hideChanges?: boolean;
30
31
  fitContent?: boolean;
@@ -51,6 +52,7 @@ export function ActionPlanDetailView(props: ActionPlanDetailViewProps) {
51
52
  issueCount = 0,
52
53
  isValidating = false,
53
54
  isDryRun = false,
55
+ pendingApprovalCount,
54
56
  hideTopBar = false,
55
57
  hideChanges = false,
56
58
  fitContent = false,
@@ -122,6 +124,7 @@ export function ActionPlanDetailView(props: ActionPlanDetailViewProps) {
122
124
  issueCount={issueCount}
123
125
  isValidating={isValidating}
124
126
  isDryRun={isDryRun}
127
+ pendingApprovalCount={pendingApprovalCount}
125
128
  planName={planName}
126
129
  status={status}
127
130
  hasCurrentFacts={plan.hasCurrentFacts}
@@ -24,6 +24,7 @@ export type ConnectedPlanDetailViewProps = {
24
24
  viewMode?: PlanViewMode;
25
25
  stateOverride?: GQLActionPlanState;
26
26
  isDryRun?: boolean;
27
+ pendingApprovalCount?: number;
27
28
  ref?: Ref<ConnectedPlanDetailViewHandle>;
28
29
  onViewModeChange?: (viewMode: PlanViewMode) => void;
29
30
  onExecute?: (planIri: string) => void;
@@ -3,7 +3,7 @@ import { CircleStop, Eye, History, List, Play, RotateCcw } from "lucide-react";
3
3
  import type { ReactNode } from "react";
4
4
  import { match } from "ts-pattern";
5
5
  import { ChatToggleButton } from "../layouts/ChatToggleButton";
6
- import { isTerminalActionPlanState, type ActionPlanStatus } from "./plan-detail-utils";
6
+ import { isTerminalActionPlanState, needsApproval, type ActionPlanStatus } from "./plan-detail-utils";
7
7
  import { Button } from "#components/button/index";
8
8
  import { Tabs, TabsList, TabsTrigger } from "#components/tabs/index";
9
9
 
@@ -32,13 +32,23 @@ export type TopBarProps = {
32
32
  onModify?: (planIri: string) => void;
33
33
  topBarActions?: ReactNode;
34
34
  leadingAction?: ReactNode;
35
+ pendingApprovalCount?: number;
35
36
  };
36
37
 
37
38
  function isPlanViewMode(value: string): value is PlanViewMode {
38
39
  return value === "overview" || value === "visual" || value === "history";
39
40
  }
40
41
 
41
- function statusSubtitle(status: ActionPlanStatus, isDryRun: boolean, hasCurrentFacts: boolean): string | null {
42
+ function statusSubtitle(
43
+ status: ActionPlanStatus,
44
+ isDryRun: boolean,
45
+ hasCurrentFacts: boolean,
46
+ pendingApprovalCount: number,
47
+ ): string | null {
48
+ if (!isDryRun && needsApproval(status, pendingApprovalCount)) {
49
+ return "Needs approval";
50
+ }
51
+
42
52
  return match(status)
43
53
  .with(GQLActionPlanState.Draft, () => (hasCurrentFacts ? "Plan is ready to run" : "Not currently executable"))
44
54
  .with(GQLActionPlanState.Running, () => (isDryRun ? "Dry run in progress" : "Plan is running"))
@@ -65,6 +75,7 @@ export function TopBar(props: TopBarProps) {
65
75
  onRerun,
66
76
  topBarActions,
67
77
  leadingAction,
78
+ pendingApprovalCount = 0,
68
79
  } = props;
69
80
  const isDraft = status === GQLActionPlanState.Draft;
70
81
  const isRunning = status === GQLActionPlanState.Running;
@@ -77,7 +88,7 @@ export function TopBar(props: TopBarProps) {
77
88
  }
78
89
  }
79
90
 
80
- const subtitle = statusSubtitle(status, isDryRun, hasCurrentFacts);
91
+ const subtitle = statusSubtitle(status, isDryRun, hasCurrentFacts, pendingApprovalCount);
81
92
 
82
93
  return (
83
94
  <div className="shrink-0 flex h-16 items-center justify-between gap-2 border-b border-default-border px-3 sm:px-6">
@@ -77,8 +77,12 @@ export function statusLabel(status: ActionPlanStatus): string {
77
77
  .exhaustive();
78
78
  }
79
79
 
80
+ export function canNeedApproval(state: ActionPlanStatus): boolean {
81
+ return state === GQLActionPlanState.Running || state === GQLActionPlanState.Completed;
82
+ }
83
+
80
84
  export function needsApproval(state: ActionPlanStatus, pendingApprovalCount: number): boolean {
81
- return pendingApprovalCount > 0 && (state === GQLActionPlanState.Running || state === GQLActionPlanState.Completed);
85
+ return pendingApprovalCount > 0 && canNeedApproval(state);
82
86
  }
83
87
 
84
88
  export function planStateVariant(state: ActionPlanStatus, pendingApprovalCount = 0): BadgeVariant {
@@ -83,7 +83,7 @@ export function NavUser(args: NavUserArgs) {
83
83
  {pickerSlot}
84
84
  {topSlot}
85
85
  {(pickerSlot || topSlot) && <DropdownSeparator />}
86
- <DropdownGroup className="p-2">
86
+ <DropdownGroup className={children ? "px-2 pt-2" : "p-2"}>
87
87
  <DropdownItem
88
88
  className="p-1.5 flex flex-row items-center gap-1.5 typography-caption h-11"
89
89
  onClick={onNavigateToAccount}
@@ -1,30 +1,19 @@
1
- import { APPROVAL } from "@manta-eu/client/shared/index";
2
- import { type ToolInvocationCardProps } from "./ToolInvocationCard";
3
1
  import { Button } from "#components/button/index";
4
2
 
5
3
  type ToolActionsProps = {
6
- toolCallId: string;
7
- toolName: string;
8
- onSubmit: ToolInvocationCardProps["onSubmit"];
4
+ onApprove: () => void;
5
+ onReject: () => void;
9
6
  };
10
7
 
11
8
  export function ToolActions(props: ToolActionsProps) {
12
- const { toolCallId, toolName: _toolName, onSubmit } = props;
13
-
14
- function handleReject() {
15
- onSubmit({ toolCallId, result: APPROVAL.NO });
16
- }
17
-
18
- function handleApprove() {
19
- onSubmit({ toolCallId, result: APPROVAL.YES });
20
- }
9
+ const { onApprove, onReject } = props;
21
10
 
22
11
  return (
23
12
  <div className="flex gap-2 justify-end">
24
- <Button variant="destructive" size="sm" onClick={handleReject}>
13
+ <Button variant="destructive" size="sm" onClick={onReject}>
25
14
  Reject
26
15
  </Button>
27
- <Button size="sm" onClick={handleApprove}>
16
+ <Button size="sm" onClick={onApprove}>
28
17
  Approve
29
18
  </Button>
30
19
  </div>
@@ -1,18 +1,24 @@
1
1
  import { getToolUIPartLabel } from "@manta-eu/client/shared/tools";
2
2
  import type { ToolUIPart } from "ai";
3
- import { CheckCircle2, ChevronDown, Loader2 } from "lucide-react";
3
+ import { AlertTriangle, CheckCircle2, ChevronDown, Loader2, XCircle } from "lucide-react";
4
+ import { match } from "ts-pattern";
4
5
 
5
6
  type ToolHeaderProps = {
6
7
  toolUIPart: ToolUIPart;
7
- needsConfirmation: boolean;
8
8
  isExpanded: boolean;
9
9
  onToggle: () => void;
10
10
  };
11
11
 
12
- export function ToolHeader(props: ToolHeaderProps) {
13
- const { toolUIPart, isExpanded, onToggle, needsConfirmation } = props;
12
+ function StateIcon(props: { state: ToolUIPart["state"] }) {
13
+ return match(props.state)
14
+ .with("output-available", () => <CheckCircle2 size={16} />)
15
+ .with("output-denied", () => <XCircle size={16} />)
16
+ .with("output-error", () => <AlertTriangle size={16} />)
17
+ .otherwise(() => <Loader2 size={16} className="animate-spin" />);
18
+ }
14
19
 
15
- const isComplete = !needsConfirmation && toolUIPart.state === "output-available";
20
+ export function ToolHeader(props: ToolHeaderProps) {
21
+ const { toolUIPart, isExpanded, onToggle } = props;
16
22
 
17
23
  const humanToolName = getToolUIPartLabel(toolUIPart);
18
24
 
@@ -23,8 +29,9 @@ export function ToolHeader(props: ToolHeaderProps) {
23
29
  return (
24
30
  <button type="button" onClick={handleToggle} className="w-full flex items-center gap-2 cursor-pointer">
25
31
  <h4 className="typography-small-medium flex items-center gap-2 flex-1 text-left">
26
- {isComplete ? <CheckCircle2 size={16} /> : <Loader2 size={16} className="animate-spin" />}
32
+ <StateIcon state={toolUIPart.state} />
27
33
  {humanToolName}
34
+ {toolUIPart.state === "output-denied" && <span className="text-muted-foreground">Rejected</span>}
28
35
  </h4>
29
36
  <ChevronDown
30
37
  size={16}
@@ -2,7 +2,6 @@ import { getToolUIPartLabel } from "@manta-eu/client/shared/tools";
2
2
  import type { ToolUIPart } from "ai";
3
3
  import { useCallback, useState } from "react";
4
4
  import { match } from "ts-pattern";
5
- import { ToolActions } from "./ToolActions";
6
5
  import { ToolArgumentsDisplay } from "./ToolArgumentsDisplay";
7
6
  import { ToolHeader } from "./ToolHeader";
8
7
  import { ToolLoadingState } from "./ToolLoadingState";
@@ -12,30 +11,21 @@ import { cn } from "#lib/utils";
12
11
 
13
12
  export type ToolInvocationCardProps = {
14
13
  toolUIPart: ToolUIPart;
15
- toolCallId: string;
16
- needsConfirmation: boolean;
17
- onSubmit: ({ toolCallId, result }: { toolCallId: string; result: string }) => void;
18
- addToolResult: (toolCallId: string, result: string) => void;
19
14
  };
20
15
 
21
16
  export function ToolInvocationCard(props: ToolInvocationCardProps) {
22
- const { toolUIPart, toolCallId, needsConfirmation, onSubmit } = props;
17
+ const { toolUIPart } = props;
23
18
  const [isExpanded, setIsExpanded] = useState(false);
24
19
  const loadingLabel = getToolUIPartLabel(toolUIPart);
25
20
  const [executionStartTime] = useState(Date.now());
26
21
 
27
22
  const handleToggle = useCallback(() => {
28
- setIsExpanded(!isExpanded);
23
+ setIsExpanded((expanded) => !expanded);
29
24
  }, []);
30
25
 
31
26
  return (
32
27
  <Card className="my-3 w-full max-w-2xl card-base overflow-hidden">
33
- <ToolHeader
34
- toolUIPart={toolUIPart}
35
- needsConfirmation={needsConfirmation}
36
- isExpanded={isExpanded}
37
- onToggle={handleToggle}
38
- />
28
+ <ToolHeader toolUIPart={toolUIPart} isExpanded={isExpanded} onToggle={handleToggle} />
39
29
 
40
30
  <div
41
31
  className={cn([
@@ -46,22 +36,15 @@ export function ToolInvocationCard(props: ToolInvocationCardProps) {
46
36
  <div className="overflow-y-auto" style={{ maxHeight: isExpanded ? "580px" : "0px" }}>
47
37
  <ToolArgumentsDisplay toolUIPart={toolUIPart} />
48
38
 
49
- {match({ needsConfirmation, state: toolUIPart.state })
50
- .with({ needsConfirmation: true, state: "input-available" }, () => (
51
- <ToolActions
52
- toolCallId={toolCallId}
53
- toolName={toolUIPart.type.replace("tool-", "")}
54
- onSubmit={onSubmit}
55
- />
56
- ))
57
- .with({ needsConfirmation: false, state: "output-available" }, () => (
39
+ {match(toolUIPart.state)
40
+ .with("output-available", () => (
58
41
  <ToolResultDisplay
59
42
  result={toolUIPart.output}
60
43
  executionStartTime={executionStartTime}
61
44
  toolUIPart={toolUIPart}
62
45
  />
63
46
  ))
64
- .with({ state: "input-streaming" }, () => <ToolLoadingState label={loadingLabel} />)
47
+ .with("input-streaming", () => <ToolLoadingState label={loadingLabel} />)
65
48
  .otherwise(() => null)}
66
49
  </div>
67
50
  </div>
@@ -20,6 +20,7 @@ import { isPresent } from "@manta-eu/lib/present";
20
20
  import { match, P } from "ts-pattern";
21
21
  import { parseDatalogRuleCached, parseGroundedDatalogRuleCached } from "../rules/datalog-cache";
22
22
  import {
23
+ explainRuleAst,
23
24
  getRuleBodyTerms,
24
25
  kindForBodyFormula,
25
26
  kindForSyntheticTerm,
@@ -362,12 +363,6 @@ function ruleNameTermFromHeadText(headText: string): string {
362
363
  return predicate.trim();
363
364
  }
364
365
 
365
- // Explain payloads echo rules as the datastore serializes them (bare ON, atom-free aggregates),
366
- // so authored text falls through to the expanded grammar when the strict parse fails.
367
- function explainRuleAst(rule: string): DatalogRule | undefined {
368
- return parseDatalogRuleCached(rule).ast ?? parseGroundedDatalogRuleCached(rule).ast ?? undefined;
369
- }
370
-
371
366
  function collectBindings(ast: DatalogRule, groundedAst: DatalogRule): VariableBinding[] {
372
367
  const bindings = new Map<string, GroundedBinding>();
373
368
  const predicates = new Map<string, string>();
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  classAssertionFromAtom,
3
+ formulaSourceTexts,
3
4
  isDatalogAtom,
4
5
  isLiteralTerm,
5
6
  isVariableTerm,
@@ -11,6 +12,7 @@ import {
11
12
  type DatalogAtom,
12
13
  type DatalogBodyFormula,
13
14
  type DatalogExpression,
15
+ type DatalogRule,
14
16
  } from "@manta-eu/datalog-parser";
15
17
  import { isPresent } from "@manta-eu/lib/present";
16
18
  import { countBy } from "ramda";
@@ -472,13 +474,33 @@ export function ruleHeadText(rule: string): string {
472
474
  );
473
475
  }
474
476
 
477
+ // Explain payloads echo rules as the datastore serializes them (bare ON, atom-free aggregates),
478
+ // so authored text falls through to the expanded grammar when the strict parse fails.
479
+ export function explainRuleAst(rule: string): DatalogRule | undefined {
480
+ return parseDatalogRuleCached(rule).ast ?? parseGroundedDatalogRuleCached(rule).ast ?? undefined;
481
+ }
482
+
483
+ function parsedSourceTexts(rule: string) {
484
+ const parsed = explainRuleAst(rule);
485
+
486
+ return parsed ? formulaSourceTexts(parsed) : undefined;
487
+ }
488
+
475
489
  export function ruleHeadTerms(rule: string): string[] {
490
+ const parsed = parsedSourceTexts(rule);
491
+
492
+ if (parsed) return parsed.head;
493
+
476
494
  return splitTopLevel(ruleHeadText(rule), ",")
477
495
  .map((part) => part.trim())
478
496
  .filter(isPresent);
479
497
  }
480
498
 
481
499
  export function getRuleBodyTerms(rule: string): string[] {
500
+ const parsed = parsedSourceTexts(rule);
501
+
502
+ if (parsed) return parsed.body;
503
+
482
504
  const body = bodyText(rule);
483
505
 
484
506
  if (!body) return [];
@@ -624,7 +646,7 @@ function aggregateOutputsOf(
624
646
  }
625
647
 
626
648
  function ruleBodyFormulas(rule: string, expectedCount: number): DatalogBodyFormula[] | undefined {
627
- const ast = parseDatalogRuleCached(rule).ast ?? parseGroundedDatalogRuleCached(rule).ast ?? undefined;
649
+ const ast = explainRuleAst(rule);
628
650
 
629
651
  if (!ast || ast.body.length !== expectedCount) return undefined;
630
652
 
@@ -74,7 +74,7 @@ export function toManualAction(entity: ManualActionEntity, options: ManualAction
74
74
  assignees: entity.assignee.map(String),
75
75
  label: entity.label || null,
76
76
  description: entity.description || null,
77
- observedValue: entity.observedValue || null,
77
+ observedValue: typeof entity.observedValue === "string" ? entity.observedValue || null : null,
78
78
  aboutIri: entity.about ?? null,
79
79
  retryable: String(entity.retryable) === "true",
80
80
  defaultResolution: normalizeResolution(entity.defaultResolution ?? undefined),
@@ -0,0 +1,41 @@
1
+ import {
2
+ runActionGraph,
3
+ type ActionGraphql,
4
+ type GraphOp,
5
+ type GraphRun,
6
+ type GraphRunOptions,
7
+ } from "@manta-eu/client/shared/action-graph";
8
+ import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
9
+ import { uniq } from "ramda";
10
+ import { useSparqlOptions } from "#hooks/use-sparql-options";
11
+ import { useMantaClient } from "#providers/MantaClientProvider";
12
+ import { invalidateActionPlanQueries } from "#shared/query-keys";
13
+
14
+ export type UseActionGraphOptions = Omit<GraphRunOptions, "graphql" | "readOptions">;
15
+
16
+ export const actionGraphMutationKey = ["ExecuteActionGraph"] as const;
17
+
18
+ const planGraphIris = ({ results }: GraphRun): string[] =>
19
+ uniq(results.flatMap((result) => ("planGraphIri" in result ? [result.planGraphIri] : [])));
20
+
21
+ /** Sends a change of several entities as one plan and reads back what each op did; `results[i]` is `ops[i]`. */
22
+ export function useActionGraph(
23
+ options: UseActionGraphOptions = {},
24
+ ): UseMutationResult<GraphRun, Error, readonly GraphOp[]> {
25
+ const { mantaClient } = useMantaClient();
26
+ const readOptions = useSparqlOptions();
27
+ const queryClient = useQueryClient();
28
+
29
+ const graphql: ActionGraphql = (document, variables, request) =>
30
+ mantaClient.request({ document, variables, signal: request?.signal });
31
+
32
+ return useMutation<GraphRun, Error, readonly GraphOp[]>({
33
+ mutationKey: actionGraphMutationKey,
34
+ mutationFn: (ops) => runActionGraph(ops, { ...options, graphql, readOptions }),
35
+ onSuccess: async (run) => {
36
+ await Promise.all(
37
+ planGraphIris(run).map((planGraphIri) => invalidateActionPlanQueries(queryClient, planGraphIri)),
38
+ );
39
+ },
40
+ });
41
+ }
@@ -65,6 +65,8 @@ const orgRoleSchema = z.object({
65
65
  capabilities: z.array(z.string()).transform((values) => values.filter(isCapability)),
66
66
  });
67
67
 
68
+ export const userContextQueryKey = ["user-context"] as const;
69
+
68
70
  export const contextResponseSchema = z.object({
69
71
  datastoreAccessGranted: z.boolean().default(true),
70
72
  user: z.object({
@@ -197,7 +199,7 @@ export function useAuth(): UseAuthResult {
197
199
  isLoading: isContextLoading,
198
200
  error: contextError,
199
201
  } = useQuery({
200
- queryKey: ["user-context", datastoreId],
202
+ queryKey: [...userContextQueryKey, datastoreId],
201
203
  queryFn: async ({ signal }) => {
202
204
  const res = await authorizedFetch(new URL("/graphql", mantaBaseUrl), {
203
205
  method: "POST",
@@ -19,6 +19,7 @@ export type RouteElements = {
19
19
  Team?: ReactNode;
20
20
  CustomApps?: ReactNode;
21
21
  CustomAppUsers?: ReactNode;
22
+ Profile?: ReactNode;
22
23
  DataExplorerLayout?: ReactNode;
23
24
  DataExplorerIndex?: ReactNode;
24
25
  DataExplorerRecent?: ReactNode;
@@ -48,6 +49,7 @@ export const globalRoutes: RouteDefinition[] = [
48
49
  { path: "/shopify/connect", render: (e) => e.ShopifyConnect || notFoundElement(e) },
49
50
  { path: "/custom-apps", render: (e) => e.CustomApps || notFoundElement(e) },
50
51
  { path: "/custom-apps/:clientId/users", render: (e) => e.CustomAppUsers || notFoundElement(e) },
52
+ { path: "/profile", render: (e) => e.Profile || notFoundElement(e) },
51
53
  ];
52
54
 
53
55
  export const datastoreRoutes: RouteDefinition[] = [
@@ -44,6 +44,7 @@ const toolIconMap: Record<ToolName, LucideIcon> = {
44
44
  setDailyChangeLimit: DollarSign,
45
45
  markAsLowRisk: CheckCircle2,
46
46
  readUploadedFile: Paperclip,
47
+ requestBudgetChange: DollarSign,
47
48
  };
48
49
 
49
50
  type StepIconName =