@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
@@ -1,9 +1,10 @@
1
- export interface GoogleOauthTokens {
2
- accessToken: string
3
- refreshToken?: string
4
- expiresAt: number
5
- scope: string
6
- }
1
+ /**
2
+ * Google OAuth scope sets apps request through the shell-owned credential
3
+ * flow (`credentials.connectOAuth`). The OAuth client machinery itself —
4
+ * PKCE, authorize URL, token exchange, refresh — lives in the Electron main
5
+ * process (`packages/shell/src/modules/credentials/googleOAuth.ts`); no
6
+ * renderer ever handles credential material.
7
+ */
7
8
 
8
9
  export const GMAIL_SCOPES = [
9
10
  'https://www.googleapis.com/auth/gmail.modify',
@@ -13,149 +14,3 @@ export const GMAIL_SCOPES = [
13
14
  export const CALENDAR_SCOPES = [
14
15
  'https://www.googleapis.com/auth/calendar.events',
15
16
  ]
16
-
17
- export const GOOGLE_AUTHORIZATION_ENDPOINT =
18
- 'https://accounts.google.com/o/oauth2/v2/auth'
19
- export const GOOGLE_TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token'
20
-
21
- const EXPIRY_SLACK_MS = 60_000
22
-
23
- export interface PkcePair {
24
- verifier: string
25
- challenge: string
26
- }
27
-
28
- function base64UrlEncode(bytes: Uint8Array): string {
29
- let binary = ''
30
- for (const byte of bytes) {
31
- binary += String.fromCharCode(byte)
32
- }
33
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
34
- }
35
-
36
- export function randomUrlSafeString(byteLength = 32): string {
37
- const bytes = new Uint8Array(byteLength)
38
- crypto.getRandomValues(bytes)
39
- return base64UrlEncode(bytes)
40
- }
41
-
42
- export async function createPkcePair(): Promise<PkcePair> {
43
- const verifier = randomUrlSafeString(48)
44
- const digest = await crypto.subtle.digest(
45
- 'SHA-256',
46
- new TextEncoder().encode(verifier),
47
- )
48
- return { verifier, challenge: base64UrlEncode(new Uint8Array(digest)) }
49
- }
50
-
51
- export interface GoogleAuthUrlInput {
52
- clientId: string
53
- challenge: string
54
- state: string
55
- scopes?: string[]
56
- }
57
-
58
- export function buildGoogleAuthorizationUrl(input: GoogleAuthUrlInput): string {
59
- const url = new URL(GOOGLE_AUTHORIZATION_ENDPOINT)
60
- url.searchParams.set('client_id', input.clientId)
61
- url.searchParams.set('response_type', 'code')
62
- url.searchParams.set('scope', (input.scopes ?? CALENDAR_SCOPES).join(' '))
63
- url.searchParams.set('code_challenge', input.challenge)
64
- url.searchParams.set('code_challenge_method', 'S256')
65
- url.searchParams.set('state', input.state)
66
- url.searchParams.set('access_type', 'offline')
67
- url.searchParams.set('prompt', 'consent')
68
- return url.toString()
69
- }
70
-
71
- interface GoogleTokenResponse {
72
- access_token: string
73
- refresh_token?: string
74
- expires_in: number
75
- scope: string
76
- }
77
-
78
- export function tokensFromTokenResponse(
79
- payload: GoogleTokenResponse,
80
- previousRefreshToken?: string,
81
- now = Date.now(),
82
- ): GoogleOauthTokens {
83
- const refreshToken = payload.refresh_token ?? previousRefreshToken
84
- return {
85
- accessToken: payload.access_token,
86
- ...(refreshToken === undefined ? {} : { refreshToken }),
87
- expiresAt: now + payload.expires_in * 1000,
88
- scope: payload.scope,
89
- }
90
- }
91
-
92
- export function accessTokenIsFresh(
93
- tokens: GoogleOauthTokens,
94
- now = Date.now(),
95
- ): boolean {
96
- return tokens.expiresAt - EXPIRY_SLACK_MS > now
97
- }
98
-
99
- export interface TokenExchangeInput {
100
- clientId: string
101
- clientSecret?: string
102
- code: string
103
- verifier: string
104
- redirectUri: string
105
- }
106
-
107
- type FormPost = (
108
- url: string,
109
- body: string,
110
- ) => Promise<{ ok: boolean; status: number; body: string }>
111
-
112
- export async function exchangeCodeForTokens(
113
- post: FormPost,
114
- input: TokenExchangeInput,
115
- ): Promise<GoogleOauthTokens> {
116
- const body = new URLSearchParams({
117
- client_id: input.clientId,
118
- grant_type: 'authorization_code',
119
- code: input.code,
120
- code_verifier: input.verifier,
121
- redirect_uri: input.redirectUri,
122
- })
123
- if (input.clientSecret) body.set('client_secret', input.clientSecret)
124
- const response = await post(GOOGLE_TOKEN_ENDPOINT, body.toString())
125
- if (!response.ok) {
126
- throw new Error(
127
- `Google token exchange failed (${response.status}): ${response.body}`,
128
- )
129
- }
130
- return tokensFromTokenResponse(
131
- JSON.parse(response.body) as GoogleTokenResponse,
132
- )
133
- }
134
-
135
- export interface TokenRefreshInput {
136
- clientId: string
137
- clientSecret?: string
138
- refreshToken: string
139
- }
140
-
141
- export async function refreshAccessToken(
142
- post: FormPost,
143
- input: TokenRefreshInput,
144
- ): Promise<GoogleOauthTokens> {
145
- const body = new URLSearchParams({
146
- client_id: input.clientId,
147
- grant_type: 'refresh_token',
148
- refresh_token: input.refreshToken,
149
- })
150
- if (input.clientSecret) body.set('client_secret', input.clientSecret)
151
- const response = await post(GOOGLE_TOKEN_ENDPOINT, body.toString())
152
- if (!response.ok) {
153
- throw new Error(
154
- `Google token refresh failed (${response.status}): ${response.body}`,
155
- )
156
- }
157
- return tokensFromTokenResponse(
158
- JSON.parse(response.body) as GoogleTokenResponse,
159
- input.refreshToken,
160
- )
161
- }
@@ -0,0 +1,74 @@
1
+ export type McpClientTransportType = 'http' | 'sse' | 'stdio'
2
+ export type McpClientAuthType = 'none' | 'bearer' | 'apiKey'
3
+
4
+ /** A configured external MCP server (no secret material — that stays shell-side). */
5
+ export interface McpClientServerConfig {
6
+ /** Stable slug — `[a-z0-9-]+`. */
7
+ id: string
8
+ label: string
9
+ transport: McpClientTransportType
10
+ /** Remote endpoint for `http` / `sse` transports. */
11
+ url?: string
12
+ /** Executable for the `stdio` transport. */
13
+ command?: string
14
+ args?: string[]
15
+ enabled: boolean
16
+ authType: McpClientAuthType
17
+ /** Env var the stored secret is injected as, when `authType` is `apiKey`. */
18
+ apiKeyEnvVar?: string
19
+ /** Short tool names opted into auto-run; everything else asks first. */
20
+ autoApproveTools?: string[]
21
+ }
22
+
23
+ export interface McpClientServerView extends McpClientServerConfig {
24
+ hasSecret: boolean
25
+ }
26
+
27
+ export interface McpClientSettingsView {
28
+ enabled: boolean
29
+ servers: McpClientServerView[]
30
+ }
31
+
32
+ export interface McpClientTestResult {
33
+ ok: boolean
34
+ serverId: string
35
+ toolNames: string[]
36
+ error?: string
37
+ }
38
+
39
+ export interface McpToolInfo {
40
+ name: string
41
+ description?: string
42
+ inputSchema: Record<string, unknown>
43
+ }
44
+
45
+ export interface McpToolRunResult {
46
+ content: string
47
+ isError: boolean
48
+ }
49
+
50
+ export function listMcpClientSettings(): Promise<McpClientSettingsView>
51
+ export function setMcpClientEnabled(
52
+ enabled: boolean,
53
+ ): Promise<McpClientSettingsView>
54
+ export function upsertMcpClientServer(
55
+ config: McpClientServerConfig,
56
+ ): Promise<McpClientSettingsView>
57
+ export function removeMcpClientServer(
58
+ serverId: string,
59
+ ): Promise<McpClientSettingsView>
60
+ export function setMcpClientServerSecret(
61
+ serverId: string,
62
+ value: string | null,
63
+ ): Promise<McpClientSettingsView>
64
+ export function testMcpClientServer(
65
+ config: McpClientServerConfig,
66
+ ): Promise<McpClientTestResult>
67
+ export function listMcpClientServerTools(
68
+ serverId: string,
69
+ ): Promise<McpToolInfo[]>
70
+ export function callMcpClientServerTool(
71
+ serverId: string,
72
+ toolName: string,
73
+ args: Record<string, unknown>,
74
+ ): Promise<McpToolRunResult>
@@ -0,0 +1,79 @@
1
+ import { bridge } from './client.mjs'
2
+ import { PLATFORM_BRIDGE_METHODS } from './methods.mjs'
3
+
4
+ /**
5
+ * Outbound MCP client registry (external MCP servers whose tools in-app
6
+ * agents can call). Secret material never crosses this bridge — only the
7
+ * redacted settings view does. The shell gates these methods to the
8
+ * assistant app: this is machine-level, secret-bearing configuration.
9
+ */
10
+
11
+ /**
12
+ * @returns {Promise<import('./mcpClient.d.mts').McpClientSettingsView>}
13
+ */
14
+ export async function listMcpClientSettings() {
15
+ return bridge.call(PLATFORM_BRIDGE_METHODS.MCPCLIENT_LIST, [])
16
+ }
17
+
18
+ /**
19
+ * @param {boolean} enabled
20
+ * @returns {Promise<import('./mcpClient.d.mts').McpClientSettingsView>}
21
+ */
22
+ export async function setMcpClientEnabled(enabled) {
23
+ return bridge.call(PLATFORM_BRIDGE_METHODS.MCPCLIENT_SET_ENABLED, [enabled])
24
+ }
25
+
26
+ /**
27
+ * @param {import('./mcpClient.d.mts').McpClientServerConfig} config
28
+ * @returns {Promise<import('./mcpClient.d.mts').McpClientSettingsView>}
29
+ */
30
+ export async function upsertMcpClientServer(config) {
31
+ return bridge.call(PLATFORM_BRIDGE_METHODS.MCPCLIENT_UPSERT, [config])
32
+ }
33
+
34
+ /**
35
+ * @param {string} serverId
36
+ * @returns {Promise<import('./mcpClient.d.mts').McpClientSettingsView>}
37
+ */
38
+ export async function removeMcpClientServer(serverId) {
39
+ return bridge.call(PLATFORM_BRIDGE_METHODS.MCPCLIENT_REMOVE, [serverId])
40
+ }
41
+
42
+ /**
43
+ * @param {string} serverId
44
+ * @param {string | null} value
45
+ * @returns {Promise<import('./mcpClient.d.mts').McpClientSettingsView>}
46
+ */
47
+ export async function setMcpClientServerSecret(serverId, value) {
48
+ return bridge.call(PLATFORM_BRIDGE_METHODS.MCPCLIENT_SET_SECRET, [
49
+ { serverId, value },
50
+ ])
51
+ }
52
+
53
+ /**
54
+ * @param {import('./mcpClient.d.mts').McpClientServerConfig} config
55
+ * @returns {Promise<import('./mcpClient.d.mts').McpClientTestResult>}
56
+ */
57
+ export async function testMcpClientServer(config) {
58
+ return bridge.call(PLATFORM_BRIDGE_METHODS.MCPCLIENT_TEST, [config])
59
+ }
60
+
61
+ /**
62
+ * @param {string} serverId
63
+ * @returns {Promise<import('./mcpClient.d.mts').McpToolInfo[]>}
64
+ */
65
+ export async function listMcpClientServerTools(serverId) {
66
+ return bridge.call(PLATFORM_BRIDGE_METHODS.MCPCLIENT_LIST_TOOLS, [serverId])
67
+ }
68
+
69
+ /**
70
+ * @param {string} serverId
71
+ * @param {string} toolName
72
+ * @param {Record<string, unknown>} args
73
+ * @returns {Promise<import('./mcpClient.d.mts').McpToolRunResult>}
74
+ */
75
+ export async function callMcpClientServerTool(serverId, toolName, args) {
76
+ return bridge.call(PLATFORM_BRIDGE_METHODS.MCPCLIENT_CALL_TOOL, [
77
+ { serverId, toolName, arguments: args },
78
+ ])
79
+ }
@@ -20,6 +20,8 @@ export declare const PLATFORM_BRIDGE_METHODS: {
20
20
  readonly SETTINGS_APP_UPDATE: 'settings.app.update'
21
21
  readonly STORAGE_READ_JSON: 'storage.readJson'
22
22
  readonly STORAGE_WRITE_JSON: 'storage.writeJson'
23
+ readonly OPERATIONS_RECORD: 'operations.record'
24
+ readonly OPERATIONS_LIST: 'operations.list'
23
25
  readonly FS_LIST: 'fs.list'
24
26
  readonly FS_READ_PREVIEW: 'fs.readPreview'
25
27
  readonly FS_READ_BINARY: 'fs.readBinary'
@@ -63,6 +65,9 @@ export declare const PLATFORM_BRIDGE_METHODS: {
63
65
  readonly NETWORK_FETCH: 'network.fetch'
64
66
  readonly RESEARCH_QUERY: 'research.query'
65
67
  readonly VISION_GENERATE_IMAGE: 'vision.generateImage'
68
+ readonly NOTIFICATIONS_SCHEDULE: 'notifications.schedule'
69
+ readonly NOTIFICATIONS_CANCEL: 'notifications.cancel'
70
+ readonly NOTIFICATIONS_CANCEL_ALL: 'notifications.cancelAll'
66
71
  readonly OAUTH_LOOPBACK_AUTHORIZE: 'oauth.loopbackAuthorize'
67
72
  readonly SECRETS_GET: 'secrets.get'
68
73
  readonly SECRETS_SET: 'secrets.set'
@@ -74,6 +79,14 @@ export declare const PLATFORM_BRIDGE_METHODS: {
74
79
  readonly MYTOOLS_DELETE: 'mytools.delete'
75
80
  readonly MYTOOLS_TEST: 'mytools.test'
76
81
  readonly MYTOOLS_RESEARCH: 'mytools.research'
82
+ readonly MCPCLIENT_LIST: 'mcpClient.list'
83
+ readonly MCPCLIENT_SET_ENABLED: 'mcpClient.setEnabled'
84
+ readonly MCPCLIENT_UPSERT: 'mcpClient.upsertServer'
85
+ readonly MCPCLIENT_REMOVE: 'mcpClient.removeServer'
86
+ readonly MCPCLIENT_SET_SECRET: 'mcpClient.setSecret'
87
+ readonly MCPCLIENT_TEST: 'mcpClient.testServer'
88
+ readonly MCPCLIENT_LIST_TOOLS: 'mcpClient.listTools'
89
+ readonly MCPCLIENT_CALL_TOOL: 'mcpClient.callTool'
77
90
  readonly DEBUG_EMIT_EVENT: 'debug.emitEvent'
78
91
  readonly PLUGINS_INSTALL: 'plugins.install'
79
92
  readonly PLUGINS_EVALUATE_BUILD: 'plugins.evaluateBuild'
@@ -88,6 +101,11 @@ export declare const PLATFORM_BRIDGE_METHODS: {
88
101
  readonly CREDENTIALS_LIST: 'credentials.list'
89
102
  readonly CREDENTIALS_SET: 'credentials.set'
90
103
  readonly CREDENTIALS_DELETE: 'credentials.delete'
104
+ readonly CREDENTIALS_STATUS: 'credentials.status'
105
+ readonly CREDENTIALS_SET_OAUTH_CONFIG: 'credentials.setOAuthConfig'
106
+ readonly CREDENTIALS_CONNECT_OAUTH: 'credentials.connectOAuth'
107
+ readonly CREDENTIALS_DISCONNECT: 'credentials.disconnect'
108
+ readonly CREDENTIALS_OAUTH_ACCESS_TOKEN: 'credentials.oauthAccessToken'
91
109
  readonly AGENTS_SESSIONS_CREATE: 'agents.sessions.create'
92
110
  readonly AGENTS_SESSIONS_LIST: 'agents.sessions.list'
93
111
  readonly AGENTS_SESSIONS_READ: 'agents.sessions.read'
@@ -98,6 +116,7 @@ export declare const PLATFORM_BRIDGE_METHODS: {
98
116
  readonly AGENTS_JOBS_READ: 'agents.jobs.read'
99
117
  readonly AGENTS_JOBS_CANCEL: 'agents.jobs.cancel'
100
118
  readonly AGENTS_JOBS_RUN_NOW: 'agents.jobs.runNow'
119
+ readonly AGENTS_DELEGATIONS_LIST: 'agents.delegations.list'
101
120
  readonly AGENTS_SESSION_SEND_MESSAGE: 'agents.session.sendMessage'
102
121
  readonly AGENTS_SESSION_APPROVE_TOOLS: 'agents.session.approveTools'
103
122
  readonly AGENTS_SESSION_REJECT_TOOLS: 'agents.session.rejectTools'
@@ -141,13 +160,16 @@ export declare const PLATFORM_BRIDGE_METHOD_PREFIXES: {
141
160
  readonly NETWORK: 'network.'
142
161
  readonly RESEARCH: 'research.'
143
162
  readonly VISION: 'vision.'
163
+ readonly NOTIFICATIONS: 'notifications.'
144
164
  readonly OAUTH: 'oauth.'
145
165
  readonly SECRETS: 'secrets.'
146
166
  readonly MYTOOLS: 'mytools.'
167
+ readonly MCPCLIENT: 'mcpClient.'
147
168
  readonly AGENTS: 'agents.'
148
169
  readonly PEOPLE: 'people.'
149
170
  readonly DEBUG: 'debug.'
150
171
  readonly DOCUMENTS: 'documents.'
172
+ readonly OPERATIONS: 'operations.'
151
173
  }
152
174
 
153
175
  export type PlatformBridgeMethodName =
@@ -27,6 +27,8 @@ export const PLATFORM_BRIDGE_METHODS = {
27
27
 
28
28
  STORAGE_READ_JSON: 'storage.readJson',
29
29
  STORAGE_WRITE_JSON: 'storage.writeJson',
30
+ OPERATIONS_RECORD: 'operations.record',
31
+ OPERATIONS_LIST: 'operations.list',
30
32
 
31
33
  FS_LIST: 'fs.list',
32
34
  FS_READ_PREVIEW: 'fs.readPreview',
@@ -78,6 +80,9 @@ export const PLATFORM_BRIDGE_METHODS = {
78
80
  NETWORK_FETCH: 'network.fetch',
79
81
  RESEARCH_QUERY: 'research.query',
80
82
  VISION_GENERATE_IMAGE: 'vision.generateImage',
83
+ NOTIFICATIONS_SCHEDULE: 'notifications.schedule',
84
+ NOTIFICATIONS_CANCEL: 'notifications.cancel',
85
+ NOTIFICATIONS_CANCEL_ALL: 'notifications.cancelAll',
81
86
  OAUTH_LOOPBACK_AUTHORIZE: 'oauth.loopbackAuthorize',
82
87
  SECRETS_GET: 'secrets.get',
83
88
  SECRETS_SET: 'secrets.set',
@@ -90,6 +95,15 @@ export const PLATFORM_BRIDGE_METHODS = {
90
95
  MYTOOLS_TEST: 'mytools.test',
91
96
  MYTOOLS_RESEARCH: 'mytools.research',
92
97
 
98
+ MCPCLIENT_LIST: 'mcpClient.list',
99
+ MCPCLIENT_SET_ENABLED: 'mcpClient.setEnabled',
100
+ MCPCLIENT_UPSERT: 'mcpClient.upsertServer',
101
+ MCPCLIENT_REMOVE: 'mcpClient.removeServer',
102
+ MCPCLIENT_SET_SECRET: 'mcpClient.setSecret',
103
+ MCPCLIENT_TEST: 'mcpClient.testServer',
104
+ MCPCLIENT_LIST_TOOLS: 'mcpClient.listTools',
105
+ MCPCLIENT_CALL_TOOL: 'mcpClient.callTool',
106
+
93
107
  DEBUG_EMIT_EVENT: 'debug.emitEvent',
94
108
 
95
109
  PLUGINS_INSTALL: 'plugins.install',
@@ -106,6 +120,11 @@ export const PLATFORM_BRIDGE_METHODS = {
106
120
  CREDENTIALS_LIST: 'credentials.list',
107
121
  CREDENTIALS_SET: 'credentials.set',
108
122
  CREDENTIALS_DELETE: 'credentials.delete',
123
+ CREDENTIALS_STATUS: 'credentials.status',
124
+ CREDENTIALS_SET_OAUTH_CONFIG: 'credentials.setOAuthConfig',
125
+ CREDENTIALS_CONNECT_OAUTH: 'credentials.connectOAuth',
126
+ CREDENTIALS_DISCONNECT: 'credentials.disconnect',
127
+ CREDENTIALS_OAUTH_ACCESS_TOKEN: 'credentials.oauthAccessToken',
109
128
 
110
129
  AGENTS_SESSIONS_CREATE: 'agents.sessions.create',
111
130
  AGENTS_SESSIONS_LIST: 'agents.sessions.list',
@@ -118,6 +137,7 @@ export const PLATFORM_BRIDGE_METHODS = {
118
137
  AGENTS_JOBS_READ: 'agents.jobs.read',
119
138
  AGENTS_JOBS_CANCEL: 'agents.jobs.cancel',
120
139
  AGENTS_JOBS_RUN_NOW: 'agents.jobs.runNow',
140
+ AGENTS_DELEGATIONS_LIST: 'agents.delegations.list',
121
141
 
122
142
  AGENTS_SESSION_SEND_MESSAGE: 'agents.session.sendMessage',
123
143
  AGENTS_SESSION_APPROVE_TOOLS: 'agents.session.approveTools',
@@ -163,11 +183,14 @@ export const PLATFORM_BRIDGE_METHOD_PREFIXES = {
163
183
  NETWORK: 'network.',
164
184
  RESEARCH: 'research.',
165
185
  VISION: 'vision.',
186
+ NOTIFICATIONS: 'notifications.',
166
187
  OAUTH: 'oauth.',
167
188
  SECRETS: 'secrets.',
168
189
  MYTOOLS: 'mytools.',
190
+ MCPCLIENT: 'mcpClient.',
169
191
  AGENTS: 'agents.',
170
192
  PEOPLE: 'people.',
171
193
  DEBUG: 'debug.',
172
194
  DOCUMENTS: 'documents.',
195
+ OPERATIONS: 'operations.',
173
196
  }
@@ -0,0 +1,12 @@
1
+ export function scheduleNotification(request: {
2
+ id: string
3
+ title: string
4
+ body?: string
5
+ fireAt: number
6
+ }): Promise<{ ok: true }>
7
+
8
+ export function cancelNotification(request: {
9
+ id: string
10
+ }): Promise<{ ok: true }>
11
+
12
+ export function cancelAllNotifications(): Promise<{ ok: true }>
@@ -0,0 +1,36 @@
1
+ import { bridge } from './client.mjs'
2
+ import { PLATFORM_BRIDGE_METHODS } from './methods.mjs'
3
+
4
+ /**
5
+ * OS notification scheduling — a benign local capability (no plugin permission).
6
+ * Schedules are held in the shell main process and die when the app quits;
7
+ * reminders while the app is closed are out of scope. The app-side scheduler
8
+ * that decides *what* to schedule is the caller's responsibility.
9
+ */
10
+
11
+ /**
12
+ * Schedule an OS notification to fire at `fireAt`. Reusing an `id` replaces any
13
+ * pending schedule (dedupe). `fireAt` in the past fires ~immediately.
14
+ * @param {{ id: string, title: string, body?: string, fireAt: number }} request
15
+ * @returns {Promise<{ ok: true }>}
16
+ */
17
+ export async function scheduleNotification(request) {
18
+ return bridge.call(PLATFORM_BRIDGE_METHODS.NOTIFICATIONS_SCHEDULE, [request])
19
+ }
20
+
21
+ /**
22
+ * Cancel a pending schedule; no-op if the id is unknown.
23
+ * @param {{ id: string }} request
24
+ * @returns {Promise<{ ok: true }>}
25
+ */
26
+ export async function cancelNotification(request) {
27
+ return bridge.call(PLATFORM_BRIDGE_METHODS.NOTIFICATIONS_CANCEL, [request])
28
+ }
29
+
30
+ /**
31
+ * Clear every pending schedule (used when an app reloads its reminder set).
32
+ * @returns {Promise<{ ok: true }>}
33
+ */
34
+ export async function cancelAllNotifications() {
35
+ return bridge.call(PLATFORM_BRIDGE_METHODS.NOTIFICATIONS_CANCEL_ALL, [])
36
+ }
@@ -0,0 +1,44 @@
1
+ export type PlatformOperationLane = 'user' | 'agent'
2
+
3
+ export interface PlatformOperationInput {
4
+ /** Who acted: a person in the UI, or an agent doing work. */
5
+ lane: PlatformOperationLane
6
+ /** Stable machine-readable kind, dot-namespaced, e.g. `document.save`. */
7
+ kind: string
8
+ /** Originating app slug, or `shell` for platform-level operations. */
9
+ appSlug: string
10
+ /** One human-readable sentence describing the operation. */
11
+ summary: string
12
+ /** Optional free-form elaboration. */
13
+ detail?: string
14
+ /** Optional references (path, sessionId, runId, …) for drill-down. */
15
+ refs?: Record<string, string>
16
+ }
17
+
18
+ export interface PlatformOperation extends PlatformOperationInput {
19
+ id: string
20
+ /** ISO timestamp stamped by the shell. */
21
+ at: string
22
+ }
23
+
24
+ export interface PlatformOperationsListQuery {
25
+ lane?: PlatformOperationLane
26
+ /** Max entries returned, newest first. */
27
+ limit?: number
28
+ }
29
+
30
+ export interface PlatformOperationsListResult {
31
+ operations: PlatformOperation[]
32
+ }
33
+
34
+ export declare function recordPlatformOperation(
35
+ input: PlatformOperationInput,
36
+ ): Promise<PlatformOperation>
37
+
38
+ export declare function listPlatformOperations(
39
+ query?: PlatformOperationsListQuery,
40
+ ): Promise<PlatformOperationsListResult>
41
+
42
+ export declare function onPlatformOperationRecorded(
43
+ listener: (operation: PlatformOperation) => void,
44
+ ): () => void
@@ -0,0 +1,40 @@
1
+ import { bridge } from './client.mjs'
2
+ import { PLATFORM_BRIDGE_EVENTS } from './events.mjs'
3
+ import { PLATFORM_BRIDGE_METHODS } from './methods.mjs'
4
+
5
+ /**
6
+ * Platform operations ledger: one suite-wide record of what happened,
7
+ * split into two lanes — `user` interactions and `agent` interactions.
8
+ * The shell stores every entry and broadcasts each append, so UIs (the
9
+ * PureAssistant tab in particular) can follow activity in real time.
10
+ */
11
+
12
+ /**
13
+ * Record one operation into the ledger.
14
+ *
15
+ * @param {import('./operations.d.mts').PlatformOperationInput} input
16
+ * @returns {Promise<import('./operations.d.mts').PlatformOperation>}
17
+ */
18
+ export async function recordPlatformOperation(input) {
19
+ return bridge.call(PLATFORM_BRIDGE_METHODS.OPERATIONS_RECORD, [input])
20
+ }
21
+
22
+ /**
23
+ * List recorded operations, newest first.
24
+ *
25
+ * @param {import('./operations.d.mts').PlatformOperationsListQuery} [query]
26
+ * @returns {Promise<import('./operations.d.mts').PlatformOperationsListResult>}
27
+ */
28
+ export async function listPlatformOperations(query) {
29
+ return bridge.call(PLATFORM_BRIDGE_METHODS.OPERATIONS_LIST, [query ?? {}])
30
+ }
31
+
32
+ /**
33
+ * Subscribe to ledger appends. The listener receives each new operation.
34
+ *
35
+ * @param {(operation: import('./operations.d.mts').PlatformOperation) => void} listener
36
+ * @returns {() => void} unsubscribe
37
+ */
38
+ export function onPlatformOperationRecorded(listener) {
39
+ return bridge.onEvent(PLATFORM_BRIDGE_EVENTS.OPERATIONS_CHANGED, listener)
40
+ }
@@ -104,16 +104,35 @@ describe('usePlatformAgentSessionDrive', () => {
104
104
  ;(globalThis as ReactActGlobal).IS_REACT_ACT_ENVIRONMENT = true
105
105
  })
106
106
 
107
- beforeEach(() => {
108
- latestDrive = null
109
- host = document.createElement('div')
110
- document.body.appendChild(host)
111
- root = createRoot(host)
112
- agentsMock.readAgentSession.mockResolvedValue(sessionRecord)
113
- agentsMock.advanceAgentSessionPending.mockResolvedValue(null)
114
- agentsMock.readAgentLlmMenuOptions.mockResolvedValue([])
115
- agentsMock.onAgentRunEvent.mockReturnValue(vi.fn())
116
- })
107
+ beforeEach(() => {
108
+ latestDrive = null
109
+ host = document.createElement('div')
110
+ document.body.appendChild(host)
111
+ root = createRoot(host)
112
+ agentsMock.readAgentSession.mockResolvedValue(sessionRecord)
113
+ agentsMock.advanceAgentSessionPending.mockResolvedValue({
114
+ session: sessionRecord,
115
+ snapshot: {
116
+ runId: sessionRecord.runId,
117
+ state: sessionRecord.snapshot.state,
118
+ turn: sessionRecord.snapshot.turn,
119
+ messages: sessionRecord.snapshot.messages,
120
+ },
121
+ effectiveToolPolicy: {
122
+ allowedTargetApps: ['book'],
123
+ defaultRequiresApproval: false,
124
+ approvalOverrides: {
125
+ 'book.write': true,
126
+ 'book.review': true,
127
+ },
128
+ },
129
+ manualPendingToolCalls: [
130
+ { id: 'call-manual', name: 'book.review', arguments: {} },
131
+ ],
132
+ })
133
+ agentsMock.readAgentLlmMenuOptions.mockResolvedValue([])
134
+ agentsMock.onAgentRunEvent.mockReturnValue(vi.fn())
135
+ })
117
136
 
118
137
  afterEach(() => {
119
138
  act(() => root.unmount())