@puredesktop/puredesktop-ui-bridge 2.1.7 → 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.
- package/package.json +89 -5
- package/src/agents/mcpServerPresets.test.ts +105 -0
- package/src/agents/mcpServerPresets.ts +380 -0
- package/src/agents/runtime/index.ts +7 -0
- package/src/agents/runtime/mapAgentSnapshot.ts +11 -8
- package/src/agents/runtime/pendingAsks.test.ts +117 -0
- package/src/agents/runtime/pendingAsks.ts +110 -0
- package/src/bridge/agentScopeDefaults.ts +65 -0
- package/src/bridge/agentTypes.ts +57 -3
- package/src/bridge/agents.d.mts +6 -0
- package/src/bridge/agents.mjs +10 -0
- package/src/bridge/agents.ts +4 -0
- package/src/bridge/collectionImagePaste.ts +19 -1
- package/src/bridge/context.mjs +32 -0
- package/src/bridge/credentials.d.mts +62 -0
- package/src/bridge/credentials.mjs +53 -0
- package/src/bridge/documents.d.mts +76 -0
- package/src/bridge/documents.mjs +40 -0
- package/src/bridge/events.d.mts +1 -0
- package/src/bridge/events.mjs +1 -0
- package/src/bridge/googleAuth.ts +7 -152
- package/src/bridge/mcpClient.d.mts +74 -0
- package/src/bridge/mcpClient.mjs +79 -0
- package/src/bridge/methods.d.mts +45 -0
- package/src/bridge/methods.mjs +46 -0
- package/src/bridge/notifications.d.mts +12 -0
- package/src/bridge/notifications.mjs +36 -0
- package/src/bridge/operations.d.mts +44 -0
- package/src/bridge/operations.mjs +40 -0
- package/src/bridge/pureRender/base.css +44 -0
- package/src/bridge/pureRender/document.ts +29 -2
- package/src/bridge/pureRender/index.ts +3 -0
- package/src/bridge/pureRender/normalize.ts +167 -0
- package/src/bridge/pureRender/sanitizeExportBody.test.ts +51 -0
- package/src/bridge/pureRender/sanitizeExportBody.ts +61 -0
- package/src/bridge/pureRender/styles.ts +125 -5
- package/src/bridge/pureRender/theme.test.ts +259 -0
- package/src/bridge/pureRender/theme.ts +19 -1
- package/src/bridge/pureRender/types.ts +33 -0
- package/src/bridge/pureRender/visualLayoutInspection.test.ts +26 -0
- package/src/bridge/pureRender/visualLayoutInspection.ts +75 -0
- package/src/bridge/react/useDocumentHotkeys.ts +41 -0
- package/src/bridge/react/useDocumentLifecycle.tsx +359 -0
- package/src/bridge/react/usePlatformAgentSessionDrive.test.tsx +29 -10
- package/src/bridge/react/usePlatformAgentSessionDrive.tsx +10 -81
- package/src/bridge/react/usePlatformAppSettings.test.tsx +105 -0
- package/src/bridge/react/usePlatformAppSettings.tsx +104 -0
- package/src/bridge/react/usePlatformJsonStore.test.tsx +152 -0
- package/src/bridge/react/usePlatformJsonStore.tsx +149 -0
- package/src/bridge/react/usePlatformOperations.tsx +75 -0
- package/src/bridge/storage.d.mts +23 -10
- package/src/bridge/storage.mjs +6 -3
- package/src/bridge/storage.test.ts +6 -6
- package/src/bridge/types.ts +7 -0
- package/src/components/agents/AgentComposer.tsx +10 -6
- package/src/components/agents/AgentDrawerPanel.test.tsx +2 -205
- package/src/components/agents/AgentDrawerPanel.tsx +23 -62
- package/src/components/agents/AgentMessageBubble.tsx +59 -33
- package/src/components/agents/AgentMessageList.tsx +4 -4
- package/src/components/agents/AgentQuestionPromptList.tsx +34 -212
- package/src/components/agents/AgentToolApprovalActions.tsx +13 -11
- package/src/components/agents/AgentToolCallCard.tsx +20 -20
- package/src/components/agents/AgentToolPendingList.tsx +6 -6
- package/src/components/agents/AgentToolPendingReview.tsx +6 -6
- package/src/components/agents/ContextPicker.tsx +239 -0
- package/src/components/agents/QuestionRequestList.tsx +278 -0
- package/src/components/agents/agentPanelStyles.ts +8 -31
- package/src/components/agents/agentTypes.ts +25 -41
- package/src/components/agents/index.ts +4 -0
- package/src/components/assets/asset-library/sidebar/AssetLibraryList.tsx +10 -4
- package/src/components/chrome/WorkspaceTabStrip.tsx +22 -4
- package/src/components/chrome/workspaceTabTypes.ts +2 -0
- package/src/components/common/chat/ChatBox.tsx +9 -9
- package/src/components/common/chat/ChatModelMenu.tsx +34 -4
- package/src/components/common/chat/ChatThread.tsx +23 -7
- package/src/components/common/connections/ProviderConnection.test.tsx +208 -0
- package/src/components/common/connections/ProviderConnection.tsx +277 -0
- package/src/components/common/connections/index.ts +5 -0
- package/src/components/common/containers/AppFrame.test.tsx +4 -4
- package/src/components/common/containers/AppFrame.tsx +26 -10
- package/src/components/common/containers/AppHeader.tsx +37 -15
- package/src/components/common/documents/DocumentHeaderActions.tsx +206 -0
- package/src/components/common/documents/DocumentSwitcher.tsx +1181 -0
- package/src/components/common/documents/index.ts +8 -0
- package/src/components/common/dropdown/MenuDropdownItem.tsx +14 -2
- package/src/components/common/dropdown/MenuPortal.tsx +3 -1
- package/src/components/common/dropdown/menuViewportPosition.test.ts +20 -10
- package/src/components/common/dropdown/menuViewportPosition.ts +35 -27
- package/src/components/common/overlays/Modal.tsx +13 -2
- package/src/components/common/research/EvidenceDossier.tsx +1232 -150
- package/src/components/common/research/index.ts +2 -0
- package/src/components/editor/EditorLinkPromptDialog.tsx +1 -1
- package/src/components/print-design/PrintDesignPanel.test.tsx +430 -40
- package/src/components/print-design/PrintDesignPanel.tsx +1770 -207
- package/src/components/print-design/index.ts +2 -0
- package/src/components/print-design/previewContent.test.ts +32 -0
- package/src/components/print-design/previewContent.ts +31 -0
- package/src/context/buildContextDocument.test.ts +240 -0
- package/src/context/buildContextDocument.ts +309 -0
- package/src/context/contextAccess.ts +66 -0
- package/src/context/contextConfig.ts +72 -0
- package/src/context/contextDocument.test.ts +155 -0
- package/src/context/contextDocument.ts +300 -0
- package/src/context/contextSections.test.ts +88 -0
- package/src/context/contextSections.ts +84 -0
- package/src/context/htmlToContextMarkdown.test.ts +134 -0
- package/src/context/htmlToContextMarkdown.ts +369 -0
- package/src/ics/generateIcs.test.ts +153 -0
- package/src/ics/generateIcs.ts +197 -0
- package/src/index.ts +3 -0
- package/src/net/httpRetry.test.ts +117 -0
- package/src/net/httpRetry.ts +111 -0
- package/src/theme/appAccents.test.ts +36 -35
- package/src/theme/appAccents.ts +93 -104
- package/src/theme/appIdentityCss.mjs +178 -229
- package/src/theme/appIdentityCss.ts +222 -231
- package/src/theme/themes/dark.ts +3 -1
- package/src/theme/themes/light.ts +3 -1
- package/src/bridge/googleProviderConfig.ts +0 -73
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
})
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -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
|
+
}
|
package/src/bridge/storage.d.mts
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
|
-
export type PlatformStorageJsonFileName = `${string}.json`
|
|
2
|
-
|
|
3
|
-
export interface PlatformStorageJsonRequest {
|
|
4
|
-
appSlug: string
|
|
5
|
-
fileName: PlatformStorageJsonFileName
|
|
6
|
-
}
|
|
1
|
+
export type PlatformStorageJsonFileName = `${string}.json`
|
|
2
|
+
|
|
3
|
+
export interface PlatformStorageJsonRequest {
|
|
4
|
+
appSlug: string
|
|
5
|
+
fileName: PlatformStorageJsonFileName
|
|
6
|
+
}
|
|
7
7
|
|
|
8
8
|
export interface PlatformStorageJsonReadResult {
|
|
9
9
|
path: string
|
|
10
10
|
value: unknown | null
|
|
11
|
+
/** Content hash for optimistic concurrency (null when the file is absent). */
|
|
12
|
+
version: string | null
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export interface PlatformStorageJsonWriteRequest
|
|
14
16
|
extends PlatformStorageJsonRequest {
|
|
15
17
|
value: unknown
|
|
18
|
+
/**
|
|
19
|
+
* The version this write was derived from (null = file must not exist).
|
|
20
|
+
* Omit for an unconditional write. On mismatch nothing is written and the
|
|
21
|
+
* result carries conflict: true — re-read, re-apply, retry.
|
|
22
|
+
*/
|
|
23
|
+
ifMatch?: string | null
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
export
|
|
19
|
-
path: string
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
export type PlatformStorageJsonWriteResult =
|
|
27
|
+
| { path: string; ok: true; version: string }
|
|
28
|
+
| {
|
|
29
|
+
path: string
|
|
30
|
+
ok: false
|
|
31
|
+
conflict: true
|
|
32
|
+
value: unknown | null
|
|
33
|
+
version: string | null
|
|
34
|
+
}
|
|
22
35
|
|
|
23
36
|
export declare function readPlatformStorageJson(
|
|
24
37
|
request: PlatformStorageJsonRequest,
|
package/src/bridge/storage.mjs
CHANGED
|
@@ -3,15 +3,18 @@ import { PLATFORM_BRIDGE_METHODS } from './methods.mjs'
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @param {{ appSlug: string, fileName: string }} request
|
|
6
|
-
* @returns {Promise<{ path: string, value: unknown | null }>}
|
|
6
|
+
* @returns {Promise<{ path: string, value: unknown | null, version: string | null }>}
|
|
7
7
|
*/
|
|
8
8
|
export async function readPlatformStorageJson(request) {
|
|
9
9
|
return bridge.call(PLATFORM_BRIDGE_METHODS.STORAGE_READ_JSON, [request])
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Optional `ifMatch` (version from readPlatformStorageJson) makes the write
|
|
14
|
+
* conditional: on conflict nothing is written and the result has
|
|
15
|
+
* `conflict: true` with the current value — re-read, re-apply, retry.
|
|
16
|
+
* @param {{ appSlug: string, fileName: string, value: unknown, ifMatch?: string | null }} request
|
|
17
|
+
* @returns {Promise<{ path: string, ok: boolean, version: string | null, conflict?: true, value?: unknown | null }>}
|
|
15
18
|
*/
|
|
16
19
|
export async function writePlatformStorageJson(request) {
|
|
17
20
|
return bridge.call(PLATFORM_BRIDGE_METHODS.STORAGE_WRITE_JSON, [request])
|
|
@@ -20,7 +20,7 @@ beforeEach(() => {
|
|
|
20
20
|
|
|
21
21
|
describe('storage bridge helpers', () => {
|
|
22
22
|
it('reads JSON storage using the shell request object shape', async () => {
|
|
23
|
-
const request = { appSlug: 'puretasks', fileName: 'tasks.json' } as const
|
|
23
|
+
const request = { appSlug: 'puretasks', fileName: 'tasks.json' } as const
|
|
24
24
|
bridgeCall.mockResolvedValue({ path: '/tmp/tasks.json', value: null })
|
|
25
25
|
|
|
26
26
|
await readPlatformStorageJson(request)
|
|
@@ -32,11 +32,11 @@ describe('storage bridge helpers', () => {
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
it('writes JSON storage using the shell request object shape', async () => {
|
|
35
|
-
const request = {
|
|
36
|
-
appSlug: 'puretasks',
|
|
37
|
-
fileName: 'tasks.json',
|
|
38
|
-
value: { tasks: [] },
|
|
39
|
-
} as const
|
|
35
|
+
const request = {
|
|
36
|
+
appSlug: 'puretasks',
|
|
37
|
+
fileName: 'tasks.json',
|
|
38
|
+
value: { tasks: [] },
|
|
39
|
+
} as const
|
|
40
40
|
bridgeCall.mockResolvedValue({ path: '/tmp/tasks.json', ok: true })
|
|
41
41
|
|
|
42
42
|
await writePlatformStorageJson(request)
|
package/src/bridge/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type PlatformBridgeEventName =
|
|
|
2
2
|
import('./events.mjs').PlatformBridgeEventName
|
|
3
3
|
|
|
4
4
|
import type { PlatformThemeMode } from '../theme/tokens.js'
|
|
5
|
+
import type { PlatformAppContextManifest } from '../context/contextConfig.js'
|
|
5
6
|
|
|
6
7
|
/** Shell prefs exposed via `settings.prefs.get` / `settings.prefs.set`. */
|
|
7
8
|
export type PlatformThemePreference = PlatformThemeMode
|
|
@@ -80,6 +81,8 @@ export interface PlatformAppManifest {
|
|
|
80
81
|
productName: string
|
|
81
82
|
kind: string
|
|
82
83
|
description: string
|
|
84
|
+
/** Declarative context-file config for this app's package objects. */
|
|
85
|
+
context?: PlatformAppContextManifest
|
|
83
86
|
peopleProviders?: Array<{
|
|
84
87
|
id: string
|
|
85
88
|
label: string
|
|
@@ -205,6 +208,10 @@ export interface RenderPagedSnapshotMetrics {
|
|
|
205
208
|
captionVisible?: boolean
|
|
206
209
|
captionOrphaned?: boolean
|
|
207
210
|
strandedHeading?: boolean | null
|
|
211
|
+
/** Blocks on the target page that collide with each other. */
|
|
212
|
+
overlappingBlocks?: number
|
|
213
|
+
/** Blocks escaping the page content box. */
|
|
214
|
+
overflowingBlocks?: number
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
export interface RenderPagedPreviewResult {
|
|
@@ -20,6 +20,7 @@ export interface AgentComposerProps {
|
|
|
20
20
|
leading?: React.ReactNode
|
|
21
21
|
followUps?: AgentUiFollowUp[]
|
|
22
22
|
questionToolCalls?: AgentUiToolCall[]
|
|
23
|
+
questionPromptList?: React.ReactNode
|
|
23
24
|
usage?: AgentUiRunUsage | null
|
|
24
25
|
liveTurn?: AgentLiveTurn | null
|
|
25
26
|
onChange: (value: string) => void
|
|
@@ -76,6 +77,7 @@ export function AgentComposer({
|
|
|
76
77
|
leading,
|
|
77
78
|
followUps = [],
|
|
78
79
|
questionToolCalls = [],
|
|
80
|
+
questionPromptList = null,
|
|
79
81
|
usage = null,
|
|
80
82
|
liveTurn = null,
|
|
81
83
|
onChange,
|
|
@@ -89,7 +91,7 @@ export function AgentComposer({
|
|
|
89
91
|
const showUsage =
|
|
90
92
|
liveTurn?.contextCompaction === 'summarizing' ||
|
|
91
93
|
Boolean(usage && (usage.totalTokens > 0 || usage.contextFillTokens))
|
|
92
|
-
const hasQuestions = questionToolCalls.length > 0
|
|
94
|
+
const hasQuestions = questionPromptList !== null || questionToolCalls.length > 0
|
|
93
95
|
|
|
94
96
|
return (
|
|
95
97
|
<StyledRoot>
|
|
@@ -105,11 +107,13 @@ export function AgentComposer({
|
|
|
105
107
|
preserveChildrenOnExit
|
|
106
108
|
aria-hidden={!hasQuestions}
|
|
107
109
|
>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
{questionPromptList ?? (
|
|
111
|
+
<AgentQuestionPromptList
|
|
112
|
+
toolCalls={questionToolCalls}
|
|
113
|
+
onAnswer={onAnswerQuestionTool}
|
|
114
|
+
onDecline={onDeclineQuestionTool}
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
113
117
|
</StyledQuestionPresence>
|
|
114
118
|
<StyledComposerSurfaceAnchor>
|
|
115
119
|
{showUsage ? (
|