@puredesktop/puredesktop-ui-bridge 2.1.8 → 2.1.9

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 (75) hide show
  1. package/package.json +35 -5
  2. package/src/agents/mcpServerPresets.test.ts +105 -0
  3. package/src/agents/mcpServerPresets.ts +380 -0
  4. package/src/agents/runtime/index.ts +7 -0
  5. package/src/agents/runtime/mapAgentSnapshot.ts +11 -8
  6. package/src/agents/runtime/pendingAsks.test.ts +117 -0
  7. package/src/agents/runtime/pendingAsks.ts +110 -0
  8. package/src/bridge/agentScopeDefaults.ts +65 -0
  9. package/src/bridge/agentTypes.ts +36 -2
  10. package/src/bridge/agents.d.mts +6 -0
  11. package/src/bridge/agents.mjs +10 -0
  12. package/src/bridge/agents.ts +4 -0
  13. package/src/bridge/credentials.d.mts +62 -0
  14. package/src/bridge/credentials.mjs +53 -0
  15. package/src/bridge/events.d.mts +1 -0
  16. package/src/bridge/events.mjs +1 -0
  17. package/src/bridge/googleAuth.ts +7 -152
  18. package/src/bridge/mcpClient.d.mts +74 -0
  19. package/src/bridge/mcpClient.mjs +79 -0
  20. package/src/bridge/methods.d.mts +22 -0
  21. package/src/bridge/methods.mjs +23 -0
  22. package/src/bridge/notifications.d.mts +12 -0
  23. package/src/bridge/notifications.mjs +36 -0
  24. package/src/bridge/operations.d.mts +44 -0
  25. package/src/bridge/operations.mjs +40 -0
  26. package/src/bridge/react/usePlatformAgentSessionDrive.test.tsx +29 -10
  27. package/src/bridge/react/usePlatformAgentSessionDrive.tsx +10 -81
  28. package/src/bridge/react/usePlatformAppSettings.test.tsx +105 -105
  29. package/src/bridge/react/usePlatformAppSettings.tsx +104 -104
  30. package/src/bridge/react/usePlatformJsonStore.test.tsx +152 -152
  31. package/src/bridge/react/usePlatformJsonStore.tsx +149 -149
  32. package/src/bridge/react/usePlatformOperations.tsx +75 -0
  33. package/src/bridge/storage.d.mts +17 -4
  34. package/src/bridge/storage.mjs +6 -3
  35. package/src/components/agents/AgentComposer.tsx +10 -6
  36. package/src/components/agents/AgentDrawerPanel.test.tsx +2 -205
  37. package/src/components/agents/AgentDrawerPanel.tsx +23 -64
  38. package/src/components/agents/AgentMessageBubble.tsx +59 -33
  39. package/src/components/agents/AgentMessageList.tsx +4 -9
  40. package/src/components/agents/AgentQuestionPromptList.tsx +34 -212
  41. package/src/components/agents/AgentToolApprovalActions.tsx +13 -11
  42. package/src/components/agents/AgentToolPendingList.tsx +6 -6
  43. package/src/components/agents/AgentToolPendingReview.tsx +6 -6
  44. package/src/components/agents/QuestionRequestList.tsx +278 -0
  45. package/src/components/agents/agentPanelStyles.ts +9 -33
  46. package/src/components/agents/agentTypes.ts +25 -43
  47. package/src/components/agents/index.ts +4 -0
  48. package/src/components/assets/asset-library/sidebar/AssetLibraryList.tsx +10 -4
  49. package/src/components/chrome/WorkspaceTabStrip.tsx +4 -3
  50. package/src/components/common/chat/ChatBox.tsx +9 -9
  51. package/src/components/common/chat/ChatModelMenu.tsx +34 -4
  52. package/src/components/common/chat/ChatThread.tsx +23 -7
  53. package/src/components/common/connections/ProviderConnection.test.tsx +208 -0
  54. package/src/components/common/connections/ProviderConnection.tsx +277 -0
  55. package/src/components/common/connections/index.ts +5 -0
  56. package/src/components/common/containers/AppFrame.tsx +26 -19
  57. package/src/components/common/containers/AppHeader.tsx +20 -20
  58. package/src/components/common/documents/DocumentSwitcher.tsx +63 -58
  59. package/src/components/common/dropdown/MenuDropdownItem.tsx +14 -2
  60. package/src/components/common/dropdown/MenuPortal.tsx +3 -1
  61. package/src/components/common/dropdown/menuViewportPosition.test.ts +20 -10
  62. package/src/components/common/dropdown/menuViewportPosition.ts +35 -27
  63. package/src/components/common/overlays/Modal.tsx +13 -2
  64. package/src/components/editor/EditorLinkPromptDialog.tsx +1 -1
  65. package/src/ics/generateIcs.test.ts +153 -0
  66. package/src/ics/generateIcs.ts +197 -0
  67. package/src/index.ts +3 -0
  68. package/src/net/httpRetry.test.ts +117 -0
  69. package/src/net/httpRetry.ts +111 -0
  70. package/src/theme/appAccents.ts +6 -0
  71. package/src/theme/appIdentityCss.mjs +11 -0
  72. package/src/theme/appIdentityCss.ts +11 -0
  73. package/src/theme/themes/dark.ts +3 -1
  74. package/src/theme/themes/light.ts +3 -1
  75. package/src/bridge/googleProviderConfig.ts +0 -73
