@opengeni/react 6.1.0 → 7.0.0-canary.35585322809001

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 (206) hide show
  1. package/README.md +83 -2
  2. package/dist/artifacts-document.js +1 -1
  3. package/dist/artifacts-presentation.js +1 -1
  4. package/dist/artifacts-spreadsheet.js +1 -1
  5. package/dist/artifacts.js +3 -3
  6. package/dist/{browser-viewer-NTD6UIPR.js → browser-viewer-LGH4IP47.js} +3 -3
  7. package/dist/capability-catalog-row.d.ts +20 -0
  8. package/dist/capability-logo-fallback.d.ts +3 -0
  9. package/dist/{chunk-NOSYBS2D.js → chunk-2VHARTBL.js} +2 -1
  10. package/dist/{chunk-VRBL3F5E.js → chunk-5GAL23FF.js} +10 -2
  11. package/dist/{chunk-VRBL3F5E.js.map → chunk-5GAL23FF.js.map} +1 -1
  12. package/dist/chunk-5UONOQ3Y.js +635 -0
  13. package/dist/chunk-5UONOQ3Y.js.map +1 -0
  14. package/dist/{chunk-NYESIADM.js → chunk-AKOMC3VO.js} +3 -3
  15. package/dist/chunk-AKOMC3VO.js.map +1 -0
  16. package/dist/chunk-BRM6FVZA.js +302 -0
  17. package/dist/chunk-BRM6FVZA.js.map +1 -0
  18. package/dist/{chunk-TDSEILUH.js → chunk-D5NOQPLR.js} +1289 -1530
  19. package/dist/chunk-D5NOQPLR.js.map +1 -0
  20. package/dist/chunk-D7GAPJ3V.js +1558 -0
  21. package/dist/chunk-D7GAPJ3V.js.map +1 -0
  22. package/dist/{chunk-5M6VTFJ5.js → chunk-GBPCZH3Y.js} +2 -2
  23. package/dist/{chunk-YBM6CQRU.js → chunk-GEN54DU5.js} +4 -4
  24. package/dist/{chunk-UMECRJRV.js → chunk-HJ5VJ4JB.js} +21 -12
  25. package/dist/chunk-HJ5VJ4JB.js.map +1 -0
  26. package/dist/{chunk-R4KRSFGA.js → chunk-IP4BUBKH.js} +3 -3
  27. package/dist/{chunk-KANFU34B.js → chunk-IYUPN32E.js} +6 -2
  28. package/dist/chunk-IYUPN32E.js.map +1 -0
  29. package/dist/{chunk-L2NBTKRX.js → chunk-JKLGJQO5.js} +193 -12
  30. package/dist/chunk-JKLGJQO5.js.map +1 -0
  31. package/dist/{chunk-NLTS7JHI.js → chunk-JPHDWKZE.js} +5 -5
  32. package/dist/{chunk-ICA6R6TW.js → chunk-OWHPKAOV.js} +28 -20
  33. package/dist/chunk-OWHPKAOV.js.map +1 -0
  34. package/dist/{chunk-24M4YBCL.js → chunk-PNAVEK2U.js} +4 -4
  35. package/dist/{chunk-45XEUVTK.js → chunk-UXE5SSNN.js} +293 -463
  36. package/dist/chunk-UXE5SSNN.js.map +1 -0
  37. package/dist/{chunk-PEI6RANI.js → chunk-VR4Z73EU.js} +203 -9
  38. package/dist/chunk-VR4Z73EU.js.map +1 -0
  39. package/dist/{chunk-HUIKIDM5.js → chunk-XMXRI5JK.js} +65 -18
  40. package/dist/chunk-XMXRI5JK.js.map +1 -0
  41. package/dist/client.d.ts +1 -1
  42. package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
  43. package/dist/components/chat-composer.d.ts +13 -3
  44. package/dist/components/markdown.d.ts +4 -3
  45. package/dist/components/message-timeline.d.ts +7 -2
  46. package/dist/components/preview-loading.d.ts +2 -0
  47. package/dist/components/session-capability-frame.d.ts +24 -0
  48. package/dist/components/session-chrome.d.ts +1 -2
  49. package/dist/components/session-commands.d.ts +6 -0
  50. package/dist/components/session-connection-request.d.ts +8 -0
  51. package/dist/components/session-mcp-capability-card.d.ts +22 -0
  52. package/dist/components/timeline-search.d.ts +34 -0
  53. package/dist/composer.js +7 -7
  54. package/dist/connect-account-identity.d.ts +8 -0
  55. package/dist/connect-accounts.d.ts +5 -0
  56. package/dist/connect-chooser.d.ts +6 -0
  57. package/dist/connect-panel.d.ts +9 -2
  58. package/dist/connect-setup.d.ts +5 -1
  59. package/dist/connect.d.ts +5 -21
  60. package/dist/connect.js +1268 -584
  61. package/dist/connect.js.map +1 -1
  62. package/dist/connection-catalog.d.ts +6 -1
  63. package/dist/connection-discovery.d.ts +12 -0
  64. package/dist/connection-installed.d.ts +1 -0
  65. package/dist/connection-logo.d.ts +5 -2
  66. package/dist/connection-service-logo.d.ts +8 -0
  67. package/dist/connection-service-presentation.d.ts +13 -0
  68. package/dist/connection-skeleton.d.ts +5 -0
  69. package/dist/discovery-cache.d.ts +5 -0
  70. package/dist/hooks/use-codex-accounts.d.ts +11 -5
  71. package/dist/hooks/use-connect.d.ts +22 -0
  72. package/dist/hooks/use-session-events.d.ts +5 -0
  73. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  74. package/dist/hooks/use-workspace-sessions.d.ts +2 -0
  75. package/dist/index.d.ts +8 -2
  76. package/dist/index.js +771 -793
  77. package/dist/index.js.map +1 -1
  78. package/dist/interaction.js +2 -2
  79. package/dist/machines.js +2 -2
  80. package/dist/mcp-connection-status.d.ts +14 -0
  81. package/dist/{model-policy-picker-menu-3R336DP2.js → model-policy-picker-menu-RVOQ4ZZG.js} +4 -4
  82. package/dist/model-policy.d.ts +6 -2
  83. package/dist/model-policy.js +1 -1
  84. package/dist/{platform-activity-row-DUE2W74J.js → platform-activity-row-HLOH7FW4.js} +9 -4
  85. package/dist/platform-activity-row-HLOH7FW4.js.map +1 -0
  86. package/dist/plugin-discovery.d.ts +6 -0
  87. package/dist/{queue-surface-implementation-LK3S4BIW.js → queue-surface-implementation-3RIFK2BS.js} +4 -4
  88. package/dist/realtime.js +22 -22
  89. package/dist/realtime.js.map +1 -1
  90. package/dist/session-auth-recommendation.d.ts +5 -0
  91. package/dist/session-capability-policy.d.ts +12 -0
  92. package/dist/session-ui.d.ts +2 -0
  93. package/dist/session-ui.js +14 -12
  94. package/dist/session.d.ts +1 -0
  95. package/dist/session.js +6 -4
  96. package/dist/skill-discovery.d.ts +11 -0
  97. package/dist/timeline/compaction-copy.d.ts +10 -0
  98. package/dist/timeline/projection.d.ts +3 -1
  99. package/dist/timeline/turn-summary.d.ts +2 -2
  100. package/dist/timeline/types.d.ts +22 -1
  101. package/dist/{timeline-annotation-markers-DRSTLEM6.js → timeline-annotation-markers-OY6OWR3V.js} +2 -2
  102. package/dist/{timeline-annotation-selection-IHLUGVND.js → timeline-annotation-selection-BOXA2TIN.js} +2 -2
  103. package/dist/{timeline-annotations-dialog-6Q53N5FG.js → timeline-annotations-dialog-RGY3MHL3.js} +3 -3
  104. package/package.json +3 -3
  105. package/src/assets/README.md +8 -0
  106. package/src/assets/gmail.ico +0 -0
  107. package/src/assets/outlook.ico +0 -0
  108. package/src/capability-catalog-row.tsx +94 -0
  109. package/src/capability-logo-fallback.ts +22 -0
  110. package/src/client.ts +0 -9
  111. package/src/components/artifacts/document-editor.tsx +63 -6
  112. package/src/components/artifacts/presentation-editor.tsx +214 -9
  113. package/src/components/artifacts/spreadsheet-canvas.ts +3 -8
  114. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  115. package/src/components/artifacts/spreadsheet-grid.tsx +10 -11
  116. package/src/components/chat-composer.tsx +26 -3
  117. package/src/components/composer.tsx +10 -4
  118. package/src/components/human-input-form.tsx +12 -2
  119. package/src/components/markdown.tsx +86 -4
  120. package/src/components/message-timeline.tsx +104 -36
  121. package/src/components/model-picker.tsx +11 -3
  122. package/src/components/model-policy-picker-menu.tsx +10 -3
  123. package/src/components/preview-loading.tsx +154 -0
  124. package/src/components/sandbox-workspace.tsx +20 -36
  125. package/src/components/session-capability-frame.tsx +163 -0
  126. package/src/components/session-chrome.tsx +46 -77
  127. package/src/components/session-commands.tsx +18 -0
  128. package/src/components/session-connection-request.tsx +97 -0
  129. package/src/components/session-conversation.tsx +5 -4
  130. package/src/components/session-mcp-capability-card.tsx +532 -0
  131. package/src/components/session-status.tsx +2 -2
  132. package/src/components/timeline-anchor.tsx +9 -8
  133. package/src/components/timeline-search.tsx +168 -0
  134. package/src/components/user-message-body.tsx +7 -0
  135. package/src/connect-account-identity.tsx +34 -0
  136. package/src/connect-accounts.tsx +114 -52
  137. package/src/connect-chooser.tsx +171 -39
  138. package/src/connect-panel.tsx +73 -9
  139. package/src/connect-setup.tsx +72 -25
  140. package/src/connect.ts +13 -22
  141. package/src/connection-assets.d.ts +4 -0
  142. package/src/connection-catalog.tsx +61 -39
  143. package/src/connection-discovery.tsx +235 -0
  144. package/src/connection-installed.tsx +2 -0
  145. package/src/connection-logo.tsx +18 -10
  146. package/src/connection-service-logo.tsx +55 -0
  147. package/src/connection-service-presentation.ts +46 -0
  148. package/src/connection-skeleton.tsx +16 -0
  149. package/src/conversation-timeline.ts +1 -0
  150. package/src/discovery-cache.ts +42 -0
  151. package/src/hooks/use-codex-accounts.ts +33 -14
  152. package/src/hooks/use-composer.ts +4 -5
  153. package/src/hooks/use-connect.ts +23 -0
  154. package/src/hooks/use-session-events.ts +114 -1
  155. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  156. package/src/hooks/use-workspace-sessions.ts +10 -0
  157. package/src/index.ts +22 -2
  158. package/src/mcp-connection-status.ts +35 -0
  159. package/src/model-policy.ts +21 -0
  160. package/src/plugin-details.tsx +8 -5
  161. package/src/plugin-discovery.tsx +92 -72
  162. package/src/realtime/realtime-control.tsx +6 -8
  163. package/src/session-auth-recommendation.ts +48 -0
  164. package/src/session-capability-policy.ts +83 -0
  165. package/src/session-ui.ts +2 -0
  166. package/src/session.ts +1 -0
  167. package/src/skill-discovery.tsx +101 -56
  168. package/src/timeline/compaction-copy.ts +40 -0
  169. package/src/timeline/platform-activity-row.tsx +8 -3
  170. package/src/timeline/projection.ts +74 -2
  171. package/src/timeline/shared.tsx +6 -1
  172. package/src/timeline/startup-timings.tsx +1 -1
  173. package/src/timeline/turn-summary.tsx +17 -3
  174. package/src/timeline/types.ts +22 -2
  175. package/styles/compiled.css +325 -27
  176. package/styles/connect.css +90 -15
  177. package/styles/index.css +46 -0
  178. package/styles/session-capability.css +42 -0
  179. package/dist/chunk-45XEUVTK.js.map +0 -1
  180. package/dist/chunk-HQV2I5HV.js +0 -124
  181. package/dist/chunk-HQV2I5HV.js.map +0 -1
  182. package/dist/chunk-HUIKIDM5.js.map +0 -1
  183. package/dist/chunk-ICA6R6TW.js.map +0 -1
  184. package/dist/chunk-IHQII5WO.js +0 -886
  185. package/dist/chunk-IHQII5WO.js.map +0 -1
  186. package/dist/chunk-KANFU34B.js.map +0 -1
  187. package/dist/chunk-L2NBTKRX.js.map +0 -1
  188. package/dist/chunk-NYESIADM.js.map +0 -1
  189. package/dist/chunk-PEI6RANI.js.map +0 -1
  190. package/dist/chunk-TDSEILUH.js.map +0 -1
  191. package/dist/chunk-UMECRJRV.js.map +0 -1
  192. package/dist/hooks/use-packs.d.ts +0 -32
  193. package/dist/platform-activity-row-DUE2W74J.js.map +0 -1
  194. package/src/hooks/use-packs.ts +0 -157
  195. /package/dist/{browser-viewer-NTD6UIPR.js.map → browser-viewer-LGH4IP47.js.map} +0 -0
  196. /package/dist/{chunk-NOSYBS2D.js.map → chunk-2VHARTBL.js.map} +0 -0
  197. /package/dist/{chunk-5M6VTFJ5.js.map → chunk-GBPCZH3Y.js.map} +0 -0
  198. /package/dist/{chunk-YBM6CQRU.js.map → chunk-GEN54DU5.js.map} +0 -0
  199. /package/dist/{chunk-R4KRSFGA.js.map → chunk-IP4BUBKH.js.map} +0 -0
  200. /package/dist/{chunk-NLTS7JHI.js.map → chunk-JPHDWKZE.js.map} +0 -0
  201. /package/dist/{chunk-24M4YBCL.js.map → chunk-PNAVEK2U.js.map} +0 -0
  202. /package/dist/{model-policy-picker-menu-3R336DP2.js.map → model-policy-picker-menu-RVOQ4ZZG.js.map} +0 -0
  203. /package/dist/{queue-surface-implementation-LK3S4BIW.js.map → queue-surface-implementation-3RIFK2BS.js.map} +0 -0
  204. /package/dist/{timeline-annotation-markers-DRSTLEM6.js.map → timeline-annotation-markers-OY6OWR3V.js.map} +0 -0
  205. /package/dist/{timeline-annotation-selection-IHLUGVND.js.map → timeline-annotation-selection-BOXA2TIN.js.map} +0 -0
  206. /package/dist/{timeline-annotations-dialog-6Q53N5FG.js.map → timeline-annotations-dialog-RGY3MHL3.js.map} +0 -0
