@manta-eu/react 0.4.0 → 0.5.1

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 (96) hide show
  1. package/README.md +1 -0
  2. package/dist/components/chat-view/Chat.d.ts +27 -1
  3. package/dist/components/chat-view/Chat.d.ts.map +1 -1
  4. package/dist/components/chat-view/Chat.js +76 -18
  5. package/dist/components/chat-view/Chat.js.map +1 -1
  6. package/dist/components/embedded-chat/index.d.ts +1 -1
  7. package/dist/components/embedded-chat/index.d.ts.map +1 -1
  8. package/dist/components/embedded-chat/index.js +1 -1
  9. package/dist/components/embedded-chat/index.js.map +1 -1
  10. package/dist/components/layouts/Sidebar.d.ts.map +1 -1
  11. package/dist/components/layouts/Sidebar.js +4 -2
  12. package/dist/components/layouts/Sidebar.js.map +1 -1
  13. package/dist/components/planning/ActioningView.d.ts.map +1 -1
  14. package/dist/components/planning/ActioningView.js +1 -1
  15. package/dist/components/planning/ActioningView.js.map +1 -1
  16. package/dist/components/planning/ExecutionTopBar.d.ts +1 -0
  17. package/dist/components/planning/ExecutionTopBar.d.ts.map +1 -1
  18. package/dist/components/planning/ExecutionTopBar.js +3 -2
  19. package/dist/components/planning/ExecutionTopBar.js.map +1 -1
  20. package/dist/components/planning/plan-detail-utils.d.ts +1 -0
  21. package/dist/components/planning/plan-detail-utils.d.ts.map +1 -1
  22. package/dist/components/planning/plan-detail-utils.js +5 -2
  23. package/dist/components/planning/plan-detail-utils.js.map +1 -1
  24. package/dist/components/sidebar/nav-user.js +1 -1
  25. package/dist/components/sidebar/nav-user.js.map +1 -1
  26. package/dist/components/tool-invocation-card/ToolOutputRenderer.d.ts.map +1 -1
  27. package/dist/components/tool-invocation-card/ToolOutputRenderer.js +5 -2
  28. package/dist/components/tool-invocation-card/ToolOutputRenderer.js.map +1 -1
  29. package/dist/explain-fact/rule-presentation.d.ts.map +1 -1
  30. package/dist/explain-fact/rule-presentation.js +1 -6
  31. package/dist/explain-fact/rule-presentation.js.map +1 -1
  32. package/dist/explain-fact/tree-model.d.ts +2 -1
  33. package/dist/explain-fact/tree-model.d.ts.map +1 -1
  34. package/dist/explain-fact/tree-model.js +17 -2
  35. package/dist/explain-fact/tree-model.js.map +1 -1
  36. package/dist/hooks/use-connection-problems.d.ts.map +1 -1
  37. package/dist/hooks/use-manual-actions.js +1 -1
  38. package/dist/hooks/use-manual-actions.js.map +1 -1
  39. package/dist/hooks/useAuth.d.ts +1 -0
  40. package/dist/hooks/useAuth.d.ts.map +1 -1
  41. package/dist/hooks/useAuth.js +2 -1
  42. package/dist/hooks/useAuth.js.map +1 -1
  43. package/dist/hooks/useValidateActionPlan.d.ts.map +1 -1
  44. package/dist/lib/chat-types.d.ts +17 -0
  45. package/dist/lib/chat-types.d.ts.map +1 -1
  46. package/dist/lib/chat-types.js.map +1 -1
  47. package/dist/lib/datastore-route-table.d.ts +1 -0
  48. package/dist/lib/datastore-route-table.d.ts.map +1 -1
  49. package/dist/lib/datastore-route-table.js +1 -0
  50. package/dist/lib/datastore-route-table.js.map +1 -1
  51. package/dist/providers/AuthContext.d.ts +1 -0
  52. package/dist/providers/AuthContext.d.ts.map +1 -1
  53. package/dist/providers/AuthContext.js.map +1 -1
  54. package/dist/providers/ChatManagerProvider.d.ts +7 -0
  55. package/dist/providers/ChatManagerProvider.d.ts.map +1 -1
  56. package/dist/providers/ChatManagerProvider.js +129 -16
  57. package/dist/providers/ChatManagerProvider.js.map +1 -1
  58. package/dist/providers/chat-connection-context.d.ts +4 -1
  59. package/dist/providers/chat-connection-context.d.ts.map +1 -1
  60. package/dist/providers/chat-connection-context.js.map +1 -1
  61. package/dist/providers/chat-connection-lifecycle.d.ts +1 -0
  62. package/dist/providers/chat-connection-lifecycle.d.ts.map +1 -1
  63. package/dist/providers/chat-connection-lifecycle.js.map +1 -1
  64. package/dist/providers/chat-socket-auth.d.ts +3 -1
  65. package/dist/providers/chat-socket-auth.d.ts.map +1 -1
  66. package/dist/providers/chat-socket-auth.js +27 -9
  67. package/dist/providers/chat-socket-auth.js.map +1 -1
  68. package/dist/providers/chat-startup-timing.d.ts +14 -0
  69. package/dist/providers/chat-startup-timing.d.ts.map +1 -0
  70. package/dist/providers/chat-startup-timing.js +72 -0
  71. package/dist/providers/chat-startup-timing.js.map +1 -0
  72. package/dist/routes/plans/components/ActionPlansTableColumns.d.ts.map +1 -1
  73. package/dist/routes/plans/components/ActionPlansTableColumns.js +3 -3
  74. package/dist/routes/plans/components/ActionPlansTableColumns.js.map +1 -1
  75. package/package.json +7 -7
  76. package/src/components/chat-view/Chat.tsx +122 -20
  77. package/src/components/embedded-chat/index.tsx +9 -2
  78. package/src/components/layouts/Sidebar.tsx +3 -2
  79. package/src/components/planning/ActioningView.tsx +1 -0
  80. package/src/components/planning/ExecutionTopBar.tsx +14 -2
  81. package/src/components/planning/plan-detail-utils.tsx +6 -2
  82. package/src/components/sidebar/nav-user.tsx +1 -1
  83. package/src/components/tool-invocation-card/ToolOutputRenderer.tsx +5 -2
  84. package/src/explain-fact/rule-presentation.ts +1 -6
  85. package/src/explain-fact/tree-model.ts +23 -1
  86. package/src/hooks/use-manual-actions.ts +1 -1
  87. package/src/hooks/useAuth.ts +3 -1
  88. package/src/lib/chat-types.ts +18 -0
  89. package/src/lib/datastore-route-table.ts +2 -0
  90. package/src/providers/AuthContext.ts +1 -0
  91. package/src/providers/ChatManagerProvider.tsx +166 -15
  92. package/src/providers/chat-connection-context.ts +4 -0
  93. package/src/providers/chat-connection-lifecycle.ts +1 -0
  94. package/src/providers/chat-socket-auth.ts +31 -12
  95. package/src/providers/chat-startup-timing.ts +89 -0
  96. package/src/routes/plans/components/ActionPlansTableColumns.tsx +3 -2