@@ -0,0 +1,277 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react'
2
+ import {
3
+ connectCredentialOAuth,
4
+ disconnectCredential,
5
+ readCredentialStatus,
6
+ setCredentialOAuthConfig,
7
+ type OAuthCredentialStatus,
8
+ } from '../../../bridge/credentials.mjs'
9
+ import { Button } from '../buttons/Button.js'
10
+ import { FormField } from '../inputs/FormField.js'
11
+ import { TextField } from '../inputs/TextField.js'
12
+ import {
13
+ ConnectionButtonRow,
14
+ ConnectionFieldGrid,
15
+ ConnectionPanel,
16
+ ConnectionPanelText,
17
+ ConnectionPanelTitle,
18
+ ConnectionTestResult,
19
+ } from './ConnectionSetup.js'
20
+
21
+ /**
22
+ * Bridge surface the component talks to. Overridable for tests; production
23
+ * defaults to the shell credentials bridge. The shell owns all state — this
24
+ * component never caches connection state and never sees secret material.
25
+ */
26
+ export interface ProviderConnectionClient {
27
+ status(request: { id: string }): Promise<OAuthCredentialStatus>
28
+ setOAuthConfig(request: {
29
+ id: string
30
+ clientId: string
31
+ clientSecret: string
32
+ scopes?: string[]
33
+ }): Promise<unknown>
34
+ connectOAuth(request: {
35
+ id: string
36
+ scopes?: string[]
37
+ }): Promise<{ email?: string }>
38
+ disconnect(request: { id: string; forget?: boolean }): Promise<unknown>
39
+ }
40
+
41
+ const bridgeClient: ProviderConnectionClient = {
42
+ status: readCredentialStatus,
43
+ setOAuthConfig: setCredentialOAuthConfig,
44
+ connectOAuth: connectCredentialOAuth,
45
+ disconnect: disconnectCredential,
46
+ }
47
+
48
+ export interface ProviderConnectionProps {
49
+ /** Shell credential id, e.g. `google.oauth`. */
50
+ credentialId: string
51
+ title: string
52
+ description?: string
53
+ /** Scopes this app needs; merged into the connection request. */
54
+ scopes?: string[]
55
+ /** "How to create an OAuth client" walkthrough for the unconfigured state. */
56
+ helpUrl?: string
57
+ /** Called after connect/disconnect so the host app can rebuild providers. */
58
+ onStatusChange?: (status: OAuthCredentialStatus) => void
59
+ client?: ProviderConnectionClient
60
+ }
61
+
62
+ type Busy = 'idle' | 'saving' | 'connecting' | 'disconnecting'
63
+
64
+ export function ProviderConnection({
65
+ credentialId,
66
+ title,
67
+ description,
68
+ scopes,
69
+ helpUrl,
70
+ onStatusChange,
71
+ client = bridgeClient,
72
+ }: ProviderConnectionProps): React.ReactElement {
73
+ const [status, setStatus] = useState<OAuthCredentialStatus | null>(null)
74
+ const [clientIdDraft, setClientIdDraft] = useState('')
75
+ const [clientSecretDraft, setClientSecretDraft] = useState('')
76
+ const [busy, setBusy] = useState<Busy>('idle')
77
+ const [error, setError] = useState<string | null>(null)
78
+
79
+ const refresh = useCallback(async () => {
80
+ const next = await client.status({ id: credentialId })
81
+ setStatus(next)
82
+ onStatusChange?.(next)
83
+ return next
84
+ }, [client, credentialId, onStatusChange])
85
+
86
+ useEffect(() => {
87
+ refresh().catch(cause =>
88
+ setError(cause instanceof Error ? cause.message : String(cause)),
89
+ )
90
+ }, [refresh])
91
+
92
+ const scopesRequest = useMemo(
93
+ () => (scopes?.length ? { scopes } : {}),
94
+ [scopes],
95
+ )
96
+
97
+ const saveConfig = useCallback(async () => {
98
+ setBusy('saving')
99
+ setError(null)
100
+ try {
101
+ await client.setOAuthConfig({
102
+ id: credentialId,
103
+ clientId: clientIdDraft,
104
+ clientSecret: clientSecretDraft,
105
+ ...scopesRequest,
106
+ })
107
+ // Write-only: the secret leaves the DOM the moment it is saved.
108
+ setClientSecretDraft('')
109
+ setClientIdDraft('')
110
+ await refresh()
111
+ } catch (cause) {
112
+ setError(cause instanceof Error ? cause.message : String(cause))
113
+ } finally {
114
+ setBusy('idle')
115
+ }
116
+ }, [client, credentialId, clientIdDraft, clientSecretDraft, refresh, scopesRequest])
117
+
118
+ const connect = useCallback(async () => {
119
+ setBusy('connecting')
120
+ setError(null)
121
+ try {
122
+ await client.connectOAuth({ id: credentialId, ...scopesRequest })
123
+ await refresh()
124
+ } catch (cause) {
125
+ setError(cause instanceof Error ? cause.message : String(cause))
126
+ } finally {
127
+ setBusy('idle')
128
+ }
129
+ }, [client, credentialId, refresh, scopesRequest])
130
+
131
+ const disconnect = useCallback(async () => {
132
+ setBusy('disconnecting')
133
+ setError(null)
134
+ try {
135
+ await client.disconnect({ id: credentialId })
136
+ await refresh()
137
+ } catch (cause) {
138
+ setError(cause instanceof Error ? cause.message : String(cause))
139
+ } finally {
140
+ setBusy('idle')
141
+ }
142
+ }, [client, credentialId, refresh])
143
+
144
+ const configured = status?.configured === true
145
+ const connected = status?.connected === true
146
+ const needsReconnect = status?.needsReconnect === true
147
+ const working = busy !== 'idle'
148
+
149
+ return (
150
+ <ConnectionPanel aria-busy={working}>
151
+ <ConnectionPanelTitle>{title}</ConnectionPanelTitle>
152
+ {description ? (
153
+ <ConnectionPanelText>{description}</ConnectionPanelText>
154
+ ) : null}
155
+
156
+ {!configured ? (
157
+ <>
158
+ <ConnectionPanelText>
159
+ Paste the OAuth client id and secret from your Google Cloud
160
+ console.{' '}
161
+ {helpUrl ? (
162
+ <a href={helpUrl} target="_blank" rel="noreferrer">
163
+ How to create an OAuth client
164
+ </a>
165
+ ) : null}
166
+ </ConnectionPanelText>
167
+ <ConnectionFieldGrid>
168
+ <FormField label="Client id" htmlFor={`${credentialId}-client-id`}>
169
+ <TextField
170
+ id={`${credentialId}-client-id`}
171
+ value={clientIdDraft}
172
+ autoComplete="off"
173
+ spellCheck={false}
174
+ disabled={working}
175
+ onChange={event => setClientIdDraft(event.target.value)}
176
+ />
177
+ </FormField>
178
+ <FormField
179
+ label="Client secret"
180
+ htmlFor={`${credentialId}-client-secret`}
181
+ hint="Stored in the encrypted vault; never shown again."
182
+ >
183
+ <TextField
184
+ id={`${credentialId}-client-secret`}
185
+ type="password"
186
+ value={clientSecretDraft}
187
+ autoComplete="off"
188
+ spellCheck={false}
189
+ disabled={working}
190
+ onChange={event => setClientSecretDraft(event.target.value)}
191
+ />
192
+ </FormField>
193
+ </ConnectionFieldGrid>
194
+ <ConnectionButtonRow>
195
+ <Button
196
+ type="button"
197
+ disabled={
198
+ working || !clientIdDraft.trim() || !clientSecretDraft.trim()
199
+ }
200
+ onClick={() => void saveConfig()}
201
+ >
202
+ {busy === 'saving' ? 'Saving…' : 'Save'}
203
+ </Button>
204
+ </ConnectionButtonRow>
205
+ </>
206
+ ) : null}
207
+
208
+ {configured && !connected && !needsReconnect ? (
209
+ <ConnectionButtonRow>
210
+ <Button
211
+ type="button"
212
+ variant="subtle"
213
+ disabled={working}
214
+ onClick={() => void disconnect()}
215
+ >
216
+ Remove
217
+ </Button>
218
+ <Button
219
+ type="button"
220
+ disabled={working}
221
+ onClick={() => void connect()}
222
+ >
223
+ {busy === 'connecting' ? 'Waiting for browser…' : 'Sign in with Google'}
224
+ </Button>
225
+ </ConnectionButtonRow>
226
+ ) : null}
227
+
228
+ {needsReconnect ? (
229
+ <ConnectionTestResult
230
+ tone="warning"
231
+ title="Reconnect required"
232
+ message={
233
+ status?.email
234
+ ? `Access for ${status.email} has expired or was revoked.`
235
+ : 'Access has expired or was revoked.'
236
+ }
237
+ />
238
+ ) : null}
239
+
240
+ {configured && (connected || needsReconnect) ? (
241
+ <ConnectionButtonRow>
242
+ <Button
243
+ type="button"
244
+ variant="subtle"
245
+ disabled={working}
246
+ onClick={() => void disconnect()}
247
+ >
248
+ Disconnect
249
+ </Button>
250
+ <Button
251
+ type="button"
252
+ disabled={working}
253
+ onClick={() => void connect()}
254
+ >
255
+ {busy === 'connecting'
256
+ ? 'Waiting for browser…'
257
+ : needsReconnect
258
+ ? 'Reconnect'
259
+ : 'Reconnect account'}
260
+ </Button>
261
+ </ConnectionButtonRow>
262
+ ) : null}
263
+
264
+ {connected ? (
265
+ <ConnectionTestResult
266
+ tone="success"
267
+ title={status?.email ? `Connected as ${status.email}` : 'Connected'}
268
+ message="Mail and Calendar share this connection."
269
+ />
270
+ ) : null}
271
+
272
+ {error ? (
273
+ <ConnectionTestResult tone="error" title="Something went wrong" message={error} />
274
+ ) : null}
275
+ </ConnectionPanel>
276
+ )
277
+ }
@@ -24,3 +24,8 @@ export {
24
24
  type ConnectionTestResultProps,
25
25
  type ConnectionTestTone,
26
26
  } from './ConnectionSetup.js'
