@opengeni/react 6.1.0 → 7.0.0-canary.35625901699001

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 (214) hide show
  1. package/README.md +83 -2
  2. package/dist/approvals.d.ts +2 -1
  3. package/dist/artifacts-document.js +1 -1
  4. package/dist/artifacts-presentation.js +1 -1
  5. package/dist/artifacts-spreadsheet.js +1 -1
  6. package/dist/artifacts.js +3 -3
  7. package/dist/{browser-viewer-NTD6UIPR.js → browser-viewer-LGH4IP47.js} +3 -3
  8. package/dist/capability-catalog-row.d.ts +20 -0
  9. package/dist/capability-logo-fallback.d.ts +3 -0
  10. package/dist/{chunk-NOSYBS2D.js → chunk-2VHARTBL.js} +2 -1
  11. package/dist/{chunk-VRBL3F5E.js → chunk-5GAL23FF.js} +10 -2
  12. package/dist/{chunk-VRBL3F5E.js.map → chunk-5GAL23FF.js.map} +1 -1
  13. package/dist/chunk-5UONOQ3Y.js +635 -0
  14. package/dist/chunk-5UONOQ3Y.js.map +1 -0
  15. package/dist/{chunk-NYESIADM.js → chunk-AKOMC3VO.js} +3 -3
  16. package/dist/chunk-AKOMC3VO.js.map +1 -0
  17. package/dist/chunk-BRM6FVZA.js +302 -0
  18. package/dist/chunk-BRM6FVZA.js.map +1 -0
  19. package/dist/{chunk-KANFU34B.js → chunk-DBPVUGQT.js} +11 -2
  20. package/dist/chunk-DBPVUGQT.js.map +1 -0
  21. package/dist/chunk-FHIGTPME.js +1589 -0
  22. package/dist/chunk-FHIGTPME.js.map +1 -0
  23. package/dist/{chunk-5M6VTFJ5.js → chunk-GBPCZH3Y.js} +2 -2
  24. package/dist/{chunk-YBM6CQRU.js → chunk-GEN54DU5.js} +4 -4
  25. package/dist/{chunk-UMECRJRV.js → chunk-HJ5VJ4JB.js} +21 -12
  26. package/dist/chunk-HJ5VJ4JB.js.map +1 -0
  27. package/dist/{chunk-R4KRSFGA.js → chunk-IP4BUBKH.js} +3 -3
  28. package/dist/{chunk-NLTS7JHI.js → chunk-JPHDWKZE.js} +5 -5
  29. package/dist/{chunk-TDSEILUH.js → chunk-MI6ALLLD.js} +1297 -1535
  30. package/dist/chunk-MI6ALLLD.js.map +1 -0
  31. package/dist/{chunk-ICA6R6TW.js → chunk-OWHPKAOV.js} +28 -20
  32. package/dist/chunk-OWHPKAOV.js.map +1 -0
  33. package/dist/{chunk-24M4YBCL.js → chunk-PNAVEK2U.js} +4 -4
  34. package/dist/{chunk-L2NBTKRX.js → chunk-TXCBE32L.js} +203 -19
  35. package/dist/chunk-TXCBE32L.js.map +1 -0
  36. package/dist/{chunk-45XEUVTK.js → chunk-UXE5SSNN.js} +293 -463
  37. package/dist/chunk-UXE5SSNN.js.map +1 -0
  38. package/dist/{chunk-PEI6RANI.js → chunk-VR4Z73EU.js} +203 -9
  39. package/dist/chunk-VR4Z73EU.js.map +1 -0
  40. package/dist/{chunk-HUIKIDM5.js → chunk-XMXRI5JK.js} +65 -18
  41. package/dist/chunk-XMXRI5JK.js.map +1 -0
  42. package/dist/client.d.ts +1 -1
  43. package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
  44. package/dist/components/chat-composer.d.ts +13 -3
  45. package/dist/components/markdown.d.ts +6 -3
  46. package/dist/components/message-timeline.d.ts +7 -2
  47. package/dist/components/preview-loading.d.ts +2 -0
  48. package/dist/components/session-capability-frame.d.ts +24 -0
  49. package/dist/components/session-chrome.d.ts +1 -2
  50. package/dist/components/session-commands.d.ts +6 -0
  51. package/dist/components/session-connection-request.d.ts +8 -0
  52. package/dist/components/session-conversation.d.ts +3 -0
  53. package/dist/components/session-mcp-capability-card.d.ts +22 -0
  54. package/dist/components/timeline-search.d.ts +34 -0
  55. package/dist/composer.js +7 -7
  56. package/dist/connect-account-identity.d.ts +8 -0
  57. package/dist/connect-accounts.d.ts +5 -0
  58. package/dist/connect-chooser.d.ts +6 -0
  59. package/dist/connect-panel.d.ts +9 -2
  60. package/dist/connect-setup.d.ts +5 -1
  61. package/dist/connect.d.ts +5 -21
  62. package/dist/connect.js +1268 -584
  63. package/dist/connect.js.map +1 -1
  64. package/dist/connection-catalog.d.ts +6 -1
  65. package/dist/connection-discovery.d.ts +12 -0
  66. package/dist/connection-installed.d.ts +1 -0
  67. package/dist/connection-logo.d.ts +5 -2
  68. package/dist/connection-service-logo.d.ts +8 -0
  69. package/dist/connection-service-presentation.d.ts +13 -0
  70. package/dist/connection-skeleton.d.ts +5 -0
  71. package/dist/discovery-cache.d.ts +5 -0
  72. package/dist/hooks/use-codex-accounts.d.ts +11 -5
  73. package/dist/hooks/use-connect.d.ts +22 -0
  74. package/dist/hooks/use-session-events.d.ts +5 -0
  75. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  76. package/dist/hooks/use-workspace-sessions.d.ts +2 -0
  77. package/dist/index.d.ts +8 -2
  78. package/dist/index.js +771 -793
  79. package/dist/index.js.map +1 -1
  80. package/dist/interaction.js +2 -2
  81. package/dist/machines.js +2 -2
  82. package/dist/mcp-connection-status.d.ts +14 -0
  83. package/dist/{model-policy-picker-menu-3R336DP2.js → model-policy-picker-menu-RVOQ4ZZG.js} +4 -4
  84. package/dist/model-policy.d.ts +6 -2
  85. package/dist/model-policy.js +1 -1
  86. package/dist/{platform-activity-row-DUE2W74J.js → platform-activity-row-HLOH7FW4.js} +9 -4
  87. package/dist/platform-activity-row-HLOH7FW4.js.map +1 -0
  88. package/dist/plugin-discovery.d.ts +6 -0
  89. package/dist/{queue-surface-implementation-LK3S4BIW.js → queue-surface-implementation-3RIFK2BS.js} +4 -4
  90. package/dist/realtime.js +22 -22
  91. package/dist/realtime.js.map +1 -1
  92. package/dist/session-auth-recommendation.d.ts +5 -0
  93. package/dist/session-capability-policy.d.ts +12 -0
  94. package/dist/session-ui.d.ts +2 -0
  95. package/dist/session-ui.js +14 -12
  96. package/dist/session.d.ts +1 -0
  97. package/dist/session.js +6 -4
  98. package/dist/skill-discovery.d.ts +11 -0
  99. package/dist/timeline/compaction-copy.d.ts +10 -0
  100. package/dist/timeline/projection.d.ts +3 -1
  101. package/dist/timeline/tool-display-name.d.ts +2 -1
  102. package/dist/timeline/turn-summary.d.ts +2 -2
  103. package/dist/timeline/types.d.ts +24 -2
  104. package/dist/{timeline-annotation-markers-DRSTLEM6.js → timeline-annotation-markers-OY6OWR3V.js} +2 -2
  105. package/dist/{timeline-annotation-selection-IHLUGVND.js → timeline-annotation-selection-BOXA2TIN.js} +2 -2
  106. package/dist/{timeline-annotations-dialog-6Q53N5FG.js → timeline-annotations-dialog-RGY3MHL3.js} +3 -3
  107. package/package.json +3 -3
  108. package/src/approvals.ts +8 -1
  109. package/src/assets/README.md +8 -0
  110. package/src/assets/gmail.ico +0 -0
  111. package/src/assets/outlook.ico +0 -0
  112. package/src/capability-catalog-row.tsx +94 -0
  113. package/src/capability-logo-fallback.ts +22 -0
  114. package/src/client.ts +0 -9
  115. package/src/components/approval-surface.tsx +12 -2
  116. package/src/components/artifacts/document-editor.tsx +63 -6
  117. package/src/components/artifacts/presentation-editor.tsx +214 -9
  118. package/src/components/artifacts/spreadsheet-canvas.ts +3 -8
  119. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  120. package/src/components/artifacts/spreadsheet-grid.tsx +10 -11
  121. package/src/components/chat-composer.tsx +26 -3
  122. package/src/components/composer.tsx +10 -4
  123. package/src/components/human-input-form.tsx +12 -2
  124. package/src/components/markdown.tsx +133 -9
  125. package/src/components/message-timeline.tsx +104 -36
  126. package/src/components/model-picker.tsx +11 -3
  127. package/src/components/model-policy-picker-menu.tsx +10 -3
  128. package/src/components/preview-loading.tsx +154 -0
  129. package/src/components/sandbox-workspace.tsx +20 -36
  130. package/src/components/session-capability-frame.tsx +163 -0
  131. package/src/components/session-chrome.tsx +46 -77
  132. package/src/components/session-commands.tsx +18 -0
  133. package/src/components/session-connection-request.tsx +97 -0
  134. package/src/components/session-conversation.tsx +10 -5
  135. package/src/components/session-mcp-capability-card.tsx +532 -0
  136. package/src/components/session-status.tsx +2 -2
  137. package/src/components/timeline-anchor.tsx +9 -8
  138. package/src/components/timeline-search.tsx +168 -0
  139. package/src/components/user-message-body.tsx +7 -0
  140. package/src/connect-account-identity.tsx +34 -0
  141. package/src/connect-accounts.tsx +114 -52
  142. package/src/connect-chooser.tsx +171 -39
  143. package/src/connect-panel.tsx +73 -9
  144. package/src/connect-setup.tsx +72 -25
  145. package/src/connect.ts +13 -22
  146. package/src/connection-assets.d.ts +4 -0
  147. package/src/connection-catalog.tsx +61 -39
  148. package/src/connection-discovery.tsx +235 -0
  149. package/src/connection-installed.tsx +2 -0
  150. package/src/connection-logo.tsx +18 -10
  151. package/src/connection-service-logo.tsx +55 -0
  152. package/src/connection-service-presentation.ts +46 -0
  153. package/src/connection-skeleton.tsx +16 -0
  154. package/src/conversation-timeline.ts +1 -0
  155. package/src/discovery-cache.ts +42 -0
  156. package/src/hooks/use-codex-accounts.ts +33 -14
  157. package/src/hooks/use-composer.ts +4 -5
  158. package/src/hooks/use-connect.ts +23 -0
  159. package/src/hooks/use-session-events.ts +114 -1
  160. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  161. package/src/hooks/use-workspace-sessions.ts +10 -0
  162. package/src/index.ts +22 -2
  163. package/src/mcp-connection-status.ts +35 -0
  164. package/src/model-policy.ts +21 -0
  165. package/src/plugin-details.tsx +8 -5
  166. package/src/plugin-discovery.tsx +92 -72
  167. package/src/realtime/realtime-control.tsx +6 -8
  168. package/src/session-auth-recommendation.ts +48 -0
  169. package/src/session-capability-policy.ts +83 -0
  170. package/src/session-ui.ts +2 -0
  171. package/src/session.ts +1 -0
  172. package/src/skill-discovery.tsx +101 -56
  173. package/src/timeline/compaction-copy.ts +40 -0
  174. package/src/timeline/platform-activity-row.tsx +8 -3
  175. package/src/timeline/projection.ts +78 -2
  176. package/src/timeline/rolling-activity.tsx +4 -2
  177. package/src/timeline/shared.tsx +6 -1
  178. package/src/timeline/startup-timings.tsx +1 -1
  179. package/src/timeline/tool-display-name.ts +8 -6
  180. package/src/timeline/tool-renderers.tsx +5 -3
  181. package/src/timeline/turn-summary.tsx +17 -3
  182. package/src/timeline/types.ts +24 -2
  183. package/styles/compiled.css +325 -27
  184. package/styles/connect.css +90 -15
  185. package/styles/index.css +46 -0
  186. package/styles/session-capability.css +42 -0
  187. package/dist/chunk-45XEUVTK.js.map +0 -1
  188. package/dist/chunk-HQV2I5HV.js +0 -124
  189. package/dist/chunk-HQV2I5HV.js.map +0 -1
  190. package/dist/chunk-HUIKIDM5.js.map +0 -1
  191. package/dist/chunk-ICA6R6TW.js.map +0 -1
  192. package/dist/chunk-IHQII5WO.js +0 -886
  193. package/dist/chunk-IHQII5WO.js.map +0 -1
  194. package/dist/chunk-KANFU34B.js.map +0 -1
  195. package/dist/chunk-L2NBTKRX.js.map +0 -1
  196. package/dist/chunk-NYESIADM.js.map +0 -1
  197. package/dist/chunk-PEI6RANI.js.map +0 -1
  198. package/dist/chunk-TDSEILUH.js.map +0 -1
  199. package/dist/chunk-UMECRJRV.js.map +0 -1
  200. package/dist/hooks/use-packs.d.ts +0 -32
  201. package/dist/platform-activity-row-DUE2W74J.js.map +0 -1
  202. package/src/hooks/use-packs.ts +0 -157
  203. /package/dist/{browser-viewer-NTD6UIPR.js.map → browser-viewer-LGH4IP47.js.map} +0 -0
  204. /package/dist/{chunk-NOSYBS2D.js.map → chunk-2VHARTBL.js.map} +0 -0
  205. /package/dist/{chunk-5M6VTFJ5.js.map → chunk-GBPCZH3Y.js.map} +0 -0
  206. /package/dist/{chunk-YBM6CQRU.js.map → chunk-GEN54DU5.js.map} +0 -0
  207. /package/dist/{chunk-R4KRSFGA.js.map → chunk-IP4BUBKH.js.map} +0 -0
  208. /package/dist/{chunk-NLTS7JHI.js.map → chunk-JPHDWKZE.js.map} +0 -0
  209. /package/dist/{chunk-24M4YBCL.js.map → chunk-PNAVEK2U.js.map} +0 -0
  210. /package/dist/{model-policy-picker-menu-3R336DP2.js.map → model-policy-picker-menu-RVOQ4ZZG.js.map} +0 -0
  211. /package/dist/{queue-surface-implementation-LK3S4BIW.js.map → queue-surface-implementation-3RIFK2BS.js.map} +0 -0
  212. /package/dist/{timeline-annotation-markers-DRSTLEM6.js.map → timeline-annotation-markers-OY6OWR3V.js.map} +0 -0
  213. /package/dist/{timeline-annotation-selection-IHLUGVND.js.map → timeline-annotation-selection-BOXA2TIN.js.map} +0 -0
  214. /package/dist/{timeline-annotations-dialog-6Q53N5FG.js.map → timeline-annotations-dialog-RGY3MHL3.js.map} +0 -0