@@ -1,5 +1,13 @@
1
+ import { createDiscoveryCache } from "./discovery-cache";
1
2
  import { useEffect, useRef, useState } from "react";
2
3
  import type { PluginDiscoveryItem, PluginDiscoveryPage } from "@opengeni/sdk";
4
+ import { BoxesIcon } from "lucide-react";
5
+ import { CapabilityCatalogRow } from "./capability-catalog-row";
6
+ import { ConnectionLogo } from "./connection-logo";
7
+
8
+ const discoveryCache = createDiscoveryCache<PluginDiscoveryPage>();
9
+ const EMPTY_INSTALLED_IDS: ReadonlySet<string> = new Set();
10
+
3
11
  export type PluginDiscoveryProps = {
4
12
  client: {
5
13
  discoverPlugins(
@@ -9,30 +17,43 @@ export type PluginDiscoveryProps = {
9
17
  };
10
18
  workspaceId: string;
11
19
  query: string;
20
+ /** Initially selected registry. Users can still switch registries or browse all. */
21
+ defaultProvider?: "" | "openai" | "anthropic";
22
+ /** Discovery identities already installed in the current workspace. */
23
+ installedIds?: ReadonlySet<string>;
24
+ resultLimit?: number;
25
+ onShowMore?: () => void;
12
26
  onOpen: (item: PluginDiscoveryItem) => void;
13
27
  };
14
28
  export function PluginDiscovery(props: PluginDiscoveryProps) {
15
- const [provider, setProvider] = useState("");
29
+ const [provider, setProvider] = useState(props.defaultProvider ?? "");
16
30
  return (
17
- <section className="og-plugin-discovery" aria-label="Discover plugins">
31
+ <section
32
+ className={`og-plugin-discovery ${props.resultLimit ? "og-catalog-overview" : ""}`}
33
+ aria-label="Discover plugins"
34
+ >
18
35
  <header>
19
- <h3>Browse plugins</h3>
20
- <div className="og-plugin-filters" role="group" aria-label="Plugin registry">
21
- {[
22
- { value: "", label: "All" },
23
- { value: "openai", label: "OpenAI plugin registry" },
24
- { value: "anthropic", label: "Anthropic plugin registry" },
25
- ].map((option) => (
26
- <button
27
- key={option.value}
28
- type="button"
29
- aria-pressed={provider === option.value}
30
- onClick={() => setProvider(option.value)}
31
- >
32
- {option.label}
33
- </button>
34
- ))}
35
- </div>
36
+ <h3>{props.resultLimit ? "Plugins" : "Browse plugins"}</h3>
37
+ {!props.resultLimit ? (
38
+ <div className="og-plugin-filters" role="group" aria-label="Plugin registry">
39
+ {(
40
+ [
41
+ { value: "", label: "All" },
42
+ { value: "openai", label: "OpenAI plugin registry" },
43
+ { value: "anthropic", label: "Anthropic plugin registry" },
44
+ ] as const
45
+ ).map((option) => (
46
+ <button
47
+ key={option.value}
48
+ type="button"
49
+ aria-pressed={provider === option.value}
50
+ onClick={() => setProvider(option.value)}
51
+ >
52
+ {option.label}
53
+ </button>
54
+ ))}
55
+ </div>
56
+ ) : null}
36
57
  </header>
37
58
  <Results
38
59
  key={`${props.workspaceId}:${props.query}:${provider}`}
@@ -47,39 +68,49 @@ function Results({
47
68
  workspaceId,
48
69
  query,
49
70
  provider,
71
+ installedIds = EMPTY_INSTALLED_IDS,
50
72
  onOpen,
73
+ resultLimit,
74
+ onShowMore,
51
75
  }: PluginDiscoveryProps & { provider: string }) {
52
- const [items, setItems] = useState<PluginDiscoveryItem[]>([]);
76
+ const initial = discoveryCache.peek(client, JSON.stringify([workspaceId, query, provider, 0]));
77
+ const [items, setItems] = useState<PluginDiscoveryItem[]>(initial?.items ?? []);
53
78
  const [offset, setOffset] = useState(0);
54
- const [next, setNext] = useState<number | null>(null);
55
- const [total, setTotal] = useState(0);
56
- const [loading, setLoading] = useState(true);
79
+ const [next, setNext] = useState<number | null>(initial?.nextOffset ?? null);
80
+ const [total, setTotal] = useState(initial?.total ?? 0);
81
+ const [loading, setLoading] = useState(!initial);
57
82
  const [error, setError] = useState(false);
58
83
  const [retry, setRetry] = useState(0);
59
84
  const sentinel = useRef<HTMLDivElement>(null);
60
85
  useEffect(() => {
61
86
  let active = true;
62
- setLoading(true);
87
+ const cacheKey = JSON.stringify([workspaceId, query, provider, offset]);
88
+ const cached = discoveryCache.peek(client, cacheKey);
89
+ setLoading(!cached);
63
90
  setError(false);
64
91
  const timer = setTimeout(
65
92
  () => {
66
- void client.discoverPlugins(workspaceId, { query, provider, offset }).then(
67
- (page) => {
68
- if (!active) return;
69
- setItems((previous) => (offset ? [...previous, ...page.items] : page.items));
70
- setTotal(page.total);
71
- setNext(page.nextOffset);
72
- setLoading(false);
73
- },
74
- () => {
75
- if (active) {
76
- setError(true);
93
+ void discoveryCache
94
+ .read(client, cacheKey, () =>
95
+ client.discoverPlugins(workspaceId, { query, provider, offset }),
96
+ )
97
+ .then(
98
+ (page) => {
99
+ if (!active) return;
100
+ setItems((previous) => (offset ? [...previous, ...page.items] : page.items));
101
+ setTotal(page.total);
102
+ setNext(page.nextOffset);
77
103
  setLoading(false);
78
- }
79
- },
80
- );
104
+ },
105
+ () => {
106
+ if (active) {
107
+ setError(true);
108
+ setLoading(false);
109
+ }
110
+ },
111
+ );
81
112
  },
82
- offset ? 0 : 200,
113
+ cached || offset ? 0 : 200,
83
114
  );
84
115
  return () => {
85
116
  active = false;
@@ -87,7 +118,7 @@ function Results({
87
118
  };
88
119
  }, [client, workspaceId, query, provider, offset, retry]);
89
120
  useEffect(() => {
90
- if (loading || error || next === null || !sentinel.current) return;
121
+ if (resultLimit || loading || error || next === null || !sentinel.current) return;
91
122
  const observer = new IntersectionObserver(
92
123
  (entries) => {
93
124
  if (entries.some((entry) => entry.isIntersecting)) setOffset(next);
@@ -96,7 +127,7 @@ function Results({
96
127
  );
97
128
  observer.observe(sentinel.current);
98
129
  return () => observer.disconnect();
99
- }, [loading, error, next]);
130
+ }, [loading, error, next, resultLimit]);
100
131
  return (
101
132
  <>
102
133
  {!loading && !error ? (
@@ -105,39 +136,23 @@ function Results({
105
136
  </p>
106
137
  ) : null}
107
138
  <div className="og-plugin-discovery-grid">
108
- {items.map((item) => (
109
- <button
139
+ {items.slice(0, resultLimit).map((item) => (
140
+ <CapabilityCatalogRow
110
141
  key={item.id}
111
- className="og-plugin-discovery-row"
112
- type="button"
113
- onClick={() => onOpen(item)}
114
- >
115
- {item.logoUrl ? (
116
- <img
142
+ data-plugin-id={item.id}
143
+ name={item.displayName.replace(/-/g, " ")}
144
+ description={item.description}
145
+ status={installedIds.has(item.id) ? "added" : "available"}
146
+ statusLabel={installedIds.has(item.id) ? "Installed" : "Available to install"}
147
+ onOpen={() => onOpen(item)}
148
+ icon={
149
+ <ConnectionLogo
117
150
  src={item.logoUrl}
118
- alt=""
119
- loading="lazy"
120
- onError={(event) => {
121
- event.currentTarget.style.display = "none";
122
- }}
151
+ name={item.displayName}
152
+ fallback={<BoxesIcon aria-hidden="true" />}
123
153
  />
124
- ) : null}
125
- <span className="og-plugin-copy">
126
- <span className="og-plugin-title">
127
- <strong>{item.displayName.replace(/-/g, " ")}</strong>
128
- <small>
129
- {item.provider === "openai"
130
- ? "OpenAI plugin registry"
131
- : "Anthropic plugin registry"}
132
- </small>
133
- </span>
134
- <span className="og-plugin-description">{item.description}</span>
135
- {item.category ? <span className="og-plugin-category">{item.category}</span> : null}
136
- </span>
137
- <span className="og-plugin-open" aria-hidden="true">
138
- ›
139
- </span>
140
- </button>
154
+ }
155
+ />
141
156
  ))}
142
157
  </div>
143
158
  {loading ? <p role="status">Loading plugins…</p> : null}
@@ -149,8 +164,13 @@ function Results({
149
164
  </button>
150
165
  </p>
151
166
  ) : null}
167
+ {resultLimit && !loading && !error && total > resultLimit ? (
168
+ <button className="og-catalog-more" type="button" onClick={onShowMore}>
169
+ View all plugins
170
+ </button>
171
+ ) : null}
152
172
  <div ref={sentinel}>
153
- {next !== null && !loading && !error ? (
173
+ {!resultLimit && next !== null && !loading && !error ? (
154
174
  <button className="og-plugin-more" type="button" onClick={() => setOffset(next)}>
155
175
  Show more plugins
156
176
  </button>
@@ -1105,16 +1105,14 @@ export function RealtimeVoiceModelPanel(props: {
1105
1105
 
1106
1106
  return (
1107
1107
  <div className="flex min-h-0 flex-1 flex-col" data-testid="realtime-voice-model-panel">
1108
- <div className="flex shrink-0 items-start gap-1 px-2 pt-1 pb-1.5">
1108
+ <div className="flex min-h-10 shrink-0 items-center gap-2 border-b border-og-border px-2 pb-2">
1109
1109
  {props.leading}
1110
- <div className="min-w-0">
1111
- <div className="text-og-menu font-medium text-og-fg">Voice model</div>
1112
- <p className="mt-0.5 text-og-control text-og-fg-subtle">
1113
- Used when you start a voice conversation.
1114
- </p>
1115
- </div>
1110
+ <div className="min-w-0 flex-1 text-og-menu font-medium text-og-fg">Voice model</div>
1116
1111
  </div>
1117
- <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-1.5 pb-1.5">
1112
+ <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-2">
1113
+ <p className="mb-3 text-og-control text-og-fg-muted">
1114
+ Used when you start a voice conversation.
1115
+ </p>
1118
1116
  <RealtimeModelPickerMenu
1119
1117
  models={props.models}
1120
1118
  selectedModel={props.selectedModel}
@@ -0,0 +1,48 @@
1
+ import type { AuthNeededItem } from "./timeline/types";
2
+ import type { CapabilityCatalogItem } from "@opengeni/sdk";
3
+
4
+ /** Recovery events identify runtime servers/connections, while recommendation
5
+ * events already carry a catalog id. Never guess among same-domain accounts. */
6
+ export function sessionAuthRecommendation(
7
+ item: AuthNeededItem,
8
+ catalog: CapabilityCatalogItem[],
9
+ ): AuthNeededItem | undefined {
10
+ if (
11
+ item.authoritySource === "host" ||
12
+ item.reason === "unsupported_auth" ||
13
+ item.reason === "resource_scope_unavailable"
14
+ )
15
+ return undefined;
16
+ if (item.capability) return item;
17
+ const recoveryServerId = item.canonicalServerId ?? item.serverId;
18
+ const matches = catalog.filter(
19
+ (candidate) =>
20
+ (recoveryServerId !== null &&
21
+ candidate.runtime.mcpServerId === recoveryServerId &&
22
+ recoveryServerId !== "opengeni") ||
23
+ (item.connectionId !== null && candidate.connectionRef?.connectionId === item.connectionId),
24
+ );
25
+ if (matches.length !== 1) return undefined;
26
+ const entry = matches[0]!;
27
+ const personalAccess = item.reason === "personal_authority_unavailable";
28
+ if (
29
+ personalAccess &&
30
+ ((item.connectionSubjectScope ?? entry.connectionRef?.subjectScope) !== "subject" ||
31
+ entry.connectionRef?.authoritySource === "host")
32
+ )
33
+ return undefined;
34
+ return {
35
+ ...item,
36
+ capability: {
37
+ id: entry.id,
38
+ name: entry.name,
39
+ kind: entry.kind,
40
+ source: entry.source,
41
+ action: "connect",
42
+ rationale: personalAccess
43
+ ? "Review permission to use your personal account in this conversation."
44
+ : "Reconnect this integration to use its tools in this conversation.",
45
+ requiredVariables: [],
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,83 @@
1
+ import type { FirstPartyMcpToolName, CapabilityCatalogItem } from "@opengeni/sdk";
2
+ import type { OpenGeniBrowserClient as OpenGeniClient } from "@opengeni/sdk/browser";
3
+
4
+ /** Catalog tool discovery may be lazy; selecting the installed server does not
5
+ * require its individual tool inventory to have been fetched. */
6
+ export function sessionCapabilityTools(item: CapabilityCatalogItem) {
7
+ const serverId = item.kind === "mcp" ? item.runtime?.mcpServerId : undefined;
8
+ return serverId && !item.tools.some((tool) => tool.kind === "mcp" && tool.id === serverId)
9
+ ? [...item.tools, { kind: "mcp" as const, id: serverId }]
10
+ : item.tools;
11
+ }
12
+
13
+ /** A connection never resets the human's existing tool selection. Default-mode
14
+ * sessions discover newly enabled servers at admission; explicit/inherited
15
+ * policies add only this reviewed capability, with the existing CAS fence. */
16
+ export async function attachSessionCapability(
17
+ client: OpenGeniClient,
18
+ workspaceId: string,
19
+ sessionId: string,
20
+ item: CapabilityCatalogItem,
21
+ stillCurrent: () => boolean = () => true,
22
+ ): Promise<void> {
23
+ const tools = sessionCapabilityTools(item);
24
+ if (item.kind === "skill" || tools.length === 0) return;
25
+ if (!stillCurrent()) throw new Error("Connection setup was interrupted.");
26
+ const session = await client.getSession(workspaceId, sessionId);
27
+ if (!stillCurrent()) throw new Error("Connection setup was interrupted.");
28
+ // Session.tools includes mandatory runtime servers, which must never be
29
+ // echoed into the human's explicit selection.
30
+ let existing = session.tools.filter(
31
+ (tool) => !session.effectiveToolPolicy?.mandatoryIds?.includes(tool.id),
32
+ );
33
+ if (session.toolPolicy.mode === "workspace_default") {
34
+ const policy = session.effectiveToolPolicy;
35
+ if (!policy || policy.idsTruncated)
36
+ throw new Error(
37
+ "Connected, but the full session tool selection could not be read. Review the Tools picker before continuing.",
38
+ );
39
+ // A workspace may choose a restricted default instead of all enabled servers.
40
+ // Preserve that exact selection if this connection needs an explicit addition.
41
+ // selectedIds contains only explicit additions, and is normally empty for
42
+ // automatic selection. Compare against the resolved defaults instead.
43
+ existing = policy.effectiveIds
44
+ .filter((id) => !policy.mandatoryIds.includes(id))
45
+ .map((id) => ({ kind: "mcp" as const, id }));
46
+ }
47
+ const additions = tools.filter(
48
+ (tool) => !existing.some((current) => current.kind === tool.kind && current.id === tool.id),
49
+ );
50
+ const recommended = Array.isArray(item.metadata?.firstPartyMcpTools)
51
+ ? (item.metadata.firstPartyMcpTools.filter(
52
+ (name): name is string => typeof name === "string",
53
+ ) as FirstPartyMcpToolName[])
54
+ : [];
55
+ const firstParty = [...new Set([...session.firstPartyMcpTools, ...recommended])];
56
+ if (additions.length === 0 && firstParty.length === session.firstPartyMcpTools.length) return;
57
+ await client.updateSessionToolPolicy(workspaceId, sessionId, {
58
+ mode: "explicit",
59
+ tools: [...existing, ...additions],
60
+ firstPartyMcpTools: firstParty,
61
+ expectedVersion: session.toolPolicyVersion,
62
+ });
63
+ }
64
+
65
+ /** Native OAuth callbacks have already saved the provider connection. Finish
66
+ * the separate, version-fenced session tool selection before reporting success. */
67
+ export async function completeSessionCapabilityOAuth(
68
+ client: OpenGeniClient,
69
+ workspaceId: string,
70
+ sessionId: string,
71
+ capabilityId: string | null,
72
+ ): Promise<void> {
73
+ if (!capabilityId)
74
+ throw new Error(
75
+ "The authorized integration could not be identified. Review its connection card to finish setup.",
76
+ );
77
+ const connected = (await client.listCapabilities(workspaceId)).items.find(
78
+ (item) => item.id === capabilityId,
79
+ );
80
+ if (!connected?.enabled)
81
+ throw new Error("The connection was authorized, but enabling its tools could not be verified.");
82
+ await attachSessionCapability(client, workspaceId, sessionId, connected);
83
+ }
package/src/session-ui.ts CHANGED
@@ -13,6 +13,7 @@ export type { ApprovalSurfaceProps, ApprovalSurfaceMessages } from "./components
13
13
  export type { HumanInputSurfaceProps } from "./components/human-input-surface";
14
14
  export { MessageTimeline, TimelineRow } from "./components/message-timeline";
15
15
  export type { MessageTimelineProps } from "./components/message-timeline";
16
+ export type { TimelineSearchTarget } from "./components/timeline-search";
16
17
  export { createOlderHistoryLoadReceipt } from "./older-history";
17
18
  export type { OlderHistoryLoader, OlderHistoryLoadReceipt } from "./older-history";
18
19
  export { UserMessageBody, userMessageLikelyNeedsDisclosure } from "./components/user-message-body";
@@ -46,6 +47,7 @@ export type {
46
47
  SessionChromeSignalTone,
47
48
  } from "./components/session-chrome";
48
49
  export { SessionCommandsPanel } from "./components/session-commands-panel";
50
+ export { SessionCommands } from "./components/session-commands";
49
51
 
50
52
  export { KnowledgeActivityProvider } from "./timeline/knowledge-receipt";
51
53
  export type { KnowledgeActivityActions } from "./timeline/knowledge-receipt";
package/src/session.ts CHANGED
@@ -23,6 +23,7 @@ export type {
23
23
  EmbeddedSessionClientOverride as ClientOverride,
24
24
  } from "./session-context";
25
25
  export { createEmbeddedSessionClient } from "./embedded-session-client";
26
+ export { conversationTimeline } from "./conversation-timeline";
26
27
  export type {
27
28
  ComposerDraftMapContext,
28
29
  CreateEmbeddedSessionClientOptions,
@@ -1,4 +1,7 @@
1
1
  import { useEffect, useState } from "react";
2
+ import { createDiscoveryCache } from "./discovery-cache";
3
+ import { BookOpenIcon } from "lucide-react";
4
+ import { CapabilityCatalogRow, type CapabilityCatalogStatus } from "./capability-catalog-row";
2
5
 
3
6
  export type SkillDiscoveryItem = {
4
7
  id: string;
@@ -8,6 +11,8 @@ export type SkillDiscoveryItem = {
8
11
  url: string;
9
12
  };
10
13
  export type SkillDiscoveryPage = { items: SkillDiscoveryItem[]; nextCursor: null };
14
+ const discoveryCache = createDiscoveryCache<SkillDiscoveryPage>();
15
+
11
16
  export type SkillDiscoveryClient = {
12
17
  searchPublicSkills(workspaceId: string, query: string): Promise<SkillDiscoveryPage>;
13
18
  };
@@ -18,35 +23,53 @@ export type SkillDiscoveryProps = {
18
23
  query: string;
19
24
  onImport: (url: string) => void;
20
25
  canManage: boolean;
26
+ localSkills?: readonly {
27
+ id: string;
28
+ name: string;
29
+ description?: string;
30
+ status?: CapabilityCatalogStatus;
31
+ statusLabel?: string;
32
+ onOpen: () => void;
33
+ }[];
34
+ resultLimit?: number;
35
+ onShowMore?: () => void;
21
36
  onSearch?: (() => void) | undefined;
22
37
  };
23
38
 
24
39
  /** Host-owned query and import flow; browse presentation is reusable independently. */
25
40
  export function SkillDiscovery(props: SkillDiscoveryProps) {
26
41
  const query = props.query.trim();
42
+ const discoveryQuery = query || "agent";
27
43
  return (
28
44
  <section className="og-skill-discovery" aria-label="Discover skills">
29
45
  <header>
30
- <h3>Discover skills</h3>
31
- <a
32
- href="https://skills.sh/"
33
- target="_blank"
34
- rel="noopener noreferrer"
35
- className="og-skill-discovery-browse"
36
- >
37
- Browse skills.sh ↗
38
- </a>
46
+ <h3>{props.resultLimit ? "Skills" : query ? "Search skills" : "Browse skills"}</h3>
47
+ {!props.resultLimit ? (
48
+ <a
49
+ href="https://skills.sh/"
50
+ target="_blank"
51
+ rel="noopener noreferrer"
52
+ className="og-skill-discovery-browse"
53
+ >
54
+ Browse skills.sh ↗
55
+ </a>
56
+ ) : null}
39
57
  </header>
40
- {!query && props.onSearch ? (
41
- <button type="button" className="og-skill-discovery-search" onClick={props.onSearch}>
42
- Search skills
43
- </button>
58
+ {!query ? (
59
+ <p>Agent skills from skills.sh. Search above for another topic.</p>
60
+ ) : query.length < 2 ? (
61
+ <p>Enter at least two characters to search skills.sh.</p>
44
62
  ) : null}
45
- <DiscoveryResults key={`${props.workspaceId}:${query}`} {...props} query={query} />
63
+ <DiscoveryResults
64
+ key={`${props.workspaceId}:${discoveryQuery}`}
65
+ {...props}
66
+ query={discoveryQuery}
67
+ />
46
68
  </section>
47
69
  );
48
70
  }
49
71
 
72
+ const EMPTY_LOCAL_SKILLS: NonNullable<SkillDiscoveryProps["localSkills"]> = [];
50
73
  const EMPTY_INSTALLED_SKILLS: NonNullable<SkillDiscoveryProps["installedSkills"]> = [];
51
74
 
52
75
  function DiscoveryResults({
@@ -55,21 +78,29 @@ function DiscoveryResults({
55
78
  query,
56
79
  canManage,
57
80
  onImport,
81
+ resultLimit,
82
+ onShowMore,
83
+ localSkills = EMPTY_LOCAL_SKILLS,
58
84
  installedSkills = EMPTY_INSTALLED_SKILLS,
59
85
  }: SkillDiscoveryProps) {
60
- const [result, setResult] = useState<SkillDiscoveryPage | null>(null);
61
- const [loading, setLoading] = useState(query.length >= 2);
86
+ const cacheKey = JSON.stringify([workspaceId, query]);
87
+ const cached = discoveryCache.peek(client, cacheKey);
88
+ const [result, setResult] = useState<SkillDiscoveryPage | null>(cached ?? null);
89
+ const [loading, setLoading] = useState(query.length >= 2 && !cached);
62
90
  const [error, setError] = useState(false);
63
91
  const [retry, setRetry] = useState(0);
64
92
  useEffect(() => {
65
93
  let active = true;
66
94
  setError(false);
67
95
  if (query.length < 2) return;
68
- setLoading(true);
96
+ const existing = discoveryCache.peek(client, cacheKey);
97
+ setLoading(!existing);
69
98
  let timer: ReturnType<typeof setTimeout>;
70
99
  const search = async (attempt: number) => {
71
100
  try {
72
- const data = await client.searchPublicSkills(workspaceId, query);
101
+ const data = await discoveryCache.read(client, cacheKey, () =>
102
+ client.searchPublicSkills(workspaceId, query),
103
+ );
73
104
  if (!active) return;
74
105
  setResult(data);
75
106
  setLoading(false);
@@ -90,51 +121,65 @@ function DiscoveryResults({
90
121
  setLoading(false);
91
122
  }
92
123
  };
93
- timer = setTimeout(() => void search(0), 300);
124
+ timer = setTimeout(() => void search(0), existing ? 0 : 300);
94
125
  return () => {
95
126
  active = false;
96
127
  clearTimeout(timer);
97
128
  };
98
- }, [client, workspaceId, query, retry]);
129
+ }, [client, workspaceId, query, cacheKey, retry]);
130
+ const remoteSkills =
131
+ result?.items.filter(
132
+ (skill) =>
133
+ !localSkills.some((local) => local.name.toLowerCase() === skill.name.toLowerCase()),
134
+ ) ?? [];
99
135
  return (
100
136
  <div aria-busy={loading}>
101
137
  <div className="og-skill-discovery-grid">
102
- {result?.items.map((skill) => {
103
- const installed = installedSkills.some(
104
- (entry) =>
105
- entry.sourceUrl.replace(/\/$/, "").toLowerCase() === skill.url.toLowerCase() ||
106
- (entry.repositoryUrl.replace(/\/$/, "").toLowerCase() ===
107
- `https://github.com/${skill.source}`.toLowerCase() &&
108
- entry.name.toLowerCase() === skill.name.toLowerCase()),
109
- );
110
- return (
111
- <button
112
- key={skill.id}
113
- type="button"
114
- className="og-skill-discovery-item"
115
- disabled={!canManage}
116
- onClick={() => onImport(skill.url)}
117
- >
118
- <span className="og-skill-discovery-copy">
119
- <strong>{skill.name}</strong>
120
- <span>{skill.source}</span>
121
- <small>{skill.installs.toLocaleString()} installs</small>
122
- </span>
123
- <span className="og-skill-discovery-action">
124
- {installed ? (
125
- <>
126
- <span aria-hidden="true">✓ </span>Installed
127
- </>
128
- ) : !canManage ? (
129
- "Admin required"
130
- ) : (
131
- "Preview →"
132
- )}
133
- </span>
134
- </button>
135
- );
136
- })}
138
+ {localSkills.slice(0, resultLimit).map((skill) => (
139
+ <CapabilityCatalogRow
140
+ key={skill.id}
141
+ name={skill.name}
142
+ description={skill.description}
143
+ icon={<BookOpenIcon aria-hidden="true" />}
144
+ status={skill.status ?? "added"}
145
+ statusLabel={skill.statusLabel}
146
+ onOpen={skill.onOpen}
147
+ />
148
+ ))}
149
+ {remoteSkills
150
+ .slice(
151
+ 0,
152
+ resultLimit === undefined ? undefined : Math.max(0, resultLimit - localSkills.length),
153
+ )
154
+ .map((skill) => {
155
+ const installed = installedSkills.some(
156
+ (entry) =>
157
+ entry.sourceUrl.replace(/\/$/, "").toLowerCase() === skill.url.toLowerCase() ||
158
+ (entry.repositoryUrl.replace(/\/$/, "").toLowerCase() ===
159
+ `https://github.com/${skill.source}`.toLowerCase() &&
160
+ entry.name.toLowerCase() === skill.name.toLowerCase()),
161
+ );
162
+ return (
163
+ <CapabilityCatalogRow
164
+ key={skill.id}
165
+ disabled={!canManage}
166
+ name={skill.name}
167
+ description={skill.source}
168
+ icon={<BookOpenIcon aria-hidden="true" />}
169
+ status={!canManage ? "unavailable" : installed ? "added" : "available"}
170
+ statusLabel={
171
+ !canManage ? "Admin required" : installed ? "Installed" : "Available to add"
172
+ }
173
+ onOpen={() => onImport(skill.url)}
174
+ />
175
+ );
176
+ })}
137
177
  </div>
178
+ {resultLimit && remoteSkills.length + localSkills.length > resultLimit && !loading ? (
179
+ <button className="og-catalog-more" type="button" onClick={onShowMore}>
180
+ View all skills
181
+ </button>
182
+ ) : null}
138
183
  {loading ? <p role="status">Loading skills…</p> : null}
139
184
  {error ? (
140
185
  <div role="alert" className="og-skill-discovery-error">
@@ -144,7 +189,7 @@ function DiscoveryResults({
144
189
  </button>
145
190
  </div>
146
191
  ) : null}
147
- {result && !result.items.length && !loading && !error ? (
192
+ {result && !remoteSkills.length && !localSkills.length && !loading && !error ? (
148
193
  <p role="status">No skills found. Try a different search.</p>
149
194
  ) : null}
150
195
  </div>