27
+ export {
28
+ ProviderConnection,
29
+ type ProviderConnectionClient,
30
+ type ProviderConnectionProps,
31
+ } from './ProviderConnection.js'
@@ -15,14 +15,18 @@ export interface AppFrameProps
15
15
  themeFromBridge?: boolean
16
16
  /** Optional actions rendered in the app header's action slot. */
17
17
  headerActions?: ReactNode
18
- /** Open document's file name, shown beside the wordmark. */
19
- headerDocumentName?: ReactNode
20
- }
18
+ /** Open document's file name, shown beside the wordmark. */
19
+ headerDocumentName?: ReactNode
20
+ /** Hide the app wordmark when identity is already provided by host chrome. */
21
+ hideWordmark?: boolean
22
+ /** Optional app identity slug used before the shell bridge is ready. */
23
+ identityAppSlug?: string
24
+ }
21
25
 
22
26
  const StyledRoot = styled.div`
23
27
  display: flex;
24
28
  flex-direction: column;
25
- /* width: 100%; */
29
+ width: 100%;
26
30
  height: 100%;
27
31
  min-width: 0;
28
32
  min-height: 0;
@@ -38,10 +42,12 @@ export function AppFrame({
38
42
  children,
39
43
  className,
40
44
  themeFromBridge = true,
41
- headerActions,
42
- headerDocumentName,
43
- ...rest
44
- }: AppFrameProps): React.ReactElement {
45
+ headerActions,
46
+ headerDocumentName,
47
+ hideWordmark = false,
48
+ identityAppSlug,
49
+ ...rest
50
+ }: AppFrameProps): React.ReactElement {
45
51
  const { meta, ready } = usePlatformBridge()
46
52
  const { manifest } = usePlatformAppManifest(meta?.appSlug ?? null, ready)
47
53
 
@@ -51,17 +57,18 @@ export function AppFrame({
51
57
 
52
58
  useEffect(() => {
53
59
  const root = document.documentElement
54
- if (meta?.appSlug) {
55
- root.dataset.app = meta.appSlug
60
+ const appSlug = meta?.appSlug ?? identityAppSlug
61
+ if (appSlug) {
62
+ root.dataset.app = appSlug
56
63
  } else {
57
64
  delete root.dataset.app
58
65
  }
59
66
  return () => {
60
- if (root.dataset.app === meta?.appSlug) {
67
+ if (root.dataset.app === appSlug) {
61
68
  delete root.dataset.app
62
69
  }
63
70
  }
64
- }, [meta?.appSlug])
71
+ }, [identityAppSlug, meta?.appSlug])
65
72
 
66
73
  usePlatformThemeFromBridge(themeFromBridge)
67
74
  const themeAttrs = usePlatformThemeDocumentAttrs(themeFromBridge)
@@ -76,13 +83,13 @@ export function AppFrame({
76
83
  data-platform-surface-palette={themeAttrs.surfacePalette || undefined}
77
84
  {...rest}
78
85
  >
79
- {manifest && (
80
- <AppHeader
81
- manifest={manifest}
82
- actions={headerActions}
83
- documentName={headerDocumentName}
84
- />
85
- )}
86
+ {manifest && !hideWordmark && (
87
+ <AppHeader
88
+ manifest={manifest}
89
+ actions={headerActions}
90
+ documentName={headerDocumentName}
91
+ />
92
+ )}
86
93
  {children}
87
94
  </StyledRoot>
88
95
  </>
@@ -4,13 +4,13 @@ import type { PlatformAppManifest } from '../../../bridge/types.js'
4
4
 
5
5
  export type { PlatformAppManifest }
6
6
 
7
- export interface AppHeaderProps {
8
- manifest: PlatformAppManifest
9
- /** Optional slot for the app's tools — the header earns its row. */
10
- actions?: React.ReactNode
11
- /**
12
- * Convention: the open document's file name, shown beside the wordmark
13
- * (like any document-based app's window title).
7
+ export interface AppHeaderProps {
8
+ manifest: PlatformAppManifest
9
+ /** Optional slot for the app's tools — the header earns its row. */
10
+ actions?: React.ReactNode
11
+ /**
12
+ * Convention: the open document's file name, shown beside the wordmark
13
+ * (like any document-based app's window title).
14
14
  */
15
15
  documentName?: React.ReactNode
16
16
  }
@@ -66,19 +66,19 @@ const ActionSlot = styled.div`
66
66
 
67
67
  export const APP_HEADER_ACTIONS_PORTAL_ID = 'purescience-app-header-actions'
68
68
 
69
- export function AppHeader({
70
- manifest,
71
- actions,
72
- documentName,
73
- }: AppHeaderProps): React.ReactElement {
74
- return (
75
- <StyledHeader>
76
- <PureAppWordmark
77
- appName={manifest.navigationLabel ?? manifest.slug}
78
- size="sm"
79
- accentName
80
- />
81
- {documentName ? <DocumentName>{documentName}</DocumentName> : null}
69
+ export function AppHeader({
70
+ manifest,
71
+ actions,
72
+ documentName,
73
+ }: AppHeaderProps): React.ReactElement {
74
+ return (
75
+ <StyledHeader>
76
+ <PureAppWordmark
77
+ appName={manifest.navigationLabel ?? manifest.slug}
78
+ size="sm"
79
+ accentName
80
+ />
81
+ {documentName ? <DocumentName>{documentName}</DocumentName> : null}
82
82
  <ActionSlot id={APP_HEADER_ACTIONS_PORTAL_ID}>{actions}</ActionSlot>
83
83
  </StyledHeader>
84
84
  )