@@ -0,0 +1,168 @@
1
+ import { createContext, useContext, useLayoutEffect, useRef, type RefObject } from "react";
2
+ import type { TimelineItem } from "../timeline/types";
3
+ import { annotatableText, rangeFromOffsets, visibleClientRect } from "./timeline-annotation-shared";
4
+
5
+ export type TimelineSearchTarget = {
6
+ sequence: number;
7
+ eventId?: string | undefined;
8
+ /** Literal, case-insensitive text. No regex interpretation. */
9
+ query: string;
10
+ /** Zero-based occurrence within the source message. Defaults to zero. */
11
+ occurrence?: number | undefined;
12
+ /**
13
+ * Zero-based UTF-16 offset of the match in the original message text, as
14
+ * reported by backend all-occurrence search (messageMatchOffset). When set
15
+ * it takes precedence over `occurrence` and is validated against `query`,
16
+ * so a stale offset degrades to no highlight rather than a wrong one.
17
+ */
18
+ offset?: number | undefined;
19
+ };
20
+
21
+ /** Scoped to the target's group; disclosure expansion persists after closing find. */
22
+ export const TimelineSearchRevealContext = createContext<string | null>(null);
23
+ export const useTimelineSearchReveal = () => useContext(TimelineSearchRevealContext);
24
+
25
+ export function isTimelineSearchTarget(item: TimelineItem, target: TimelineSearchTarget): boolean {
26
+ const sources =
27
+ item.sourceEvents ??
28
+ ("annotationSource" in item && item.annotationSource ? [item.annotationSource] : []);
29
+ return sources.some(
30
+ (source) =>
31
+ source.sequence === target.sequence && (!target.eventId || source.eventId === target.eventId),
32
+ );
33
+ }
34
+
35
+ /** Scan without allocating an offset array proportional to a huge message. */
36
+ export function literalSearchOffset(text: string, query: string, occurrence = 0): number {
37
+ if (!query || !Number.isSafeInteger(occurrence) || occurrence < 0) return -1;
38
+ // Regex escaping preserves UTF-16 offsets even for case folds whose lowercase
39
+ // string representation changes length (for example dotted capital I).
40
+ const pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "giu");
41
+ let match: RegExpExecArray | null;
42
+ let index = 0;
43
+ while ((match = pattern.exec(text))) {
44
+ if (index++ === occurrence) return match.index;
45
+ }
46
+ return -1;
47
+ }
48
+
49
+ /**
50
+ * Validate a caller-supplied UTF-16 offset against the text and query. The
51
+ * sticky flag pins the match to exactly `offset`; like literalSearchOffset,
52
+ * regex escaping keeps offsets in UTF-16 units even for length-changing case
53
+ * folds. Returns -1 when the offset is stale or the text no longer matches.
54
+ */
55
+ export function matchAtOffset(text: string, query: string, offset: number): number {
56
+ if (!query || !Number.isSafeInteger(offset) || offset < 0 || offset > text.length) return -1;
57
+ const pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "iyu");
58
+ pattern.lastIndex = offset;
59
+ // Unicode regex engines may rewind a lastIndex inside a surrogate pair.
60
+ return pattern.exec(text)?.index === offset ? offset : -1;
61
+ }
62
+
63
+ /** Resolve a target to a UTF-16 offset: explicit offset wins, else the ordinal scan. */
64
+ export function searchMatchOffset(text: string, target: TimelineSearchTarget): number {
65
+ if (target.offset != null) return matchAtOffset(text, target.query, target.offset);
66
+ return literalSearchOffset(text, target.query, target.occurrence);
67
+ }
68
+
69
+ type HighlightRegistry = { set(name: string, value: unknown): void; delete(name: string): void };
70
+ let nextHighlightId = 0;
71
+
72
+ /** Range highlighting does not mutate React-owned text or its layout. */
73
+ export function useTimelineSearchNavigation(
74
+ root: RefObject<HTMLDivElement | null>,
75
+ itemId: string | undefined,
76
+ target: TimelineSearchTarget | null | undefined,
77
+ releasePin: () => void,
78
+ revision: unknown,
79
+ ) {
80
+ const highlightName = useRef("");
81
+ if (!highlightName.current) highlightName.current = `og-search-${++nextHighlightId}`;
82
+ const key = target
83
+ ? JSON.stringify([
84
+ itemId,
85
+ target.sequence,
86
+ target.eventId,
87
+ target.query,
88
+ target.occurrence ?? 0,
89
+ target.offset ?? null,
90
+ ])
91
+ : null;
92
+ const navigated = useRef<string | null>(null);
93
+ useLayoutEffect(() => {
94
+ if (!key) navigated.current = null;
95
+ if (!key || !target || !itemId || !root.current) return;
96
+ releasePin();
97
+ const scroller = root.current;
98
+ const registry = (
99
+ globalThis.CSS as (typeof CSS & { highlights?: HighlightRegistry }) | undefined
100
+ )?.highlights;
101
+ const HighlightClass = (
102
+ globalThis as unknown as { Highlight?: new (...ranges: Range[]) => unknown }
103
+ ).Highlight;
104
+ const style = document.createElement("style");
105
+ style.textContent = `::highlight(${highlightName.current}) { background: var(--og-accent, #facc15); color: var(--og-accent-fg, #171717); }`;
106
+ scroller.append(style);
107
+ let frame = 0;
108
+ const update = () => {
109
+ const source = [...scroller.querySelectorAll<HTMLElement>("[data-og-search-item]")].find(
110
+ (element) => element.dataset.ogSearchItem === itemId,
111
+ );
112
+ if (!source) return;
113
+ // A virtualized renderer can explicitly identify the selected occurrence
114
+ // when earlier occurrences are not mounted. Never accept a stale marker
115
+ // belonging to a different sequence, occurrence, or query.
116
+ const materialized = [
117
+ ...source.querySelectorAll<HTMLElement>("[data-og-search-occurrence]"),
118
+ ].find(
119
+ (element) =>
120
+ element.dataset.ogSearchSequence === String(target.sequence) &&
121
+ element.dataset.ogSearchQuery === target.query &&
122
+ (target.offset != null
123
+ ? element.dataset.ogSearchOffset === String(target.offset)
124
+ : element.dataset.ogSearchOccurrence === String(target.occurrence ?? 0)),
125
+ );
126
+ const { text, spans } = annotatableText(materialized ?? source);
127
+ // Inside a materialized chunk the match starts at the chunk start.
128
+ // Raw source offsets must NEVER be interpreted as rendered DOM offsets:
129
+ // even matching characters can identify a different repeated occurrence.
130
+ const offset = materialized
131
+ ? matchAtOffset(text, target.query, 0)
132
+ : target.offset != null
133
+ ? -1
134
+ : searchMatchOffset(text, target);
135
+ const range =
136
+ offset < 0 ? null : rangeFromOffsets(spans, offset, offset + target.query.length);
137
+ if (range && registry && HighlightClass)
138
+ registry.set(highlightName.current, new HighlightClass(range));
139
+ else registry?.delete(highlightName.current);
140
+ const rect = range ? visibleClientRect(range) : null;
141
+ // A custom renderer may mount the message body later (or materialize a
142
+ // virtualized chunk in response to renderMessageText's search context).
143
+ // Finding its empty shell is not proof that the occurrence was revealed.
144
+ if (navigated.current === key || !range || !rect) return;
145
+ const box = rect;
146
+ if (!box.height && !box.width) return;
147
+ scroller.scrollTop +=
148
+ box.top - scroller.getBoundingClientRect().top - scroller.clientHeight / 2;
149
+ navigated.current = key;
150
+ };
151
+ const schedule = () => {
152
+ cancelAnimationFrame(frame);
153
+ frame = requestAnimationFrame(update);
154
+ };
155
+ const observer = new MutationObserver(schedule);
156
+ observer.observe(scroller, { childList: true, subtree: true, characterData: true });
157
+ const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(schedule);
158
+ resize?.observe(scroller);
159
+ schedule();
160
+ return () => {
161
+ observer.disconnect();
162
+ resize?.disconnect();
163
+ cancelAnimationFrame(frame);
164
+ registry?.delete(highlightName.current);
165
+ style.remove();
166
+ };
167
+ }, [key, root, itemId, target, releasePin, revision]);
168
+ }
@@ -9,6 +9,7 @@ import {
9
9
  type ReactNode,
10
10
  } from "react";
