@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,359 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
autosavePlatformDocument,
|
|
4
|
+
createPlatformDraft,
|
|
5
|
+
duplicatePlatformDocument,
|
|
6
|
+
promotePlatformDocument,
|
|
7
|
+
renamePlatformDocument,
|
|
8
|
+
touchPlatformRecentDocument,
|
|
9
|
+
type PlatformDocumentFilePayload,
|
|
10
|
+
} from '../documents.mjs'
|
|
11
|
+
import { bridge } from '../client.mjs'
|
|
12
|
+
import { PLATFORM_BRIDGE_METHODS } from '../methods.mjs'
|
|
13
|
+
|
|
14
|
+
export type DocumentLifecycleStatus = 'none' | 'draft' | 'filed'
|
|
15
|
+
|
|
16
|
+
export interface DocumentLifecycleDoc {
|
|
17
|
+
path: string | null
|
|
18
|
+
title: string
|
|
19
|
+
status: DocumentLifecycleStatus
|
|
20
|
+
savedAt: string | null
|
|
21
|
+
saving: boolean
|
|
22
|
+
error: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UseDocumentLifecycleOptions {
|
|
26
|
+
appSlug: string
|
|
27
|
+
/** Document suffix, e.g. `.research`. */
|
|
28
|
+
suffix: string
|
|
29
|
+
kind: 'package' | 'file'
|
|
30
|
+
/** Called at flush time; returns the document's current file contents. */
|
|
31
|
+
serialize: () => PlatformDocumentFilePayload[]
|
|
32
|
+
/** Prefill for the naming dialog (e.g. an AI-generated title). */
|
|
33
|
+
suggestedTitle?: string
|
|
34
|
+
debounceMs?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DocumentLifecycle {
|
|
38
|
+
doc: DocumentLifecycleDoc
|
|
39
|
+
/** Create the durable draft if none exists yet; returns the path. */
|
|
40
|
+
ensureDraft: () => Promise<string | null>
|
|
41
|
+
/** Note a content change: lazily creates the draft, debounces an autosave. */
|
|
42
|
+
markDirty: () => void
|
|
43
|
+
/** Immediate save of the current serialize() output. */
|
|
44
|
+
flush: () => Promise<void>
|
|
45
|
+
/** Name + file the document (move out of drafts). */
|
|
46
|
+
promote: (title: string, targetDir?: string) => Promise<void>
|
|
47
|
+
rename: (title: string) => Promise<void>
|
|
48
|
+
duplicate: () => Promise<string | null>
|
|
49
|
+
revealInFinder: () => Promise<void>
|
|
50
|
+
openInFiles: () => Promise<void>
|
|
51
|
+
/** Bind an existing on-disk document (opened from PureFiles/recents). */
|
|
52
|
+
adopt: (path: string, opts?: { title?: string }) => void
|
|
53
|
+
/** Detach — back to `none` (⌘N / new document). */
|
|
54
|
+
reset: () => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function titleFromPath(path: string, suffix: string): string {
|
|
58
|
+
const name = path.split('/').pop() ?? path
|
|
59
|
+
return name.endsWith(suffix) ? name.slice(0, -suffix.length) : name
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function statusForPath(path: string): DocumentLifecycleStatus {
|
|
63
|
+
return path.includes('/PureDrafts/') ? 'draft' : 'filed'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function useDocumentLifecycle(
|
|
67
|
+
options: UseDocumentLifecycleOptions,
|
|
68
|
+
): DocumentLifecycle {
|
|
69
|
+
const { appSlug, suffix, kind, suggestedTitle } = options
|
|
70
|
+
const debounceMs = options.debounceMs ?? 1500
|
|
71
|
+
const serializeRef = useRef(options.serialize)
|
|
72
|
+
serializeRef.current = options.serialize
|
|
73
|
+
|
|
74
|
+
const [doc, setDoc] = useState<DocumentLifecycleDoc>({
|
|
75
|
+
path: null,
|
|
76
|
+
title: suggestedTitle ?? '',
|
|
77
|
+
status: 'none',
|
|
78
|
+
savedAt: null,
|
|
79
|
+
saving: false,
|
|
80
|
+
error: null,
|
|
81
|
+
})
|
|
82
|
+
const pathRef = useRef<string | null>(null)
|
|
83
|
+
const timerRef = useRef<number | null>(null)
|
|
84
|
+
// Serialize all writes for one document through a promise chain — never two
|
|
85
|
+
// concurrent autosaves, and flush() awaits everything queued before it.
|
|
86
|
+
const writeChainRef = useRef<Promise<void>>(Promise.resolve())
|
|
87
|
+
const creatingRef = useRef<Promise<string | null> | null>(null)
|
|
88
|
+
|
|
89
|
+
useEffect(
|
|
90
|
+
() => () => {
|
|
91
|
+
if (timerRef.current !== null) window.clearTimeout(timerRef.current)
|
|
92
|
+
},
|
|
93
|
+
[],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const queueWrite = useCallback(
|
|
97
|
+
(work: () => Promise<void>): Promise<void> => {
|
|
98
|
+
const next = writeChainRef.current.then(work, work)
|
|
99
|
+
writeChainRef.current = next.catch(() => undefined)
|
|
100
|
+
return next
|
|
101
|
+
},
|
|
102
|
+
[],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const saveNow = useCallback(async (): Promise<void> => {
|
|
106
|
+
const path = pathRef.current
|
|
107
|
+
if (!path) return
|
|
108
|
+
setDoc(current => ({ ...current, saving: true }))
|
|
109
|
+
try {
|
|
110
|
+
const { savedAt } = await autosavePlatformDocument({
|
|
111
|
+
path,
|
|
112
|
+
files: serializeRef.current(),
|
|
113
|
+
})
|
|
114
|
+
setDoc(current => ({ ...current, saving: false, savedAt, error: null }))
|
|
115
|
+
} catch (error) {
|
|
116
|
+
setDoc(current => ({
|
|
117
|
+
...current,
|
|
118
|
+
saving: false,
|
|
119
|
+
error: error instanceof Error ? error.message : 'Could not save.',
|
|
120
|
+
}))
|
|
121
|
+
}
|
|
122
|
+
}, [])
|
|
123
|
+
|
|
124
|
+
const ensureDraft = useCallback(async (): Promise<string | null> => {
|
|
125
|
+
if (pathRef.current) return pathRef.current
|
|
126
|
+
if (creatingRef.current) return creatingRef.current
|
|
127
|
+
creatingRef.current = (async () => {
|
|
128
|
+
try {
|
|
129
|
+
const { path } = await createPlatformDraft({
|
|
130
|
+
appSlug,
|
|
131
|
+
suffix,
|
|
132
|
+
kind,
|
|
133
|
+
files: serializeRef.current(),
|
|
134
|
+
})
|
|
135
|
+
pathRef.current = path
|
|
136
|
+
setDoc(current => ({
|
|
137
|
+
...current,
|
|
138
|
+
path,
|
|
139
|
+
title: current.title || titleFromPath(path, suffix),
|
|
140
|
+
status: 'draft',
|
|
141
|
+
savedAt: new Date().toISOString(),
|
|
142
|
+
error: null,
|
|
143
|
+
}))
|
|
144
|
+
void touchPlatformRecentDocument({
|
|
145
|
+
appSlug,
|
|
146
|
+
path,
|
|
147
|
+
title: titleFromPath(path, suffix),
|
|
148
|
+
}).catch(() => undefined)
|
|
149
|
+
return path
|
|
150
|
+
} catch (error) {
|
|
151
|
+
setDoc(current => ({
|
|
152
|
+
...current,
|
|
153
|
+
error:
|
|
154
|
+
error instanceof Error ? error.message : 'Could not create draft.',
|
|
155
|
+
}))
|
|
156
|
+
return null
|
|
157
|
+
} finally {
|
|
158
|
+
creatingRef.current = null
|
|
159
|
+
}
|
|
160
|
+
})()
|
|
161
|
+
return creatingRef.current
|
|
162
|
+
}, [appSlug, kind, suffix])
|
|
163
|
+
|
|
164
|
+
const markDirty = useCallback((): void => {
|
|
165
|
+
void (async () => {
|
|
166
|
+
if (!pathRef.current) {
|
|
167
|
+
const created = await ensureDraft()
|
|
168
|
+
if (!created) return
|
|
169
|
+
}
|
|
170
|
+
if (timerRef.current !== null) window.clearTimeout(timerRef.current)
|
|
171
|
+
timerRef.current = window.setTimeout(() => {
|
|
172
|
+
timerRef.current = null
|
|
173
|
+
void queueWrite(saveNow)
|
|
174
|
+
}, debounceMs)
|
|
175
|
+
})()
|
|
176
|
+
}, [debounceMs, ensureDraft, queueWrite, saveNow])
|
|
177
|
+
|
|
178
|
+
const flush = useCallback(async (): Promise<void> => {
|
|
179
|
+
if (timerRef.current !== null) {
|
|
180
|
+
window.clearTimeout(timerRef.current)
|
|
181
|
+
timerRef.current = null
|
|
182
|
+
}
|
|
183
|
+
if (!pathRef.current) return
|
|
184
|
+
await queueWrite(saveNow)
|
|
185
|
+
}, [queueWrite, saveNow])
|
|
186
|
+
|
|
187
|
+
// Last-chance flush when the app tab hides or unloads.
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
const onHidden = (): void => {
|
|
190
|
+
if (document.visibilityState === 'hidden') void flush()
|
|
191
|
+
}
|
|
192
|
+
window.addEventListener('beforeunload', onHidden)
|
|
193
|
+
document.addEventListener('visibilitychange', onHidden)
|
|
194
|
+
return () => {
|
|
195
|
+
window.removeEventListener('beforeunload', onHidden)
|
|
196
|
+
document.removeEventListener('visibilitychange', onHidden)
|
|
197
|
+
}
|
|
198
|
+
}, [flush])
|
|
199
|
+
|
|
200
|
+
const promote = useCallback(
|
|
201
|
+
async (title: string, targetDir?: string): Promise<void> => {
|
|
202
|
+
const path = pathRef.current ?? (await ensureDraft())
|
|
203
|
+
if (!path) return
|
|
204
|
+
await flush()
|
|
205
|
+
try {
|
|
206
|
+
const result = await promotePlatformDocument({
|
|
207
|
+
path,
|
|
208
|
+
title,
|
|
209
|
+
targetDir,
|
|
210
|
+
suffix,
|
|
211
|
+
appSlug,
|
|
212
|
+
})
|
|
213
|
+
pathRef.current = result.path
|
|
214
|
+
setDoc(current => ({
|
|
215
|
+
...current,
|
|
216
|
+
path: result.path,
|
|
217
|
+
title: titleFromPath(result.path, suffix),
|
|
218
|
+
status: statusForPath(result.path),
|
|
219
|
+
error: null,
|
|
220
|
+
}))
|
|
221
|
+
void touchPlatformRecentDocument({
|
|
222
|
+
appSlug,
|
|
223
|
+
path: result.path,
|
|
224
|
+
title: titleFromPath(result.path, suffix),
|
|
225
|
+
}).catch(() => undefined)
|
|
226
|
+
} catch (error) {
|
|
227
|
+
setDoc(current => ({
|
|
228
|
+
...current,
|
|
229
|
+
error: error instanceof Error ? error.message : 'Could not file.',
|
|
230
|
+
}))
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
[appSlug, ensureDraft, flush, suffix],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
const rename = useCallback(
|
|
237
|
+
async (title: string): Promise<void> => {
|
|
238
|
+
const path = pathRef.current
|
|
239
|
+
if (!path) {
|
|
240
|
+
setDoc(current => ({ ...current, title }))
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
await flush()
|
|
244
|
+
try {
|
|
245
|
+
const result = await renamePlatformDocument({
|
|
246
|
+
path,
|
|
247
|
+
title,
|
|
248
|
+
suffix,
|
|
249
|
+
appSlug,
|
|
250
|
+
})
|
|
251
|
+
pathRef.current = result.path
|
|
252
|
+
setDoc(current => ({
|
|
253
|
+
...current,
|
|
254
|
+
path: result.path,
|
|
255
|
+
title: titleFromPath(result.path, suffix),
|
|
256
|
+
status: statusForPath(result.path),
|
|
257
|
+
error: null,
|
|
258
|
+
}))
|
|
259
|
+
} catch (error) {
|
|
260
|
+
setDoc(current => ({
|
|
261
|
+
...current,
|
|
262
|
+
error: error instanceof Error ? error.message : 'Could not rename.',
|
|
263
|
+
}))
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
[appSlug, flush, suffix],
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
const duplicate = useCallback(async (): Promise<string | null> => {
|
|
270
|
+
const path = pathRef.current
|
|
271
|
+
if (!path) return null
|
|
272
|
+
await flush()
|
|
273
|
+
try {
|
|
274
|
+
const result = await duplicatePlatformDocument({ path, suffix })
|
|
275
|
+
return result.path
|
|
276
|
+
} catch {
|
|
277
|
+
return null
|
|
278
|
+
}
|
|
279
|
+
}, [flush, suffix])
|
|
280
|
+
|
|
281
|
+
const revealInFinder = useCallback(async (): Promise<void> => {
|
|
282
|
+
const path = pathRef.current
|
|
283
|
+
if (!path) return
|
|
284
|
+
await bridge.call(PLATFORM_BRIDGE_METHODS.OS_REVEAL, [path])
|
|
285
|
+
}, [])
|
|
286
|
+
|
|
287
|
+
const openInFiles = useCallback(async (): Promise<void> => {
|
|
288
|
+
const path = pathRef.current
|
|
289
|
+
if (!path) return
|
|
290
|
+
await bridge.call(PLATFORM_BRIDGE_METHODS.CATALOG_OPEN, [
|
|
291
|
+
{ path, appSlug: 'purefiles' },
|
|
292
|
+
])
|
|
293
|
+
}, [])
|
|
294
|
+
|
|
295
|
+
const adopt = useCallback(
|
|
296
|
+
(path: string, opts: { title?: string } = {}): void => {
|
|
297
|
+
pathRef.current = path
|
|
298
|
+
setDoc({
|
|
299
|
+
path,
|
|
300
|
+
title: opts.title ?? titleFromPath(path, suffix),
|
|
301
|
+
status: statusForPath(path),
|
|
302
|
+
savedAt: null,
|
|
303
|
+
saving: false,
|
|
304
|
+
error: null,
|
|
305
|
+
})
|
|
306
|
+
void touchPlatformRecentDocument({
|
|
307
|
+
appSlug,
|
|
308
|
+
path,
|
|
309
|
+
title: opts.title ?? titleFromPath(path, suffix),
|
|
310
|
+
}).catch(() => undefined)
|
|
311
|
+
},
|
|
312
|
+
[appSlug, suffix],
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
const reset = useCallback((): void => {
|
|
316
|
+
if (timerRef.current !== null) {
|
|
317
|
+
window.clearTimeout(timerRef.current)
|
|
318
|
+
timerRef.current = null
|
|
319
|
+
}
|
|
320
|
+
pathRef.current = null
|
|
321
|
+
setDoc({
|
|
322
|
+
path: null,
|
|
323
|
+
title: '',
|
|
324
|
+
status: 'none',
|
|
325
|
+
savedAt: null,
|
|
326
|
+
saving: false,
|
|
327
|
+
error: null,
|
|
328
|
+
})
|
|
329
|
+
}, [])
|
|
330
|
+
|
|
331
|
+
return useMemo(
|
|
332
|
+
() => ({
|
|
333
|
+
doc,
|
|
334
|
+
ensureDraft,
|
|
335
|
+
markDirty,
|
|
336
|
+
flush,
|
|
337
|
+
promote,
|
|
338
|
+
rename,
|
|
339
|
+
duplicate,
|
|
340
|
+
revealInFinder,
|
|
341
|
+
openInFiles,
|
|
342
|
+
adopt,
|
|
343
|
+
reset,
|
|
344
|
+
}),
|
|
345
|
+
[
|
|
346
|
+
doc,
|
|
347
|
+
ensureDraft,
|
|
348
|
+
markDirty,
|
|
349
|
+
flush,
|
|
350
|
+
promote,
|
|
351
|
+
rename,
|
|
352
|
+
duplicate,
|
|
353
|
+
revealInFinder,
|
|
354
|
+
openInFiles,
|
|
355
|
+
adopt,
|
|
356
|
+
reset,
|
|
357
|
+
],
|
|
358
|
+
)
|
|
359
|
+
}
|
|
@@ -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(
|
|
114
|
-
|
|
115
|
-
|
|
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())
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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,
|
|
@@ -0,0 +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
|
+
})
|