@@ -1,9 +1,10 @@
1
- import { chatTicketPath, chatTicketQueryParam } from "@manta-eu/client/shared/chat-manager";
1
+ import { bearerFetch } from "@manta-eu/client";
2
+ import { chatModeQueryParam, chatTicketPath, chatTicketQueryParam } from "@manta-eu/client/shared/chat-manager";
2
3
  import { datastoreHeader, datastoreQueryParam } from "@manta-eu/client/shared/datastore-path";
3
4
  import { isPresent } from "@manta-eu/lib/present";
4
5
  import { isTerminalCloseEvent } from "agents/client";
5
6
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
- import { providerFetch } from "./manta-client-context";
7
+ import type { ChatSocketAuthPhase } from "./chat-startup-timing";
7
8
  import { useMantaClient } from "./MantaClientProvider";
8
9
 
9
10
  const tokenRefreshCheckMs = 30_000;
@@ -30,7 +31,7 @@ function ticketFrom(body: unknown): string | undefined {
30
31
  }
31
32
 
32
33
  // partysocket clears shouldReconnect before a close it was asked for: an unmount, or a socket being replaced.
33
- function isRequestedClose(event: CloseEvent): boolean {
34
+ export function isRequestedClose(event: CloseEvent): boolean {
34
35
  const target: unknown = event.target;
35
36
  return typeof target === "object" && target !== null && "shouldReconnect" in target && !target.shouldReconnect;
36
37
  }
@@ -39,12 +40,16 @@ function isRequestedClose(event: CloseEvent): boolean {
39
40
  * A ticket is spent on its first connect, and partysocket would reconnect to the same URL, so the socket never
40
41
  * reconnects by itself: each connect waits for a new ticket, and a dropped socket asks for one.
41
42
  */
42
- export function useChatSocketAuth(datastoreQuery: Record<string, string>): ChatSocketAuth {
43
+ export function useChatSocketAuth(
44
+ datastoreQuery: Record<string, string>,
45
+ onPhase?: (phase: ChatSocketAuthPhase) => void,
46
+ ): ChatSocketAuth {
43
47
  const client = useMantaClient();
44
48
  const { getAccessToken, mantaBaseUrl } = client;
45
- const authorizedFetch = providerFetch(client);
46
- const authorizedFetchRef = useRef(authorizedFetch);
47
- authorizedFetchRef.current = authorizedFetch;
49
+ const onPhaseRef = useRef(onPhase);
50
+ onPhaseRef.current = onPhase;
51
+ const getAccessTokenRef = useRef(getAccessToken);
52
+ getAccessTokenRef.current = getAccessToken;
48
53
  const hasAccessToken = getAccessToken !== undefined;
49
54
  const datastoreKey = JSON.stringify(datastoreQuery);
50
55
  const datastoreQueryRef = useRef(datastoreQuery);
@@ -70,11 +75,24 @@ export function useChatSocketAuth(datastoreQuery: Record<string, string>): ChatS
70
75
  const delay = reconnectDelaysMs[Math.min(Math.max(drops.current.count - 1, 0), reconnectDelaysMs.length - 1)];
71
76
  const timer = setTimeout(() => {
72
77
  const query = datastoreQueryRef.current;
73
- authorizedFetchRef
74
- .current(new URL(`/${chatTicketPath}`, mantaBaseUrl), {
75
- method: "POST",
76
- headers: datastoreHeader(query[datastoreQueryParam]),
77
- })
78
+ const getToken = getAccessTokenRef.current;
79
+ if (!getToken) return;
80
+ let sends = 0;
81
+ const requestTicket = bearerFetch(getToken, async (input, init) => {
82
+ sends += 1;
83
+ onPhaseRef.current?.("ticket-requested");
84
+ const response = await fetch(input, init);
85
+ if (response.status === 401 && sends === 1) onPhaseRef.current?.("token-requested");
86
+ return response;
87
+ });
88
+ const ticketUrl = new URL(`/${chatTicketPath}`, mantaBaseUrl);
89
+ const mode = query[chatModeQueryParam];
90
+ if (isPresent(mode)) ticketUrl.searchParams.set(chatModeQueryParam, mode);
91
+ onPhaseRef.current?.("token-requested");
92
+ requestTicket(ticketUrl, {
93
+ method: "POST",
94
+ headers: datastoreHeader(query[datastoreQueryParam]),
95
+ })
78
96
  .then(async (response) => {
79
97
  if (response.status >= 400 && response.status < 500 && !retriedRefusals.has(response.status)) {
80
98
  if (!cancelled) console.warn(`Manta refused a chat ticket (${response.status}); not asking again`);
@@ -84,6 +102,7 @@ export function useChatSocketAuth(datastoreQuery: Record<string, string>): ChatS
84
102
  if (!isPresent(ticket)) throw new Error(`Manta refused a chat ticket (${response.status})`);
85
103
  if (cancelled) return;
86
104
  ticketedAt.current = Date.now();
105
+ onPhaseRef.current?.("ticket-received");
87
106
  setTicketed({ datastoreKey, query: { ...query, [chatTicketQueryParam]: ticket } });
88
107
  })
89
108
  .catch((error: unknown) => {
@@ -0,0 +1,89 @@
1
+ import type { ChatStartupReport } from "../lib/chat-types";
2
+
3
+ export type ChatSocketAuthPhase = "token-requested" | "ticket-requested" | "ticket-received";
4
+
5
+ export type ChatViewStartupPhase = "chat-mounted" | "first-paint" | "interactive";
6
+
7
+ export type ChatStartupPhase = ChatSocketAuthPhase | "socket-enabled" | "socket-open" | ChatViewStartupPhase;
8
+
9
+ export type ChatStartupTimeline = {
10
+ startedAt: number;
11
+ marks: Partial<Record<ChatStartupPhase, number>>;
12
+ ticketAttempts: number;
13
+ socketAttempts: number;
14
+ };
15
+
16
+ export function createChatStartupTimeline(): ChatStartupTimeline {
17
+ return { startedAt: performance.now(), marks: {}, ticketAttempts: 0, socketAttempts: 0 };
18
+ }
19
+
20
+ const attemptEnds: Partial<Record<ChatStartupPhase, ChatStartupPhase>> = {
21
+ "token-requested": "socket-open",
22
+ "ticket-requested": "socket-open",
23
+ "ticket-received": "socket-open",
24
+ "socket-enabled": "socket-open",
25
+ "chat-mounted": "interactive",
26
+ };
27
+
28
+ const measures: Partial<Record<ChatStartupPhase, { name: string; from: ChatStartupPhase | "start" }>> = {
29
+ "ticket-requested": { name: "host-token", from: "token-requested" },
30
+ "ticket-received": { name: "ticket", from: "ticket-requested" },
31
+ "socket-open": { name: "socket", from: "socket-enabled" },
32
+ "chat-mounted": { name: "until-chat-mount", from: "start" },
33
+ "first-paint": { name: "until-first-paint", from: "chat-mounted" },
34
+ interactive: { name: "until-interactive", from: "chat-mounted" },
35
+ };
36
+
37
+ export function markChatStartup(timeline: ChatStartupTimeline, phase: ChatStartupPhase): boolean {
38
+ const attemptEnd = attemptEnds[phase];
39
+ if (timeline.marks[attemptEnd ?? phase] !== undefined) return false;
40
+ const now = performance.now();
41
+ timeline.marks[phase] = now - timeline.startedAt;
42
+ if (phase === "chat-mounted") delete timeline.marks["first-paint"];
43
+ if (phase === "token-requested") timeline.ticketAttempts += 1;
44
+ if (phase === "socket-enabled") timeline.socketAttempts += 1;
45
+ const measure = measures[phase];
46
+ const from = measure && (measure.from === "start" ? 0 : timeline.marks[measure.from]);
47
+ try {
48
+ performance.mark(`manta-chat:${phase}`);
49
+ if (measure && from !== undefined) {
50
+ performance.measure(`manta-chat:${measure.name}`, { start: timeline.startedAt + from, end: now });
51
+ }
52
+ } catch {}
53
+ return true;
54
+ }
55
+
56
+ function elapsed(from: number, to: number): number {
57
+ return Math.max(0, Math.round(to - from));
58
+ }
59
+
60
+ export function chatStartupReport(
61
+ timeline: ChatStartupTimeline,
62
+ input: Pick<ChatStartupReport, "surface" | "mode" | "openIndex">,
63
+ ): ChatStartupReport | undefined {
64
+ const { marks } = timeline;
65
+ const socketOpen = marks["socket-open"];
66
+ const chatMounted = marks["chat-mounted"];
67
+ const firstPaint = marks["first-paint"];
68
+ const interactive = marks.interactive;
69
+ if (socketOpen === undefined || chatMounted === undefined || firstPaint === undefined || interactive === undefined) {
70
+ return undefined;
71
+ }
72
+ const tokenRequested = marks["token-requested"];
73
+ const ticketRequested = marks["ticket-requested"];
74
+ const ticketReceived = marks["ticket-received"];
75
+ const ticketed = input.mode === "ticket" && ticketRequested !== undefined && ticketReceived !== undefined;
76
+ return {
77
+ ...input,
78
+ ...(ticketed && tokenRequested !== undefined ? { hostTokenMs: elapsed(tokenRequested, ticketRequested) } : {}),
79
+ ...(ticketed
80
+ ? { ticketMs: elapsed(ticketRequested, ticketReceived), ticketAttempts: Math.max(1, timeline.ticketAttempts) }
81
+ : {}),
82
+ socketMs: elapsed(marks["socket-enabled"] ?? 0, socketOpen),
83
+ socketAttempts: Math.max(1, timeline.socketAttempts),
84
+ chatMountMs: elapsed(0, chatMounted),
85
+ socketReadyAtOpen: socketOpen <= chatMounted,
86
+ firstPaintMs: elapsed(chatMounted, firstPaint),
87
+ interactiveMs: elapsed(chatMounted, interactive),
88
+ };
89
+ }
@@ -16,6 +16,7 @@ import {
16
16
  IntegrationIcon,
17
17
  integrationToChannel,
18
18
  isTerminalActionPlanState,
19
+ needsApproval,
19
20
  planStateVariant,
20
21
  statusLabel,
21
22
  } from "#components/planning/plan-detail-utils";
@@ -38,9 +39,9 @@ function planStateLabel(plan: ActionPlan, pendingApprovalCount: number): string
38
39
  return match({
39
40
  state: plan.state,
40
41
  hasCurrentFacts: plan.hasCurrentFacts,
41
- hasPendingApproval: pendingApprovalCount > 0,
42
+ needsApproval: needsApproval(plan.state, pendingApprovalCount),
42
43
  })
43
- .with({ state: GQLActionPlanState.Running, hasPendingApproval: true }, () => "Needs approval")
44
+ .with({ needsApproval: true }, () => "Needs approval")
44
45
  .with({ state: GQLActionPlanState.Draft, hasCurrentFacts: false }, () => "Not currently executable")
45
46
  .with({ state: GQLActionPlanState.Draft }, () => "Ready to launch")
46
47
  .otherwise(({ state: matchedState }) => statusLabel(matchedState));