@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
@@ -31,10 +31,7 @@ import {
31
31
  isAgentAutoModelId,
32
32
  DEFAULT_AGENT_HIGH_LEVEL_MODEL_ID,
33
33
  } from '../agentModelTiers.js'
34
- import {
35
- getPlatformPreferences,
36
- patchPlatformPreferences,
37
- } from '../preferences.mjs'
34
+ import { patchAgentSessionScopeDefaults } from '../agentScopeDefaults.js'
38
35
  import {
39
36
  advanceAgentSessionPending,
40
37
  approveAgentSessionTools,
@@ -95,10 +92,6 @@ export interface UsePlatformAgentSessionDrive {
95
92
  /** Send the composer value, or an explicit message (programmatic). Returns the final snapshot. */
96
93
  sendMessage: (message?: string) => Promise<PlatformAgentRunSnapshot | null>
97
94
  approveTools: (toolCallIds?: readonly string[]) => Promise<void>
98
- approveToolAlwaysForSession: (
99
- qualifiedName: string,
100
- toolCallId: string,
101
- ) => Promise<void>
102
95
  rejectTools: (toolCallIds?: readonly string[]) => Promise<void>
103
96
  selectSession: (sessionId: string) => Promise<void>
104
97
  createNewSession: () => Promise<void>
@@ -121,9 +114,9 @@ interface DriveResult {
121
114
  }
122
115
 
123
116
  /** Display snapshot from a persisted session record (before any live run this turn). */
124
- function recordSnapshot(
125
- record: PlatformAgentSessionRecord | null,
126
- ): PlatformAgentRunSnapshot | null {
117
+ function recordSnapshot(
118
+ record: PlatformAgentSessionRecord | null,
119
+ ): PlatformAgentRunSnapshot | null {
127
120
  if (!record) return null
128
121
  return {
129
122
  runId: record.runId ?? '',
@@ -132,28 +125,10 @@ function recordSnapshot(
132
125
  messages: record.snapshot.messages,
133
126
  error: record.snapshot.error,
134
127
  interruptedReason: record.snapshot.interruptedReason,
135
- }
136
- }
137
-
138
- function mergeScopeDefaultPatch(
139
- base: PlatformAgentSessionConfigPatch | undefined,
140
- patch: PlatformAgentSessionConfigPatch,
141
- ): PlatformAgentSessionConfigPatch {
142
- const toolPolicy =
143
- patch.toolPolicy || base?.toolPolicy
144
- ? {
145
- ...base?.toolPolicy,
146
- ...patch.toolPolicy,
147
- approvalOverrides: {
148
- ...base?.toolPolicy?.approvalOverrides,
149
- ...patch.toolPolicy?.approvalOverrides,
150
- },
151
- }
152
- : undefined
153
- return { ...base, ...patch, ...(toolPolicy ? { toolPolicy } : {}) }
154
- }
155
-
156
- export function usePlatformAgentSessionDrive({
128
+ }
129
+ }
130
+
131
+ export function usePlatformAgentSessionDrive({
157
132
  appSlug,
158
133
  scope: initialScope = 'app',
159
134
  enabled = true,
@@ -404,7 +379,7 @@ export function usePlatformAgentSessionDrive({
404
379
  : 'This model is not available. Check API keys or local runtime in Settings.'
405
380
  }, [modelOptions, selectedModelId])
406
381
 
407
- const toolPolicy = policy ?? sessionRecord?.config?.toolPolicy ?? undefined
382
+ const toolPolicy = policy
408
383
  const acceptedPendingToolCallIds = useMemo(
409
384
  () =>
410
385
  sessionRecord?.runId && sessionRecord.runId === threadSnapshot?.runId
@@ -607,28 +582,7 @@ export function usePlatformAgentSessionDrive({
607
582
  const persistScopeDefault = useCallback(
608
583
  async (patch: PlatformAgentSessionConfigPatch) => {
609
584
  try {
610
- const prefs = (await getPlatformPreferences()) as {
611
- agentSessionDefaults?: {
612
- global?: PlatformAgentSessionConfigPatch
613
- apps?: Record<string, PlatformAgentSessionConfigPatch>
614
- }
615
- }
616
- const stored = prefs.agentSessionDefaults ?? {}
617
- if (scope === 'global') {
618
- await patchPlatformPreferences({
619
- agentSessionDefaults: {
620
- ...stored,
621
- global: mergeScopeDefaultPatch(stored.global, patch),
622
- },
623
- })
624
- return
625
- }
626
- if (!scopeAppSlug) return
627
- const apps = { ...(stored.apps ?? {}) }
628
- apps[scopeAppSlug] = mergeScopeDefaultPatch(apps[scopeAppSlug], patch)
629
- await patchPlatformPreferences({
630
- agentSessionDefaults: { ...stored, apps },
631
- })
585
+ await patchAgentSessionScopeDefaults(scope, scopeAppSlug, patch)
632
586
  } catch {
633
587
  /* scope defaults are best-effort */
634
588
  }
@@ -696,30 +650,6 @@ export function usePlatformAgentSessionDrive({
696
650
  [persistScopeDefault, sessionQuery, sessionRecord],
697
651
  )
698
652
 
699
- const approveToolAlwaysForSession = useCallback(
700
- async (qualifiedName: string, toolCallId: string) => {
701
- if (!sessionRecord) return
702
- await drive(async () => {
703
- const overrides = {
704
- ...sessionRecord.config.toolPolicy?.approvalOverrides,
705
- [qualifiedName]: false,
706
- }
707
- const updated = await updateAgentSession({
708
- query: sessionQuery(sessionRecord.sessionId),
709
- patch: { config: { toolPolicy: { approvalOverrides: overrides } } },
710
- })
711
- await persistScopeDefault({
712
- toolPolicy: { approvalOverrides: overrides },
713
- })
714
- return approveAgentSessionTools({
715
- session: sessionQuery(updated.sessionId),
716
- toolCallIds: [toolCallId],
717
- })
718
- })
719
- },
720
- [drive, persistScopeDefault, sessionQuery, sessionRecord],
721
- )
722
-
723
653
  return {
724
654
  scope,
725
655
  setScope,
@@ -744,7 +674,6 @@ export function usePlatformAgentSessionDrive({
744
674
  sendMessage,
745
675
  interruptRun,
746
676
  approveTools,
747
- approveToolAlwaysForSession,
748
677
  rejectTools,
749
678
  selectSession,
750
679
  createNewSession,
@@ -1,105 +1,105 @@
1
- // @vitest-environment happy-dom
2
- import { act } from 'react'
3
- import { createRoot, type Root } from 'react-dom/client'
4
- import {
5
- afterEach,
6
- beforeAll,
7
- beforeEach,
8
- describe,
9
- expect,
10
- it,
11
- vi,
12
- } from 'vitest'
13
- import { usePlatformAppSettings } from './usePlatformAppSettings.js'
14
-
15
- type ReactActGlobal = typeof globalThis & {
16
- IS_REACT_ACT_ENVIRONMENT?: boolean
17
- }
18
-
19
- type Settings = Record<string, unknown> & {
20
- view: 'list' | 'board'
21
- }
22
-
23
- const appSettingsMock = vi.hoisted(() => ({
24
- getPlatformAppSettings: vi.fn(),
25
- updatePlatformAppSettings: vi.fn(),
26
- }))
27
-
28
- vi.mock('../appSettings.mjs', () => appSettingsMock)
29
-
30
- let latest:
31
- | ReturnType<typeof usePlatformAppSettings<Settings>>
32
- | null = null
33
-
34
- function parseSettings(raw: Record<string, unknown>): Settings {
35
- return {
36
- view: raw.view === 'board' ? 'board' : 'list',
37
- }
38
- }
39
-
40
- function Probe(): React.ReactElement {
41
- latest = usePlatformAppSettings<Settings>({
42
- appSlug: 'tasks',
43
- parse: parseSettings,
44
- initialSettings: { view: 'list' },
45
- })
46
- return <div />
47
- }
48
-
49
- async function flushEffects(): Promise<void> {
50
- await act(async () => {
51
- await Promise.resolve()
52
- await Promise.resolve()
53
- })
54
- }
55
-
56
- describe('usePlatformAppSettings', () => {
57
- let host: HTMLDivElement
58
- let root: Root
59
-
60
- beforeAll(() => {
61
- ;(globalThis as ReactActGlobal).IS_REACT_ACT_ENVIRONMENT = true
62
- })
63
-
64
- beforeEach(() => {
65
- latest = null
66
- host = document.createElement('div')
67
- document.body.appendChild(host)
68
- root = createRoot(host)
69
- appSettingsMock.getPlatformAppSettings.mockResolvedValue({ view: 'board' })
70
- appSettingsMock.updatePlatformAppSettings.mockResolvedValue({ view: 'list' })
71
- })
72
-
73
- afterEach(() => {
74
- act(() => root.unmount())
75
- host.remove()
76
- vi.clearAllMocks()
77
- })
78
-
79
- it('loads and normalizes app settings for one app slug', async () => {
80
- act(() => {
81
- root.render(<Probe />)
82
- })
83
- await flushEffects()
84
-
85
- expect(appSettingsMock.getPlatformAppSettings).toHaveBeenCalledWith('tasks')
86
- expect(latest?.settings).toEqual({ view: 'board' })
87
- })
88
-
89
- it('patches app settings through settings.app.update', async () => {
90
- act(() => {
91
- root.render(<Probe />)
92
- })
93
- await flushEffects()
94
-
95
- await act(async () => {
96
- await latest?.patchSettings({ view: 'list' })
97
- })
98
-
99
- expect(appSettingsMock.updatePlatformAppSettings).toHaveBeenCalledWith({
100
- appSlug: 'tasks',
101
- patch: { view: 'list' },
102
- })
103
- expect(latest?.settings).toEqual({ view: 'list' })
104
- })
105
- })
1
+ // @vitest-environment happy-dom
2
+ import { act } from 'react'
3
+ import { createRoot, type Root } from 'react-dom/client'
4
+ import {
5
+ afterEach,
6
+ beforeAll,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ it,
11
+ vi,
12
+ } from 'vitest'
13
+ import { usePlatformAppSettings } from './usePlatformAppSettings.js'
14
+
15
+ type ReactActGlobal = typeof globalThis & {
16
+ IS_REACT_ACT_ENVIRONMENT?: boolean
17
+ }
18
+
19
+ type Settings = Record<string, unknown> & {
20
+ view: 'list' | 'board'
21
+ }
22
+
23
+ const appSettingsMock = vi.hoisted(() => ({
24
+ getPlatformAppSettings: vi.fn(),
25
+ updatePlatformAppSettings: vi.fn(),
26
+ }))
27
+
28
+ vi.mock('../appSettings.mjs', () => appSettingsMock)
29
+
30
+ let latest:
31
+ | ReturnType<typeof usePlatformAppSettings<Settings>>
32
+ | null = null
33
+
34
+ function parseSettings(raw: Record<string, unknown>): Settings {
35
+ return {
36
+ view: raw.view === 'board' ? 'board' : 'list',
37
+ }
38
+ }
39
+
40
+ function Probe(): React.ReactElement {
41
+ latest = usePlatformAppSettings<Settings>({
42
+ appSlug: 'tasks',
43
+ parse: parseSettings,
44
+ initialSettings: { view: 'list' },
45
+ })
46
+ return <div />
47
+ }
48
+
49
+ async function flushEffects(): Promise<void> {
50
+ await act(async () => {
51
+ await Promise.resolve()
52
+ await Promise.resolve()
53
+ })
54
+ }
55
+
56
+ describe('usePlatformAppSettings', () => {
57
+ let host: HTMLDivElement
58
+ let root: Root
59
+
60
+ beforeAll(() => {
61
+ ;(globalThis as ReactActGlobal).IS_REACT_ACT_ENVIRONMENT = true
62
+ })
63
+
64
+ beforeEach(() => {
65
+ latest = null
66
+ host = document.createElement('div')
67
+ document.body.appendChild(host)
68
+ root = createRoot(host)
69
+ appSettingsMock.getPlatformAppSettings.mockResolvedValue({ view: 'board' })
70
+ appSettingsMock.updatePlatformAppSettings.mockResolvedValue({ view: 'list' })
71
+ })
72
+
73
+ afterEach(() => {
74
+ act(() => root.unmount())
75
+ host.remove()
76
+ vi.clearAllMocks()
77
+ })
78
+
79
+ it('loads and normalizes app settings for one app slug', async () => {
80
+ act(() => {
81
+ root.render(<Probe />)
82
+ })
83
+ await flushEffects()
84
+
85
+ expect(appSettingsMock.getPlatformAppSettings).toHaveBeenCalledWith('tasks')
86
+ expect(latest?.settings).toEqual({ view: 'board' })
87
+ })
88
+
89
+ it('patches app settings through settings.app.update', async () => {
90
+ act(() => {
91
+ root.render(<Probe />)
92
+ })
93
+ await flushEffects()
94
+
95
+ await act(async () => {
96
+ await latest?.patchSettings({ view: 'list' })
97
+ })
98
+
99
+ expect(appSettingsMock.updatePlatformAppSettings).toHaveBeenCalledWith({
100
+ appSlug: 'tasks',
101
+ patch: { view: 'list' },
102
+ })
103
+ expect(latest?.settings).toEqual({ view: 'list' })
104
+ })
105
+ })
@@ -1,104 +1,104 @@
1
- import { useCallback, useEffect, useState } from 'react'
2
- import {
3
- getPlatformAppSettings,
4
- updatePlatformAppSettings,
5
- } from '../appSettings.mjs'
6
-
7
- export interface UsePlatformAppSettingsOptions<
8
- TSettings extends Record<string, unknown> = Record<string, unknown>,
9
- > {
10
- appSlug: string | null
11
- /** Wait for bridge ready before loading (default: true when appSlug exists). */
12
- enabled?: boolean
13
- /** Normalize stored settings into the app-owned settings shape. */
14
- parse?: (value: Record<string, unknown>) => TSettings
15
- initialSettings?: TSettings
16
- }
17
-
18
- export interface UsePlatformAppSettingsResult<
19
- TSettings extends Record<string, unknown> = Record<string, unknown>,
20
- > {
21
- settings: TSettings
22
- loading: boolean
23
- error: Error | null
24
- /** Shallow-merge a patch through shell `settings.app.update`. */
25
- patchSettings: (patch: Record<string, unknown>) => Promise<TSettings>
26
- reload: () => void
27
- }
28
-
29
- function defaultParse<TSettings extends Record<string, unknown>>(
30
- value: Record<string, unknown>,
31
- ): TSettings {
32
- return value as TSettings
33
- }
34
-
35
- /**
36
- * Load and update the current app's settings through `settings.app.*`.
37
- * App settings are small per-app preferences, not user-visible documents.
38
- */
39
- export function usePlatformAppSettings<
40
- TSettings extends Record<string, unknown> = Record<string, unknown>,
41
- >({
42
- appSlug,
43
- enabled = Boolean(appSlug),
44
- parse = defaultParse<TSettings>,
45
- initialSettings = {} as TSettings,
46
- }: UsePlatformAppSettingsOptions<TSettings>): UsePlatformAppSettingsResult<TSettings> {
47
- const [settings, setSettings] = useState<TSettings>(initialSettings)
48
- const [loading, setLoading] = useState(false)
49
- const [error, setError] = useState<Error | null>(null)
50
- const [reloadToken, setReloadToken] = useState(0)
51
-
52
- const reload = useCallback(() => {
53
- setReloadToken(token => token + 1)
54
- }, [])
55
-
56
- useEffect(() => {
57
- if (!enabled || !appSlug) return
58
-
59
- let cancelled = false
60
- setLoading(true)
61
- setError(null)
62
-
63
- void getPlatformAppSettings(appSlug)
64
- .then(raw => {
65
- if (!cancelled) setSettings(parse(raw))
66
- })
67
- .catch((cause: unknown) => {
68
- if (!cancelled) {
69
- setError(cause instanceof Error ? cause : new Error(String(cause)))
70
- }
71
- })
72
- .finally(() => {
73
- if (!cancelled) setLoading(false)
74
- })
75
-
76
- return () => {
77
- cancelled = true
78
- }
79
- }, [appSlug, enabled, parse, reloadToken])
80
-
81
- const patchSettings = useCallback(
82
- async (patch: Record<string, unknown>): Promise<TSettings> => {
83
- if (!appSlug) throw new Error('appSlug is required to update app settings')
84
- try {
85
- const next = parse(
86
- await updatePlatformAppSettings({
87
- appSlug,
88
- patch,
89
- }),
90
- )
91
- setSettings(next)
92
- setError(null)
93
- return next
94
- } catch (cause) {
95
- const error = cause instanceof Error ? cause : new Error(String(cause))
96
- setError(error)
97
- throw error
98
- }
99
- },
100
- [appSlug, parse],
101
- )
102
-
103
- return { settings, loading, error, patchSettings, reload }
104
- }
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import {
3
+ getPlatformAppSettings,
4
+ updatePlatformAppSettings,
5
+ } from '../appSettings.mjs'
6
+
7
+ export interface UsePlatformAppSettingsOptions<
8
+ TSettings extends Record<string, unknown> = Record<string, unknown>,
9
+ > {
10
+ appSlug: string | null
11
+ /** Wait for bridge ready before loading (default: true when appSlug exists). */
12
+ enabled?: boolean
13
+ /** Normalize stored settings into the app-owned settings shape. */
14
+ parse?: (value: Record<string, unknown>) => TSettings
15
+ initialSettings?: TSettings
16
+ }
17
+
18
+ export interface UsePlatformAppSettingsResult<
19
+ TSettings extends Record<string, unknown> = Record<string, unknown>,
20
+ > {
21
+ settings: TSettings
22
+ loading: boolean
23
+ error: Error | null
24
+ /** Shallow-merge a patch through shell `settings.app.update`. */
25
+ patchSettings: (patch: Record<string, unknown>) => Promise<TSettings>
26
+ reload: () => void
27
+ }
28
+
29
+ function defaultParse<TSettings extends Record<string, unknown>>(
30
+ value: Record<string, unknown>,
31
+ ): TSettings {
32
+ return value as TSettings
33
+ }
34
+
35
+ /**
36
+ * Load and update the current app's settings through `settings.app.*`.
37
+ * App settings are small per-app preferences, not user-visible documents.
38
+ */
39
+ export function usePlatformAppSettings<
40
+ TSettings extends Record<string, unknown> = Record<string, unknown>,
41
+ >({
42
+ appSlug,
43
+ enabled = Boolean(appSlug),
44
+ parse = defaultParse<TSettings>,
45
+ initialSettings = {} as TSettings,
46
+ }: UsePlatformAppSettingsOptions<TSettings>): UsePlatformAppSettingsResult<TSettings> {
47
+ const [settings, setSettings] = useState<TSettings>(initialSettings)
48
+ const [loading, setLoading] = useState(false)
49
+ const [error, setError] = useState<Error | null>(null)
50
+ const [reloadToken, setReloadToken] = useState(0)
51
+
52
+ const reload = useCallback(() => {
53
+ setReloadToken(token => token + 1)
54
+ }, [])
55
+
56
+ useEffect(() => {
57
+ if (!enabled || !appSlug) return
58
+
59
+ let cancelled = false
60
+ setLoading(true)
61
+ setError(null)
62
+
63
+ void getPlatformAppSettings(appSlug)
64
+ .then(raw => {
65
+ if (!cancelled) setSettings(parse(raw))
66
+ })
67
+ .catch((cause: unknown) => {
68
+ if (!cancelled) {
69
+ setError(cause instanceof Error ? cause : new Error(String(cause)))
70
+ }
71
+ })
72
+ .finally(() => {
73
+ if (!cancelled) setLoading(false)
74
+ })
75
+
76
+ return () => {
77
+ cancelled = true
78
+ }
79
+ }, [appSlug, enabled, parse, reloadToken])
80
+
81
+ const patchSettings = useCallback(
82
+ async (patch: Record<string, unknown>): Promise<TSettings> => {
83
+ if (!appSlug) throw new Error('appSlug is required to update app settings')
84
+ try {
85
+ const next = parse(
86
+ await updatePlatformAppSettings({
87
+ appSlug,
88
+ patch,
89
+ }),
90
+ )
91
+ setSettings(next)
92
+ setError(null)
93
+ return next
94
+ } catch (cause) {
95
+ const error = cause instanceof Error ? cause : new Error(String(cause))
96
+ setError(error)
97
+ throw error
98
+ }
99
+ },
100
+ [appSlug, parse],
101
+ )
102
+
103
+ return { settings, loading, error, patchSettings, reload }
104
+ }