@proteos/component-sdk 0.60.0 → 0.75.0
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/dist/{chunk-HODSL6ER.js → chunk-IYXDPNMJ.js} +2 -2
- package/dist/chunk-IYXDPNMJ.js.map +1 -0
- package/dist/{chunk-3VUWWDED.cjs → chunk-Q7PTW7DD.cjs} +2 -2
- package/dist/chunk-Q7PTW7DD.cjs.map +1 -0
- package/dist/index.cjs +78 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +92 -7
- package/dist/index.d.ts +92 -7
- package/dist/index.js +69 -15
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +5 -5
- package/dist/protocol.d.cts +181 -3
- package/dist/protocol.d.ts +181 -3
- package/dist/protocol.js +1 -1
- package/package.json +2 -2
- package/src/context.tsx +116 -16
- package/src/hooks.test.tsx +116 -3
- package/src/hooks.ts +69 -4
- package/src/index.ts +13 -0
- package/src/protocol.ts +178 -0
- package/dist/chunk-3VUWWDED.cjs.map +0 -1
- package/dist/chunk-HODSL6ER.js.map +0 -1
package/src/context.tsx
CHANGED
|
@@ -6,21 +6,27 @@ import type {
|
|
|
6
6
|
FunctionsClient,
|
|
7
7
|
KnowledgeClient,
|
|
8
8
|
MetaClient,
|
|
9
|
+
PageAction,
|
|
9
10
|
StorageClient,
|
|
10
11
|
} from '@proteos/sdk'
|
|
11
12
|
import { createContext, type ReactNode, useMemo } from 'react'
|
|
12
13
|
|
|
13
14
|
import {
|
|
15
|
+
type CloseAgentWindowMessage,
|
|
14
16
|
type Envelope,
|
|
15
17
|
isHostMessage,
|
|
16
18
|
MESSAGE_SOURCE,
|
|
17
19
|
type ModalResultMessage,
|
|
18
20
|
type ModalWidth,
|
|
19
21
|
type NavigateMessage,
|
|
22
|
+
type OpenAgentWindowMessage,
|
|
20
23
|
type OpenCallMessage,
|
|
21
24
|
type OpenModalMessage,
|
|
22
25
|
type OpenPeekMessage,
|
|
26
|
+
type PageActionContext,
|
|
27
|
+
type PageActionOutcome,
|
|
23
28
|
type PageContext,
|
|
29
|
+
type RunPageActionMessage,
|
|
24
30
|
} from './protocol'
|
|
25
31
|
|
|
26
32
|
/** Options for {@link RuntimeContextValue.navigate}. */
|
|
@@ -45,6 +51,33 @@ export interface OpenModalOptions {
|
|
|
45
51
|
height?: number
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
/** What {@link AgentWindowController.open} opens the host's agent window to. */
|
|
55
|
+
export interface OpenAgentWindowOptions {
|
|
56
|
+
/** Resume this session (its agent comes with it). */
|
|
57
|
+
sessionId?: string
|
|
58
|
+
/** Start a fresh session with this agent. Ignored when `sessionId` is set,
|
|
59
|
+
* and when the host surface pins its agent. Without it (and without a
|
|
60
|
+
* session) the window's currently selected agent is used. */
|
|
61
|
+
agentKey?: string
|
|
62
|
+
/** A message to send — into the resumed session, or as the first message
|
|
63
|
+
* of the fresh one. */
|
|
64
|
+
prompt?: string
|
|
65
|
+
/** Context handed to the model alongside `prompt` (what the component knows:
|
|
66
|
+
* the record on screen, a selection …). Read by the model, never shown in
|
|
67
|
+
* the chat; sent only together with a `prompt`. Not for secrets. */
|
|
68
|
+
context?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The host's agent session window (the floating assistant), driven from a
|
|
73
|
+
* component. `open()` posts OPEN_AGENT_WINDOW, `close()` posts
|
|
74
|
+
* CLOSE_AGENT_WINDOW — closing minimizes; the session and its stream live on.
|
|
75
|
+
*/
|
|
76
|
+
export interface AgentWindowController {
|
|
77
|
+
open: (options?: OpenAgentWindowOptions) => void
|
|
78
|
+
close: () => void
|
|
79
|
+
}
|
|
80
|
+
|
|
48
81
|
/**
|
|
49
82
|
* Manual control over iframe overlay promotion. The runtime already detects
|
|
50
83
|
* overlays automatically (anything portaled to the iframe's `document.body` —
|
|
@@ -130,6 +163,12 @@ export interface RuntimeContextValue extends RuntimeValue {
|
|
|
130
163
|
props?: Record<string, unknown>,
|
|
131
164
|
options?: OpenModalOptions,
|
|
132
165
|
) => Promise<unknown>
|
|
166
|
+
/** Open / minimize the host's agent session window (posts OPEN_AGENT_WINDOW
|
|
167
|
+
* / CLOSE_AGENT_WINDOW). */
|
|
168
|
+
agentWindow: AgentWindowController
|
|
169
|
+
/** Run a page action defined inline (posts RUN_PAGE_ACTION); resolves with
|
|
170
|
+
* how its flow ended. */
|
|
171
|
+
runPageAction: (action: PageAction, context?: PageActionContext) => Promise<PageActionOutcome>
|
|
133
172
|
}
|
|
134
173
|
|
|
135
174
|
export const RuntimeContext = createContext<RuntimeContextValue | null>(null)
|
|
@@ -158,30 +197,65 @@ function openCall(conversationBriefId: string): void {
|
|
|
158
197
|
window.parent?.postMessage(envelope, '*')
|
|
159
198
|
}
|
|
160
199
|
|
|
200
|
+
/** Post OPEN_AGENT_WINDOW — only the fields that were given ride the wire. */
|
|
201
|
+
function openAgentWindow(options?: OpenAgentWindowOptions): void {
|
|
202
|
+
const envelope: Envelope<OpenAgentWindowMessage> = {
|
|
203
|
+
source: MESSAGE_SOURCE,
|
|
204
|
+
type: 'OPEN_AGENT_WINDOW',
|
|
205
|
+
...(options?.sessionId !== undefined ? { sessionId: options.sessionId } : {}),
|
|
206
|
+
...(options?.agentKey !== undefined ? { agentKey: options.agentKey } : {}),
|
|
207
|
+
...(options?.prompt !== undefined ? { prompt: options.prompt } : {}),
|
|
208
|
+
...(options?.context !== undefined ? { context: options.context } : {}),
|
|
209
|
+
}
|
|
210
|
+
window.parent?.postMessage(envelope, '*')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Post CLOSE_AGENT_WINDOW — minimize the host's agent window. */
|
|
214
|
+
function closeAgentWindow(): void {
|
|
215
|
+
const envelope: Envelope<CloseAgentWindowMessage> = {
|
|
216
|
+
source: MESSAGE_SOURCE,
|
|
217
|
+
type: 'CLOSE_AGENT_WINDOW',
|
|
218
|
+
}
|
|
219
|
+
window.parent?.postMessage(envelope, '*')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const agentWindow: AgentWindowController = { open: openAgentWindow, close: closeAgentWindow }
|
|
223
|
+
|
|
161
224
|
/** Navigate full-page (default) or into a peek panel (`{ peek: true }`). */
|
|
162
225
|
function navigate(to: string, options?: NavigateOptions): void {
|
|
163
226
|
if (options?.peek) postOpenPeek(to)
|
|
164
227
|
else postNavigate(to)
|
|
165
228
|
}
|
|
166
229
|
|
|
167
|
-
// ──
|
|
168
|
-
// One module-level listener + a pending map, rather than a
|
|
169
|
-
// the SDK is an import-mapped singleton, so this is
|
|
230
|
+
// ── Host requests (component modals, page actions) ──────────────────────────
|
|
231
|
+
// One module-level listener + a pending map per request kind, rather than a
|
|
232
|
+
// listener per call: the SDK is an import-mapped singleton, so this is
|
|
233
|
+
// per-iframe state.
|
|
170
234
|
|
|
171
235
|
const pendingModals = new Map<string, (result: unknown) => void>()
|
|
172
|
-
|
|
236
|
+
const pendingPageActions = new Map<string, (outcome: PageActionOutcome) => void>()
|
|
237
|
+
let replyListenerAttached = false
|
|
173
238
|
|
|
174
|
-
function
|
|
175
|
-
if (
|
|
176
|
-
|
|
239
|
+
function ensureReplyListener(): void {
|
|
240
|
+
if (replyListenerAttached) return
|
|
241
|
+
replyListenerAttached = true
|
|
177
242
|
window.addEventListener('message', (event: MessageEvent) => {
|
|
178
243
|
const data: unknown = event.data
|
|
179
|
-
if (!isHostMessage(data)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
244
|
+
if (!isHostMessage(data)) return
|
|
245
|
+
// An unknown requestId is someone else's request (or a stale reply) — drop it.
|
|
246
|
+
if (data.type === 'MODAL_CLOSED') {
|
|
247
|
+
const resolve = pendingModals.get(data.requestId)
|
|
248
|
+
if (!resolve) return
|
|
249
|
+
pendingModals.delete(data.requestId)
|
|
250
|
+
resolve(data.result)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
if (data.type === 'PAGE_ACTION_SETTLED') {
|
|
254
|
+
const resolve = pendingPageActions.get(data.requestId)
|
|
255
|
+
if (!resolve) return
|
|
256
|
+
pendingPageActions.delete(data.requestId)
|
|
257
|
+
resolve(data.outcome)
|
|
258
|
+
}
|
|
185
259
|
})
|
|
186
260
|
}
|
|
187
261
|
|
|
@@ -224,7 +298,7 @@ function newRequestId(): string {
|
|
|
224
298
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
225
299
|
return crypto.randomUUID()
|
|
226
300
|
}
|
|
227
|
-
return `
|
|
301
|
+
return `request_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`
|
|
228
302
|
}
|
|
229
303
|
|
|
230
304
|
/** Post OPEN_MODAL and resolve when the host answers MODAL_CLOSED. */
|
|
@@ -233,7 +307,7 @@ function openModal(
|
|
|
233
307
|
props?: Record<string, unknown>,
|
|
234
308
|
options?: OpenModalOptions,
|
|
235
309
|
): Promise<unknown> {
|
|
236
|
-
|
|
310
|
+
ensureReplyListener()
|
|
237
311
|
const requestId = newRequestId()
|
|
238
312
|
const envelope: Envelope<OpenModalMessage> = {
|
|
239
313
|
source: MESSAGE_SOURCE,
|
|
@@ -255,6 +329,32 @@ function openModal(
|
|
|
255
329
|
})
|
|
256
330
|
}
|
|
257
331
|
|
|
332
|
+
/** Post RUN_PAGE_ACTION and resolve when the host answers PAGE_ACTION_SETTLED.
|
|
333
|
+
* The host's dialogs take focus, so it goes back to the pressed control
|
|
334
|
+
* afterwards — as for a modal. */
|
|
335
|
+
function runPageAction(
|
|
336
|
+
action: PageAction,
|
|
337
|
+
context?: PageActionContext,
|
|
338
|
+
): Promise<PageActionOutcome> {
|
|
339
|
+
ensureReplyListener()
|
|
340
|
+
const requestId = newRequestId()
|
|
341
|
+
const envelope: Envelope<RunPageActionMessage> = {
|
|
342
|
+
source: MESSAGE_SOURCE,
|
|
343
|
+
type: 'RUN_PAGE_ACTION',
|
|
344
|
+
requestId,
|
|
345
|
+
action,
|
|
346
|
+
...(context !== undefined ? { context } : {}),
|
|
347
|
+
}
|
|
348
|
+
const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
349
|
+
return new Promise<PageActionOutcome>((resolve) => {
|
|
350
|
+
pendingPageActions.set(requestId, (outcome) => {
|
|
351
|
+
restoreFocus(opener)
|
|
352
|
+
resolve(outcome)
|
|
353
|
+
})
|
|
354
|
+
window.parent?.postMessage(envelope, '*')
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
258
358
|
/** Post MODAL_RESULT — the component inside a host modal closing with a value. */
|
|
259
359
|
export function postModalResult(requestId: string, result?: unknown): void {
|
|
260
360
|
const envelope: Envelope<ModalResultMessage> = {
|
|
@@ -278,7 +378,7 @@ export function RuntimeProvider({ value, children }: { value: RuntimeValue; chil
|
|
|
278
378
|
// props and pageCtx are all set once), so memoizing on its identity recreates
|
|
279
379
|
// the context only when the runtime genuinely re-inits.
|
|
280
380
|
const ctx = useMemo<RuntimeContextValue>(
|
|
281
|
-
() => ({ ...value, navigate, openCall, openModal }),
|
|
381
|
+
() => ({ ...value, navigate, openCall, openModal, agentWindow, runPageAction }),
|
|
282
382
|
[value],
|
|
283
383
|
)
|
|
284
384
|
return <RuntimeContext.Provider value={ctx}>{children}</RuntimeContext.Provider>
|
package/src/hooks.test.tsx
CHANGED
|
@@ -4,17 +4,24 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
4
4
|
|
|
5
5
|
import { RuntimeProvider, type RuntimeValue } from './context'
|
|
6
6
|
import {
|
|
7
|
+
useAgentWindow,
|
|
7
8
|
useApiBaseUrl,
|
|
8
9
|
useComponentModal,
|
|
9
10
|
useModalHost,
|
|
10
11
|
useNavigate,
|
|
11
12
|
useOverlayExpansion,
|
|
13
|
+
usePageAction,
|
|
12
14
|
usePageContext,
|
|
13
15
|
useProps,
|
|
14
16
|
useRuntime,
|
|
15
17
|
useSdk,
|
|
16
18
|
} from './hooks'
|
|
17
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
type Envelope,
|
|
21
|
+
MESSAGE_SOURCE,
|
|
22
|
+
type ModalClosedMessage,
|
|
23
|
+
type PageActionSettledMessage,
|
|
24
|
+
} from './protocol'
|
|
18
25
|
|
|
19
26
|
// A stable stand-in for the injected SDK — the hooks only pass it through.
|
|
20
27
|
const sdk = {} as RuntimeValue['sdk']
|
|
@@ -23,7 +30,7 @@ function makeValue(overrides: Partial<RuntimeValue> = {}): RuntimeValue {
|
|
|
23
30
|
return {
|
|
24
31
|
sdk,
|
|
25
32
|
props: { title: 'Hello' },
|
|
26
|
-
pageCtx: { record: { id: 'rec_1', name: 'ACME' }, entity: { slug: 'company' } },
|
|
33
|
+
pageCtx: { record: { id: 'rec_1', name: 'ACME' }, entity: { slug: 'company' }, mode: 'view' },
|
|
27
34
|
apiBaseUrl: 'https://api.test',
|
|
28
35
|
getToken: () => 'tok_123',
|
|
29
36
|
...overrides,
|
|
@@ -56,11 +63,12 @@ describe('runtime hooks', () => {
|
|
|
56
63
|
expect(result.current.props.title).toBe('Hello')
|
|
57
64
|
expect(result.current.ctx.record).toEqual({ id: 'rec_1', name: 'ACME' })
|
|
58
65
|
expect(result.current.ctx.entity?.slug).toBe('company')
|
|
66
|
+
expect(result.current.ctx.mode).toBe('view')
|
|
59
67
|
expect(result.current.base).toBe('https://api.test')
|
|
60
68
|
})
|
|
61
69
|
|
|
62
70
|
it('usePageContext().record is undefined when the page has no bound record', () => {
|
|
63
|
-
const value = makeValue({ pageCtx: { entity: { slug: 'dashboard' } } })
|
|
71
|
+
const value = makeValue({ pageCtx: { entity: { slug: 'dashboard' }, mode: 'view' } })
|
|
64
72
|
const { result } = renderHook(() => usePageContext(), { wrapper: wrapper(value) })
|
|
65
73
|
expect(result.current.record).toBeUndefined()
|
|
66
74
|
})
|
|
@@ -75,6 +83,52 @@ describe('runtime hooks', () => {
|
|
|
75
83
|
)
|
|
76
84
|
})
|
|
77
85
|
|
|
86
|
+
it('useAgentWindow posts OPEN_AGENT_WINDOW with only the fields given, and CLOSE_AGENT_WINDOW', () => {
|
|
87
|
+
const post = vi.spyOn(window.parent, 'postMessage').mockImplementation(() => {})
|
|
88
|
+
const { result } = renderHook(() => useAgentWindow(), { wrapper: wrapper(makeValue()) })
|
|
89
|
+
|
|
90
|
+
result.current.open()
|
|
91
|
+
expect(post).toHaveBeenLastCalledWith(
|
|
92
|
+
{ source: MESSAGE_SOURCE, type: 'OPEN_AGENT_WINDOW' },
|
|
93
|
+
'*',
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
result.current.open({ sessionId: 'sess_1', prompt: 'Summarize this' })
|
|
97
|
+
expect(post).toHaveBeenLastCalledWith(
|
|
98
|
+
{
|
|
99
|
+
source: MESSAGE_SOURCE,
|
|
100
|
+
type: 'OPEN_AGENT_WINDOW',
|
|
101
|
+
sessionId: 'sess_1',
|
|
102
|
+
prompt: 'Summarize this',
|
|
103
|
+
},
|
|
104
|
+
'*',
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
result.current.open({ agentKey: 'sales-bot' })
|
|
108
|
+
expect(post).toHaveBeenLastCalledWith(
|
|
109
|
+
{ source: MESSAGE_SOURCE, type: 'OPEN_AGENT_WINDOW', agentKey: 'sales-bot' },
|
|
110
|
+
'*',
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
result.current.open({ agentKey: 'sales-bot', prompt: 'Follow up', context: 'Deal #7' })
|
|
114
|
+
expect(post).toHaveBeenLastCalledWith(
|
|
115
|
+
{
|
|
116
|
+
source: MESSAGE_SOURCE,
|
|
117
|
+
type: 'OPEN_AGENT_WINDOW',
|
|
118
|
+
agentKey: 'sales-bot',
|
|
119
|
+
prompt: 'Follow up',
|
|
120
|
+
context: 'Deal #7',
|
|
121
|
+
},
|
|
122
|
+
'*',
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
result.current.close()
|
|
126
|
+
expect(post).toHaveBeenLastCalledWith(
|
|
127
|
+
{ source: MESSAGE_SOURCE, type: 'CLOSE_AGENT_WINDOW' },
|
|
128
|
+
'*',
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
|
|
78
132
|
it('useOverlayExpansion drives the shell controller', () => {
|
|
79
133
|
const overlay = { expand: vi.fn(), collapse: vi.fn(), isExpanded: () => true }
|
|
80
134
|
const { result } = renderHook(() => useOverlayExpansion(), {
|
|
@@ -153,6 +207,65 @@ describe('runtime hooks', () => {
|
|
|
153
207
|
opener.remove()
|
|
154
208
|
})
|
|
155
209
|
|
|
210
|
+
it('usePageAction posts RUN_PAGE_ACTION and resolves with the matching outcome', async () => {
|
|
211
|
+
const post = vi.spyOn(window.parent, 'postMessage').mockImplementation(() => {})
|
|
212
|
+
const { result } = renderHook(() => usePageAction(), { wrapper: wrapper(makeValue()) })
|
|
213
|
+
const action = {
|
|
214
|
+
kind: 'intent' as const,
|
|
215
|
+
intent: 'update_record' as const,
|
|
216
|
+
entity: 'deal',
|
|
217
|
+
label: 'Update deal',
|
|
218
|
+
icon: 'Sparkles',
|
|
219
|
+
prompt: 'Apply what was agreed',
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const pending = result.current(action, {
|
|
223
|
+
scope: { notes: 'Signed' },
|
|
224
|
+
records: [{ entitySlug: 'deal', recordId: 'deal_1' }],
|
|
225
|
+
})
|
|
226
|
+
const [payload] = post.mock.calls.at(-1) as [Record<string, unknown>]
|
|
227
|
+
expect(payload).toMatchObject({
|
|
228
|
+
source: MESSAGE_SOURCE,
|
|
229
|
+
type: 'RUN_PAGE_ACTION',
|
|
230
|
+
action,
|
|
231
|
+
context: {
|
|
232
|
+
scope: { notes: 'Signed' },
|
|
233
|
+
records: [{ entitySlug: 'deal', recordId: 'deal_1' }],
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
const reply = (requestId: string): Envelope<PageActionSettledMessage> => ({
|
|
238
|
+
source: MESSAGE_SOURCE,
|
|
239
|
+
type: 'PAGE_ACTION_SETTLED',
|
|
240
|
+
requestId,
|
|
241
|
+
outcome: {
|
|
242
|
+
status: 'done',
|
|
243
|
+
kind: 'intent',
|
|
244
|
+
mode: 'update',
|
|
245
|
+
entitySlug: 'deal',
|
|
246
|
+
recordId: 'deal_1',
|
|
247
|
+
title: 'Acme renewal',
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
// Another request's reply — and a modal's with our id — must not resolve ours.
|
|
251
|
+
window.dispatchEvent(new MessageEvent('message', { data: reply('someone-else') }))
|
|
252
|
+
window.dispatchEvent(
|
|
253
|
+
new MessageEvent('message', {
|
|
254
|
+
data: { source: MESSAGE_SOURCE, type: 'MODAL_CLOSED', requestId: payload.requestId },
|
|
255
|
+
}),
|
|
256
|
+
)
|
|
257
|
+
window.dispatchEvent(new MessageEvent('message', { data: reply(payload.requestId as string) }))
|
|
258
|
+
await expect(pending).resolves.toMatchObject({ status: 'done', recordId: 'deal_1' })
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('usePageAction leaves out a context it was not given', () => {
|
|
262
|
+
const post = vi.spyOn(window.parent, 'postMessage').mockImplementation(() => {})
|
|
263
|
+
const { result } = renderHook(() => usePageAction(), { wrapper: wrapper(makeValue()) })
|
|
264
|
+
void result.current({ kind: 'link', url: 'https://example.com', label: 'Open', icon: 'Link' })
|
|
265
|
+
const [payload] = post.mock.calls.at(-1) as [Record<string, unknown>]
|
|
266
|
+
expect(payload).not.toHaveProperty('context')
|
|
267
|
+
})
|
|
268
|
+
|
|
156
269
|
it('useComponentModal carries the width and height the opener asked for', () => {
|
|
157
270
|
const post = vi.spyOn(window.parent, 'postMessage').mockImplementation(() => {})
|
|
158
271
|
const { result } = renderHook(() => useComponentModal(), { wrapper: wrapper(makeValue()) })
|
package/src/hooks.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { PageAction } from '@proteos/sdk'
|
|
1
2
|
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
|
2
3
|
|
|
3
4
|
import {
|
|
5
|
+
type AgentWindowController,
|
|
4
6
|
type ComponentSdk,
|
|
5
7
|
type NavigateOptions,
|
|
6
8
|
type OpenModalOptions,
|
|
@@ -9,7 +11,7 @@ import {
|
|
|
9
11
|
RuntimeContext,
|
|
10
12
|
type RuntimeContextValue,
|
|
11
13
|
} from './context'
|
|
12
|
-
import type { PageContext } from './protocol'
|
|
14
|
+
import type { PageActionContext, PageActionOutcome, PageContext } from './protocol'
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Low-level escape hatch: the whole runtime context (sdk, props, pageCtx,
|
|
@@ -40,9 +42,13 @@ export function useProps<T extends Record<string, unknown> = Record<string, unkn
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
|
-
* The full page context: host `record`, `entity`,
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
45
|
+
* The full page context: host `record`, `entity`, URL `params`, the viewer as
|
|
46
|
+
* `current_user` (absent on public pages) and the page `mode`
|
|
47
|
+
* (`'view' | 'create' | 'edit'` — what the viewer is doing with the record).
|
|
48
|
+
* The `record` is a snapshot taken at mount (`undefined` on record-less
|
|
49
|
+
* platform pages) — fetch via `useSdk().data` for anything that must stay
|
|
50
|
+
* fresh. `mode` is a snapshot too: the host remounts the frame when the page
|
|
51
|
+
* switches between view and edit.
|
|
46
52
|
*/
|
|
47
53
|
export function usePageContext(): PageContext {
|
|
48
54
|
return useRuntime().pageCtx
|
|
@@ -70,6 +76,26 @@ export function useOpenCall(): (conversationBriefId: string) => void {
|
|
|
70
76
|
return useRuntime().openCall
|
|
71
77
|
}
|
|
72
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Open or minimize the HOST's agent session window (the floating assistant)
|
|
81
|
+
* from a component — the iframe cannot render it itself.
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* const agentWindow = useAgentWindow()
|
|
85
|
+
* agentWindow.open() // just open it
|
|
86
|
+
* agentWindow.open({ sessionId }) // resume a session
|
|
87
|
+
* agentWindow.open({ agentKey: 'sales-bot', prompt }) // fresh session, prompt sent
|
|
88
|
+
* agentWindow.close() // minimize (session lives on)
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* `sessionId` wins over `agentKey` (the session already has an agent); a
|
|
92
|
+
* `prompt` goes into whichever session the window lands on. Ignored on public
|
|
93
|
+
* pages, and by preview hosts.
|
|
94
|
+
*/
|
|
95
|
+
export function useAgentWindow(): AgentWindowController {
|
|
96
|
+
return useRuntime().agentWindow
|
|
97
|
+
}
|
|
98
|
+
|
|
73
99
|
/** The API origin SDK calls resolve against (for building raw asset URLs). */
|
|
74
100
|
export function useApiBaseUrl(): string {
|
|
75
101
|
return useRuntime().apiBaseUrl
|
|
@@ -135,6 +161,45 @@ export function useComponentModal(): (
|
|
|
135
161
|
return useRuntime().openModal
|
|
136
162
|
}
|
|
137
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Run a page action from a component's own button — the very `PageAction` a
|
|
166
|
+
* page or list toolbar carries, defined inline. The HOST runs it exactly as a
|
|
167
|
+
* toolbar press: `action` / `workflow` open the invoke dialog, `intent`
|
|
168
|
+
* prepares a record proposal for review, `agent` opens the agent window,
|
|
169
|
+
* `link` opens a new tab. Resolves once that flow is over.
|
|
170
|
+
*
|
|
171
|
+
* ```ts
|
|
172
|
+
* const runPageAction = usePageAction()
|
|
173
|
+
* const outcome = await runPageAction(
|
|
174
|
+
* {
|
|
175
|
+
* kind: 'intent',
|
|
176
|
+
* intent: 'update_record',
|
|
177
|
+
* entity: 'deal',
|
|
178
|
+
* label: 'Update deal',
|
|
179
|
+
* icon: 'Sparkles',
|
|
180
|
+
* prompt: 'Apply what was agreed',
|
|
181
|
+
* context: '{{ notes }}',
|
|
182
|
+
* },
|
|
183
|
+
* { scope: { notes }, records: [{ entitySlug: 'deal', recordId: deal.id }] },
|
|
184
|
+
* )
|
|
185
|
+
* if (outcome.status === 'done' && outcome.kind === 'intent') refetch()
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* The action's Liquid templates render against the page scope (`record`,
|
|
189
|
+
* `entity`, `params`, `mode`, `current_user`) with `context.scope` merged over
|
|
190
|
+
* it; `context.records` says what it acts on (default: the page record). See
|
|
191
|
+
* {@link PageActionContext} and {@link PageActionOutcome}. `visible_when`
|
|
192
|
+
* still applies — an action whose rule does not match resolves
|
|
193
|
+
* `unavailable: hidden` without opening anything. One page action at a time;
|
|
194
|
+
* public pages and previews resolve `unavailable: unsupported`.
|
|
195
|
+
*/
|
|
196
|
+
export function usePageAction(): (
|
|
197
|
+
action: PageAction,
|
|
198
|
+
context?: PageActionContext,
|
|
199
|
+
) => Promise<PageActionOutcome> {
|
|
200
|
+
return useRuntime().runPageAction
|
|
201
|
+
}
|
|
202
|
+
|
|
138
203
|
export interface ModalHost {
|
|
139
204
|
/** True when this component is rendered as the content of a host modal. */
|
|
140
205
|
isInModal: boolean
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
import './global'
|
|
9
9
|
|
|
10
10
|
export type {
|
|
11
|
+
AgentWindowController,
|
|
11
12
|
ComponentSdk,
|
|
12
13
|
NavigateOptions,
|
|
14
|
+
OpenAgentWindowOptions,
|
|
13
15
|
OpenModalOptions,
|
|
14
16
|
OverlayController,
|
|
15
17
|
RuntimeContextValue,
|
|
@@ -18,6 +20,7 @@ export type {
|
|
|
18
20
|
export { RuntimeProvider } from './context'
|
|
19
21
|
export type { FillViewportOptions, HostLocation, ModalHost } from './hooks'
|
|
20
22
|
export {
|
|
23
|
+
useAgentWindow,
|
|
21
24
|
useApiBaseUrl,
|
|
22
25
|
useAuthToken,
|
|
23
26
|
useComponentModal,
|
|
@@ -27,6 +30,7 @@ export {
|
|
|
27
30
|
useNavigate,
|
|
28
31
|
useOpenCall,
|
|
29
32
|
useOverlayExpansion,
|
|
33
|
+
usePageAction,
|
|
30
34
|
usePageContext,
|
|
31
35
|
useProps,
|
|
32
36
|
useRuntime,
|
|
@@ -34,6 +38,7 @@ export {
|
|
|
34
38
|
} from './hooks'
|
|
35
39
|
export {
|
|
36
40
|
type AuthRefreshMessage,
|
|
41
|
+
type CloseAgentWindowMessage,
|
|
37
42
|
COMPONENT_OVERLAY_Z_INDEX,
|
|
38
43
|
type DataInvalidateMessage,
|
|
39
44
|
type Envelope,
|
|
@@ -48,14 +53,22 @@ export {
|
|
|
48
53
|
type ModalResultMessage,
|
|
49
54
|
type ModalWidth,
|
|
50
55
|
type NavigateMessage,
|
|
56
|
+
type OpenAgentWindowMessage,
|
|
51
57
|
type OpenCallMessage,
|
|
52
58
|
type OpenModalMessage,
|
|
53
59
|
type OpenPeekMessage,
|
|
54
60
|
type OverlayRect,
|
|
55
61
|
type OverlayRectMessage,
|
|
56
62
|
type OverlayStateMessage,
|
|
63
|
+
type PageActionContext,
|
|
64
|
+
type PageActionOutcome,
|
|
65
|
+
type PageActionRecord,
|
|
66
|
+
type PageActionSettledMessage,
|
|
57
67
|
type PageContext,
|
|
68
|
+
type PageMode,
|
|
58
69
|
type ReadyMessage,
|
|
59
70
|
type ResizeMessage,
|
|
71
|
+
type RunPageActionMessage,
|
|
60
72
|
type RuntimeMessage,
|
|
73
|
+
type TriggerPhase,
|
|
61
74
|
} from './protocol'
|