11
11
  import { cn } from "../lib/cn";
12
+ import { useTimelineSearchReveal } from "./timeline-search";
12
13
 
13
14
  const FALLBACK_LINE_THRESHOLD = 12;
14
15
  const FALLBACK_TEXT_THRESHOLD = 900;
@@ -268,6 +269,12 @@ export function UserMessageBody({
268
269
  const [expanded, setExpanded] = useState(
269
270
  () => disclosure?.expandedByMessageId.get(messageId) ?? false,
270
271
  );
272
+ const searchReveal = useTimelineSearchReveal();
273
+ useLayoutEffect(() => {
274
+ if (!searchReveal) return;
275
+ setExpanded(true);
276
+ disclosure?.expandedByMessageId.set(messageId, true);
277
+ }, [searchReveal, messageId, disclosure]);
271
278
  const collapsibleRef = useRef(userMessageLikelyNeedsDisclosure(text));
272
279
  const expandedRef = useRef(expanded);
273
280
  expandedRef.current = expanded;
@@ -0,0 +1,34 @@
1
+ import type { ConnectAccount } from "@opengeni/connect";
2
+ import type { CapabilityCatalogItem, OpenGeniClient } from "@opengeni/sdk";
3
+ import { ConnectionServiceLogo } from "./connection-service-logo";
4
+ import {
5
+ accountServiceCapability,
6
+ connectionServicePresentation,
7
+ } from "./connection-service-presentation";
8
+
9
+ /** Service presentation never changes which exact credential is acted on. */
10
+ export function ConnectAccountIdentity({
11
+ account,
12
+ client,
13
+ capabilities,
14
+ }: {
15
+ account: ConnectAccount;
16
+ client?: OpenGeniClient | undefined;
17
+ capabilities: CapabilityCatalogItem[];
18
+ }) {
19
+ const item = accountServiceCapability(account, capabilities);
20
+ const service = connectionServicePresentation({ id: account.providerId, label: account.label });
21
+ const name = item?.name ?? service.name;
22
+ return (
23
+ <>
24
+ <ConnectionServiceLogo client={client} item={item} name={name} fallback={service.logo} />
25
+ <div>
26
+ <strong>{name}</strong>
27
+ <small>
28
+ {account.ownership === "personal" ? "Only you" : "Workspace connection"}
29
+ {name !== account.label ? ` · ${account.label}` : ""}
30
+ </small>
31
+ </div>
32
+ </>
33
+ );
34
+ }
@@ -1,12 +1,19 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
  import type { ConnectAccount, ConnectController } from "@opengeni/connect";
3
- import { useConnect } from "./connect";
3
+ import { useConnect } from "./hooks/use-connect";
4
+ import { ConnectAccountIdentity } from "./connect-account-identity";
5
+ import { ConnectionSkeleton } from "./connection-skeleton";
6
+ import type { CapabilityCatalogItem, OpenGeniClient } from "@opengeni/sdk";
4
7
 
5
8
  export type ConnectAccountsProps = {
6
9
  controller: ConnectController;
7
10
  className?: string;
8
11
  /** Enables explicit account-bound reconnect. Host supplies the exact destination. */
9
12
  returnUrl?: string;
13
+ /** Uses authenticated catalogue assets and exact connection references for service identity. */
14
+ client?: OpenGeniClient | undefined;
15
+ /** Reconcile surrounding service state after a confirmed account disconnect. */
16
+ onDisconnected?: (() => void) | undefined;
10
17
  };
11
18
 
12
19
  /** Local credential revocation only. Unknown or changed versions must be
@@ -21,33 +28,50 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
21
28
  return <ScopedAccounts key={generation} {...props} />;
22
29
  }
23
30
 
24
- function ScopedAccounts({ controller, className, returnUrl }: ConnectAccountsProps) {
31
+ function ScopedAccounts({
32
+ controller,
33
+ className,
34
+ returnUrl,
35
+ client,
36
+ onDisconnected,
37
+ }: ConnectAccountsProps) {
25
38
  const view = useConnect(controller);
26
39
  const [accounts, setAccounts] = useState<ConnectAccount[] | null>(null);
27
40
  const [selected, setSelected] = useState<string | null>(null);
28
41
  const [busy, setBusy] = useState(false);
29
42
  const [error, setError] = useState(false);
30
43
  const [reload, setReload] = useState(0);
44
+ const [catalog, setCatalog] = useState<{
45
+ client: OpenGeniClient;
46
+ items: CapabilityCatalogItem[];
47
+ } | null>(null);
31
48
  const mutation = useRef<AbortController | null>(null);
32
49
  useEffect(() => () => mutation.current?.abort(), []);
33
50
  useEffect(() => {
34
51
  const abort = new AbortController();
35
52
  setAccounts(null);
53
+ setCatalog(null);
36
54
  setSelected(null);
37
55
  setError(false);
38
56
  void Promise.resolve()
39
57
  .then(() => {
40
58
  abort.signal.throwIfAborted();
41
- return controller.transport.accounts(controller.workspaceId, { signal: abort.signal });
59
+ return Promise.all([
60
+ controller.transport.accounts(controller.workspaceId, { signal: abort.signal }),
61
+ client?.listCapabilities(controller.workspaceId).catch(() => null) ?? null,
62
+ ]);
42
63
  })
43
- .then((result) => {
44
- if (!abort.signal.aborted) setAccounts(structuredClone(result));
64
+ .then(([result, metadata]) => {
65
+ if (!abort.signal.aborted) {
66
+ setAccounts(structuredClone(result.filter((account) => account.status !== "disabled")));
67
+ if (client && metadata) setCatalog({ client, items: metadata.items });
68
+ }
45
69
  })
46
70
  .catch(() => {
47
71
  if (!abort.signal.aborted) setError(true);
48
72
  });
49
73
  return () => abort.abort();
50
- }, [controller, reload]);
74
+ }, [client, controller, reload]);
51
75
 
52
76
  const disconnect = async (account: ConnectAccount) => {
53
77
  const expectedVersion = account.version;
@@ -68,7 +92,10 @@ function ScopedAccounts({ controller, className, returnUrl }: ConnectAccountsPro
68
92
  expectedVersion,
69
93
  signal: abort.signal,
70
94
  });
71
- if (!abort.signal.aborted) setReload((value) => value + 1);
95
+ if (!abort.signal.aborted) {
96
+ setReload((value) => value + 1);
97
+ onDisconnected?.();
98
+ }
72
99
  } catch {
73
100
  if (!abort.signal.aborted) {
74
101
  // Outcome may be unknown. Drop the inventory so a second click cannot
@@ -106,64 +133,99 @@ function ScopedAccounts({ controller, className, returnUrl }: ConnectAccountsPro
106
133
  aria-label="Connected accounts"
107
134
  aria-busy={busy || view.busy || (!accounts && !error)}
108
135
  >
136
+ <h3>Your connections</h3>
109
137
  {error && (
110
138
  <p role="alert">Account state could not be confirmed. Reload before trying again.</p>
111
139
  )}
112
- {!accounts && !error && <p role="status">Loading accounts…</p>}
140
+ {!accounts && !error && <ConnectionSkeleton label="Loading accounts" />}
113
141
  {accounts?.length === 0 && <p role="status">No connected accounts.</p>}
114
142
  {accounts && accounts.length > 0 && (
115
143
  <ul>
116
144
  {accounts.map((account) => (
117
- <li key={`${account.providerId}:${account.id}`}>
118
- <span>
119
- {account.label} — {account.providerId} — {account.ownership} —{" "}
120
- {account.status.replaceAll("_", " ")}
121
- </span>
122
- {returnUrl && (
123
- <button
124
- type="button"
125
- disabled={busy || view.busy || account.status === "disabled"}
126
- onClick={() => void reconnect(account)}
127
- >
128
- Reconnect {account.label}
129
- </button>
130
- )}
131
- {selected === `${account.providerId}:${account.id}` ? (
132
- <div role="group" aria-label={`Disconnect ${account.label}`}>
133
- <p>
134
- This removes local OpenGeni access. It does not revoke consent at the provider.
135
- </p>
136
- <button
137
- type="button"
138
- disabled={busy || view.busy}
139
- onClick={() => void disconnect(account)}
145
+ <li className="og-connect-account" key={`${account.providerId}:${account.id}`}>
146
+ <details className="og-connect-account-details">
147
+ <summary className="og-connect-account-heading">
148
+ <ConnectAccountIdentity
149
+ account={account}
150
+ client={client}
151
+ capabilities={catalog?.client === client ? (catalog?.items ?? []) : []}
152
+ />
153
+ <span className="og-connect-account-status" data-status={account.status}>
154
+ {account.status === "connected"
155
+ ? "✓ Connected"
156
+ : account.status === "auth_needed"
157
+ ? "Reconnect needed"
158
+ : "Disconnected"}
159
+ </span>
160
+ <svg
161
+ className="og-connect-account-chevron"
162
+ aria-hidden="true"
163
+ width="16"
164
+ height="16"
165
+ viewBox="0 0 24 24"
166
+ fill="none"
167
+ stroke="currentColor"
168
+ strokeWidth="1.5"
140
169
  >
141
- Confirm disconnect
142
- </button>
143
- <button type="button" disabled={busy} onClick={() => setSelected(null)}>
144
- Keep account
145
- </button>
170
+ <path d="m9 6 6 6-6 6" />
171
+ </svg>
172
+ </summary>
173
+ <div className="og-connect-account-actions">
174
+ {returnUrl && account.status === "auth_needed" && (
175
+ <button
176
+ type="button"
177
+ aria-label={`Reconnect ${account.label}`}
178
+ disabled={busy || view.busy}
179
+ onClick={() => void reconnect(account)}
180
+ >
181
+ Reconnect
182
+ </button>
183
+ )}
184
+ {selected === `${account.providerId}:${account.id}` ? (
185
+ <div role="group" aria-label={`Disconnect ${account.label}`}>
186
+ <p>
187
+ This removes local OpenGeni access. It does not revoke consent at the
188
+ provider.
189
+ </p>
190
+ <button
191
+ type="button"
192
+ disabled={busy || view.busy}
193
+ onClick={() => void disconnect(account)}
194
+ >
195
+ Confirm disconnect
196
+ </button>
197
+ <button type="button" disabled={busy} onClick={() => setSelected(null)}>
198
+ Keep account
199
+ </button>
200
+ </div>
201
+ ) : (
202
+ <button
203
+ type="button"
204
+ aria-label={`Disconnect ${account.label}`}
205
+ disabled={
206
+ busy ||
207
+ view.busy ||
208
+ account.status === "disabled" ||
209
+ !Number.isSafeInteger(account.version) ||
210
+ account.version! < 1
211
+ }
212
+ onClick={() => setSelected(`${account.providerId}:${account.id}`)}
213
+ >
214
+ Disconnect
215
+ </button>
216
+ )}
146
217
  </div>
147
- ) : (
148
- <button
149
- type="button"
150
- disabled={
151
- busy ||
152
- view.busy ||
153
- account.status === "disabled" ||
154
- !Number.isSafeInteger(account.version) ||
155
- account.version! < 1
156
- }
157
- onClick={() => setSelected(`${account.providerId}:${account.id}`)}
158
- >
159
- Disconnect {account.label}
160
- </button>
161
- )}
218
+ </details>
162
219
  </li>
163
220
  ))}
164
221
  </ul>
165
222
  )}
166
- <button type="button" disabled={busy} onClick={() => setReload((value) => value + 1)}>
223
+ <button
224
+ hidden={!error}
225
+ type="button"
226
+ disabled={busy}
227
+ onClick={() => setReload((value) => value + 1)}
228
+ >
167
229
  Reload accounts
168
230
  </button>
169
231
  </section>