@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,152 +1,152 @@
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 { usePlatformJsonStore } from './usePlatformJsonStore.js'
14
-
15
- type ReactActGlobal = typeof globalThis & {
16
- IS_REACT_ACT_ENVIRONMENT?: boolean
17
- }
18
-
19
- interface Store {
20
- items: string[]
21
- }
22
-
23
- const storageMock = vi.hoisted(() => ({
24
- readPlatformStorageJson: vi.fn(),
25
- writePlatformStorageJson: vi.fn(),
26
- }))
27
-
28
- vi.mock('../storage.mjs', () => storageMock)
29
-
30
- let latest: ReturnType<typeof usePlatformJsonStore<Store>> | null = null
31
-
32
- const EMPTY_STORE: Store = { items: [] }
33
-
34
- function parseStore(value: unknown): Store {
35
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
36
- return EMPTY_STORE
37
- }
38
- const record = value as Record<string, unknown>
39
- return {
40
- items: Array.isArray(record.items)
41
- ? record.items.filter((item): item is string => typeof item === 'string')
42
- : [],
43
- }
44
- }
45
-
46
- function Probe(): React.ReactElement {
47
- latest = usePlatformJsonStore<Store>({
48
- appSlug: 'tasks',
49
- fileName: 'tasks-items.json',
50
- initialValue: EMPTY_STORE,
51
- parse: parseStore,
52
- })
53
- return <div />
54
- }
55
-
56
- async function flushEffects(): Promise<void> {
57
- await act(async () => {
58
- await Promise.resolve()
59
- await Promise.resolve()
60
- })
61
- }
62
-
63
- describe('usePlatformJsonStore', () => {
64
- let host: HTMLDivElement
65
- let root: Root
66
-
67
- beforeAll(() => {
68
- ;(globalThis as ReactActGlobal).IS_REACT_ACT_ENVIRONMENT = true
69
- })
70
-
71
- beforeEach(() => {
72
- latest = null
73
- host = document.createElement('div')
74
- document.body.appendChild(host)
75
- root = createRoot(host)
76
- storageMock.readPlatformStorageJson.mockResolvedValue({
77
- path: '/data/tasks-items.json',
78
- value: { items: ['one', 2, 'two'] },
79
- })
80
- storageMock.writePlatformStorageJson.mockResolvedValue({
81
- path: '/data/tasks-items.json',
82
- ok: true,
83
- })
84
- })
85
-
86
- afterEach(() => {
87
- act(() => root.unmount())
88
- host.remove()
89
- vi.clearAllMocks()
90
- })
91
-
92
- it('loads and normalizes a JSON store file', async () => {
93
- act(() => {
94
- root.render(<Probe />)
95
- })
96
- await flushEffects()
97
-
98
- expect(storageMock.readPlatformStorageJson).toHaveBeenCalledWith({
99
- appSlug: 'tasks',
100
- fileName: 'tasks-items.json',
101
- })
102
- expect(latest?.value).toEqual({ items: ['one', 'two'] })
103
- expect(latest?.path).toBe('/data/tasks-items.json')
104
- })
105
-
106
- it('saves the complete next JSON store value', async () => {
107
- act(() => {
108
- root.render(<Probe />)
109
- })
110
- await flushEffects()
111
-
112
- await act(async () => {
113
- await latest?.saveValue({ items: ['next'] })
114
- })
115
-
116
- expect(storageMock.writePlatformStorageJson).toHaveBeenCalledWith({
117
- appSlug: 'tasks',
118
- fileName: 'tasks-items.json',
119
- value: { items: ['next'] },
120
- })
121
- expect(latest?.value).toEqual({ items: ['next'] })
122
- })
123
-
124
- it('applies queued updates to the latest saved value', async () => {
125
- act(() => {
126
- root.render(<Probe />)
127
- })
128
- await flushEffects()
129
-
130
- await act(async () => {
131
- const first = latest?.updateValue(current => ({
132
- items: [...current.items, 'three'],
133
- }))
134
- const second = latest?.updateValue(current => ({
135
- items: [...current.items, 'four'],
136
- }))
137
- await Promise.all([first, second])
138
- })
139
-
140
- expect(storageMock.writePlatformStorageJson).toHaveBeenNthCalledWith(1, {
141
- appSlug: 'tasks',
142
- fileName: 'tasks-items.json',
143
- value: { items: ['one', 'two', 'three'] },
144
- })
145
- expect(storageMock.writePlatformStorageJson).toHaveBeenNthCalledWith(2, {
146
- appSlug: 'tasks',
147
- fileName: 'tasks-items.json',
148
- value: { items: ['one', 'two', 'three', 'four'] },
149
- })
150
- expect(latest?.value).toEqual({ items: ['one', 'two', 'three', 'four'] })
151
- })
152
- })
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 { usePlatformJsonStore } from './usePlatformJsonStore.js'
14
+
15
+ type ReactActGlobal = typeof globalThis & {
16
+ IS_REACT_ACT_ENVIRONMENT?: boolean
17
+ }
18
+
19
+ interface Store {
20
+ items: string[]
21
+ }
22
+
23
+ const storageMock = vi.hoisted(() => ({
24
+ readPlatformStorageJson: vi.fn(),
25
+ writePlatformStorageJson: vi.fn(),
26
+ }))
27
+
28
+ vi.mock('../storage.mjs', () => storageMock)
29
+
30
+ let latest: ReturnType<typeof usePlatformJsonStore<Store>> | null = null
31
+
32
+ const EMPTY_STORE: Store = { items: [] }
33
+
34
+ function parseStore(value: unknown): Store {
35
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
36
+ return EMPTY_STORE
37
+ }
38
+ const record = value as Record<string, unknown>
39
+ return {
40
+ items: Array.isArray(record.items)
41
+ ? record.items.filter((item): item is string => typeof item === 'string')
42
+ : [],
43
+ }
44
+ }
45
+
46
+ function Probe(): React.ReactElement {
47
+ latest = usePlatformJsonStore<Store>({
48
+ appSlug: 'tasks',
49
+ fileName: 'tasks-items.json',
50
+ initialValue: EMPTY_STORE,
51
+ parse: parseStore,
52
+ })
53
+ return <div />
54
+ }
55
+
56
+ async function flushEffects(): Promise<void> {
57
+ await act(async () => {
58
+ await Promise.resolve()
59
+ await Promise.resolve()
60
+ })
61
+ }
62
+
63
+ describe('usePlatformJsonStore', () => {
64
+ let host: HTMLDivElement
65
+ let root: Root
66
+
67
+ beforeAll(() => {
68
+ ;(globalThis as ReactActGlobal).IS_REACT_ACT_ENVIRONMENT = true
69
+ })
70
+
71
+ beforeEach(() => {
72
+ latest = null
73
+ host = document.createElement('div')
74
+ document.body.appendChild(host)
75
+ root = createRoot(host)
76
+ storageMock.readPlatformStorageJson.mockResolvedValue({
77
+ path: '/data/tasks-items.json',
78
+ value: { items: ['one', 2, 'two'] },
79
+ })
80
+ storageMock.writePlatformStorageJson.mockResolvedValue({
81
+ path: '/data/tasks-items.json',
82
+ ok: true,
83
+ })
84
+ })
85
+
86
+ afterEach(() => {
87
+ act(() => root.unmount())
88
+ host.remove()
89
+ vi.clearAllMocks()
90
+ })
91
+
92
+ it('loads and normalizes a JSON store file', async () => {
93
+ act(() => {
94
+ root.render(<Probe />)
95
+ })
96
+ await flushEffects()
97
+
98
+ expect(storageMock.readPlatformStorageJson).toHaveBeenCalledWith({
99
+ appSlug: 'tasks',
100
+ fileName: 'tasks-items.json',
101
+ })
102
+ expect(latest?.value).toEqual({ items: ['one', 'two'] })
103
+ expect(latest?.path).toBe('/data/tasks-items.json')
104
+ })
105
+
106
+ it('saves the complete next JSON store value', async () => {
107
+ act(() => {
108
+ root.render(<Probe />)
109
+ })
110
+ await flushEffects()
111
+
112
+ await act(async () => {
113
+ await latest?.saveValue({ items: ['next'] })
114
+ })
115
+
116
+ expect(storageMock.writePlatformStorageJson).toHaveBeenCalledWith({
117
+ appSlug: 'tasks',
118
+ fileName: 'tasks-items.json',
119
+ value: { items: ['next'] },
120
+ })
121
+ expect(latest?.value).toEqual({ items: ['next'] })
122
+ })
123
+
124
+ it('applies queued updates to the latest saved value', async () => {
125
+ act(() => {
126
+ root.render(<Probe />)
127
+ })
128
+ await flushEffects()
129
+
130
+ await act(async () => {
131
+ const first = latest?.updateValue(current => ({
132
+ items: [...current.items, 'three'],
133
+ }))
134
+ const second = latest?.updateValue(current => ({
135
+ items: [...current.items, 'four'],
136
+ }))
137
+ await Promise.all([first, second])
138
+ })
139
+
140
+ expect(storageMock.writePlatformStorageJson).toHaveBeenNthCalledWith(1, {
141
+ appSlug: 'tasks',
142
+ fileName: 'tasks-items.json',
143
+ value: { items: ['one', 'two', 'three'] },
144
+ })
145
+ expect(storageMock.writePlatformStorageJson).toHaveBeenNthCalledWith(2, {
146
+ appSlug: 'tasks',
147
+ fileName: 'tasks-items.json',
148
+ value: { items: ['one', 'two', 'three', 'four'] },
149
+ })
150
+ expect(latest?.value).toEqual({ items: ['one', 'two', 'three', 'four'] })
151
+ })
152
+ })
@@ -1,149 +1,149 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react'
2
- import {
3
- readPlatformStorageJson,
4
- writePlatformStorageJson,
5
- } from '../storage.mjs'
6
-
7
- export type PlatformJsonStoreFileName = `${string}.json`
8
-
9
- export interface UsePlatformJsonStoreOptions<TValue> {
10
- appSlug: string | null
11
- fileName: PlatformJsonStoreFileName | null
12
- /** Default value when the JSON file does not exist yet. */
13
- initialValue: TValue
14
- /** Wait for bridge ready before loading (default: true when appSlug/fileName exist). */
15
- enabled?: boolean
16
- /** Normalize parsed JSON into the app-owned store shape. */
17
- parse?: (value: unknown) => TValue
18
- }
19
-
20
- export interface UsePlatformJsonStoreResult<TValue> {
21
- value: TValue
22
- path: string | null
23
- loading: boolean
24
- saving: boolean
25
- error: Error | null
26
- saveValue: (next: TValue) => Promise<TValue>
27
- updateValue: (updater: (current: TValue) => TValue) => Promise<TValue>
28
- reload: () => void
29
- }
30
-
31
- function defaultParse<TValue>(value: unknown): TValue {
32
- return value as TValue
33
- }
34
-
35
- /**
36
- * Load and replace one shell-managed JSON store file through `storage.*`.
37
- * Use this for app-owned internal records, not for user-visible documents.
38
- */
39
- export function usePlatformJsonStore<TValue>({
40
- appSlug,
41
- fileName,
42
- initialValue,
43
- enabled = Boolean(appSlug && fileName),
44
- parse = defaultParse<TValue>,
45
- }: UsePlatformJsonStoreOptions<TValue>): UsePlatformJsonStoreResult<TValue> {
46
- const [value, setValue] = useState<TValue>(initialValue)
47
- const [path, setPath] = useState<string | null>(null)
48
- const [loading, setLoading] = useState(false)
49
- const [saving, setSaving] = useState(false)
50
- const [error, setError] = useState<Error | null>(null)
51
- const [reloadToken, setReloadToken] = useState(0)
52
- const valueRef = useRef(value)
53
- const writeChainRef = useRef<Promise<TValue>>(Promise.resolve(value))
54
- valueRef.current = value
55
-
56
- const reload = useCallback(() => {
57
- setReloadToken(token => token + 1)
58
- }, [])
59
-
60
- useEffect(() => {
61
- if (!enabled || !appSlug || !fileName) return
62
-
63
- let cancelled = false
64
- setLoading(true)
65
- setError(null)
66
-
67
- void readPlatformStorageJson({ appSlug, fileName })
68
- .then(result => {
69
- if (cancelled) return
70
- const next =
71
- result.value === null ? initialValue : parse(result.value)
72
- valueRef.current = next
73
- setValue(next)
74
- setPath(result.path)
75
- })
76
- .catch((cause: unknown) => {
77
- if (!cancelled) {
78
- setError(cause instanceof Error ? cause : new Error(String(cause)))
79
- }
80
- })
81
- .finally(() => {
82
- if (!cancelled) setLoading(false)
83
- })
84
-
85
- return () => {
86
- cancelled = true
87
- }
88
- }, [appSlug, enabled, fileName, initialValue, parse, reloadToken])
89
-
90
- const queueSave = useCallback(
91
- (resolveNext: (current: TValue) => TValue): Promise<TValue> => {
92
- if (!appSlug || !fileName) {
93
- throw new Error('appSlug and fileName are required to save JSON storage')
94
- }
95
-
96
- const write = async (): Promise<TValue> => {
97
- const next = resolveNext(valueRef.current)
98
- setSaving(true)
99
- try {
100
- const result = await writePlatformStorageJson({
101
- appSlug,
102
- fileName,
103
- value: next,
104
- })
105
- valueRef.current = next
106
- setValue(next)
107
- setPath(result.path)
108
- setError(null)
109
- return next
110
- } catch (cause) {
111
- const error =
112
- cause instanceof Error ? cause : new Error(String(cause))
113
- setError(error)
114
- throw error
115
- } finally {
116
- setSaving(false)
117
- }
118
- }
119
-
120
- const queued = writeChainRef.current.then(write, write)
121
- writeChainRef.current = queued.catch(() => valueRef.current)
122
- return queued
123
- },
124
- [appSlug, fileName],
125
- )
126
-
127
- const saveValue = useCallback(
128
- (next: TValue): Promise<TValue> => queueSave(() => next),
129
- [queueSave],
130
- )
131
-
132
- const updateValue = useCallback(
133
- (updater: (current: TValue) => TValue): Promise<TValue> => {
134
- return queueSave(updater)
135
- },
136
- [queueSave],
137
- )
138
-
139
- return {
140
- value,
141
- path,
142
- loading,
143
- saving,
144
- error,
145
- saveValue,
146
- updateValue,
147
- reload,
148
- }
149
- }
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+ import {
3
+ readPlatformStorageJson,
4
+ writePlatformStorageJson,
5
+ } from '../storage.mjs'
6
+
7
+ export type PlatformJsonStoreFileName = `${string}.json`
8
+
9
+ export interface UsePlatformJsonStoreOptions<TValue> {
10
+ appSlug: string | null
11
+ fileName: PlatformJsonStoreFileName | null
12
+ /** Default value when the JSON file does not exist yet. */
13
+ initialValue: TValue
14
+ /** Wait for bridge ready before loading (default: true when appSlug/fileName exist). */
15
+ enabled?: boolean
16
+ /** Normalize parsed JSON into the app-owned store shape. */
17
+ parse?: (value: unknown) => TValue
18
+ }
19
+
20
+ export interface UsePlatformJsonStoreResult<TValue> {
21
+ value: TValue
22
+ path: string | null
23
+ loading: boolean
24
+ saving: boolean
25
+ error: Error | null
26
+ saveValue: (next: TValue) => Promise<TValue>
27
+ updateValue: (updater: (current: TValue) => TValue) => Promise<TValue>
28
+ reload: () => void
29
+ }
30
+
31
+ function defaultParse<TValue>(value: unknown): TValue {
32
+ return value as TValue
33
+ }
34
+
35
+ /**
36
+ * Load and replace one shell-managed JSON store file through `storage.*`.
37
+ * Use this for app-owned internal records, not for user-visible documents.
38
+ */
39
+ export function usePlatformJsonStore<TValue>({
40
+ appSlug,
41
+ fileName,
42
+ initialValue,
43
+ enabled = Boolean(appSlug && fileName),
44
+ parse = defaultParse<TValue>,
45
+ }: UsePlatformJsonStoreOptions<TValue>): UsePlatformJsonStoreResult<TValue> {
46
+ const [value, setValue] = useState<TValue>(initialValue)
47
+ const [path, setPath] = useState<string | null>(null)
48
+ const [loading, setLoading] = useState(false)
49
+ const [saving, setSaving] = useState(false)
50
+ const [error, setError] = useState<Error | null>(null)
51
+ const [reloadToken, setReloadToken] = useState(0)
52
+ const valueRef = useRef(value)
53
+ const writeChainRef = useRef<Promise<TValue>>(Promise.resolve(value))
54
+ valueRef.current = value
55
+
56
+ const reload = useCallback(() => {
57
+ setReloadToken(token => token + 1)
58
+ }, [])
59
+
60
+ useEffect(() => {
61
+ if (!enabled || !appSlug || !fileName) return
62
+
63
+ let cancelled = false
64
+ setLoading(true)
65
+ setError(null)
66
+
67
+ void readPlatformStorageJson({ appSlug, fileName })
68
+ .then(result => {
69
+ if (cancelled) return
70
+ const next =
71
+ result.value === null ? initialValue : parse(result.value)
72
+ valueRef.current = next
73
+ setValue(next)
74
+ setPath(result.path)
75
+ })
76
+ .catch((cause: unknown) => {
77
+ if (!cancelled) {
78
+ setError(cause instanceof Error ? cause : new Error(String(cause)))
79
+ }
80
+ })
81
+ .finally(() => {
82
+ if (!cancelled) setLoading(false)
83
+ })
84
+
85
+ return () => {
86
+ cancelled = true
87
+ }
88
+ }, [appSlug, enabled, fileName, initialValue, parse, reloadToken])
89
+
90
+ const queueSave = useCallback(
91
+ (resolveNext: (current: TValue) => TValue): Promise<TValue> => {
92
+ if (!appSlug || !fileName) {
93
+ throw new Error('appSlug and fileName are required to save JSON storage')
94
+ }
95
+
96
+ const write = async (): Promise<TValue> => {
97
+ const next = resolveNext(valueRef.current)
98
+ setSaving(true)
99
+ try {
100
+ const result = await writePlatformStorageJson({
101
+ appSlug,
102
+ fileName,
103
+ value: next,
104
+ })
105
+ valueRef.current = next
106
+ setValue(next)
107
+ setPath(result.path)
108
+ setError(null)
109
+ return next
110
+ } catch (cause) {
111
+ const error =
112
+ cause instanceof Error ? cause : new Error(String(cause))
113
+ setError(error)
114
+ throw error
115
+ } finally {
116
+ setSaving(false)
117
+ }
118
+ }
119
+
120
+ const queued = writeChainRef.current.then(write, write)
121
+ writeChainRef.current = queued.catch(() => valueRef.current)
122
+ return queued
123
+ },
124
+ [appSlug, fileName],
125
+ )
126
+
127
+ const saveValue = useCallback(
128
+ (next: TValue): Promise<TValue> => queueSave(() => next),
129
+ [queueSave],
130
+ )
131
+
132
+ const updateValue = useCallback(
133
+ (updater: (current: TValue) => TValue): Promise<TValue> => {
134
+ return queueSave(updater)
135
+ },
136
+ [queueSave],
137
+ )
138
+
139
+ return {
140
+ value,
141
+ path,
142
+ loading,
143
+ saving,
144
+ error,
145
+ saveValue,
146
+ updateValue,
147
+ reload,
148
+ }
149
+ }
@@ -0,0 +1,75 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+ import {
3
+ listPlatformOperations,
4
+ onPlatformOperationRecorded,
5
+ } from '../operations.mjs'
6
+ import type {
7
+ PlatformOperation,
8
+ PlatformOperationsListQuery,
9
+ } from '../operations.d.mts'
10
+
11
+ export interface UsePlatformOperationsResult {
12
+ /** Newest first; live — appends arrive via the operations.changed event. */
13
+ operations: PlatformOperation[]
14
+ /** True until the first list round-trip settles. */
15
+ loading: boolean
16
+ /** Re-fetch from the shell (the live stream normally makes this unnecessary). */
17
+ refresh: () => Promise<void>
18
+ }
19
+
20
+ /**
21
+ * The suite-wide operations feed for app UIs: one initial list, then live
22
+ * appends. Pass `lane` to follow a single lane ('user' | 'agent'); omit for
23
+ * both. Outside the desktop iframe (standalone dev) it settles to an empty
24
+ * feed instead of throwing.
25
+ */
26
+ export function usePlatformOperations(
27
+ query: PlatformOperationsListQuery = {},
28
+ ): UsePlatformOperationsResult {
29
+ const [operations, setOperations] = useState<PlatformOperation[]>([])
30
+ const [loading, setLoading] = useState(true)
31
+ const laneRef = useRef(query.lane)
32
+ laneRef.current = query.lane
33
+ const limitRef = useRef(query.limit)
34
+ limitRef.current = query.limit
35
+
36
+ const refresh = useCallback(async (): Promise<void> => {
37
+ try {
38
+ const result = await listPlatformOperations({
39
+ ...(laneRef.current ? { lane: laneRef.current } : {}),
40
+ ...(limitRef.current !== undefined ? { limit: limitRef.current } : {}),
41
+ })
42
+ setOperations(result.operations)
43
+ } catch {
44
+ // Standalone dev / bridge unavailable: an empty feed, not a crash.
45
+ setOperations([])
46
+ } finally {
47
+ setLoading(false)
48
+ }
49
+ }, [])
50
+
51
+ useEffect(() => {
52
+ void refresh()
53
+ }, [refresh, query.lane, query.limit])
54
+
55
+ useEffect(() => {
56
+ let unsubscribe: (() => void) | undefined
57
+ try {
58
+ unsubscribe = onPlatformOperationRecorded(operation => {
59
+ if (laneRef.current && operation.lane !== laneRef.current) return
60
+ setOperations(current => {
61
+ const next = [operation, ...current]
62
+ const limit = limitRef.current
63
+ return typeof limit === 'number' && limit >= 0
64
+ ? next.slice(0, limit)
65
+ : next
66
+ })
67
+ })
68
+ } catch {
69
+ // No bridge (standalone dev) — the initial refresh already settled.
70
+ }
71
+ return () => unsubscribe?.()
72
+ }, [])
73
+
74
+ return { operations, loading, refresh }
75
+ }