@hosty-sdk/app 0.24.0 → 0.26.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/README.md +106 -11
- package/dist/assistant.d.ts +15 -0
- package/dist/assistant.js +38 -1
- package/dist/background-server.d.ts +16 -0
- package/dist/background-server.js +52 -0
- package/dist/browser-auth.d.ts +2 -0
- package/dist/browser-auth.js +5 -0
- package/dist/continuity-browser.d.ts +11 -0
- package/dist/continuity-browser.js +92 -0
- package/dist/continuity.d.ts +9 -0
- package/dist/continuity.js +9 -0
- package/dist/overlay-react.d.ts +5 -1
- package/dist/overlay-react.js +7 -4
- package/dist/providers-server.d.ts +3 -1
- package/dist/providers-server.js +4 -2
- package/dist/providers.d.ts +4 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +15 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.js +11 -0
- package/dist/session-server.d.ts +2 -0
- package/dist/ui-context.d.ts +2 -0
- package/dist/ui-context.js +3 -0
- package/dist/ui-embedded.d.ts +45 -0
- package/dist/ui-embedded.js +278 -0
- package/dist/ui-frame.d.ts +30 -0
- package/dist/ui-frame.js +104 -0
- package/dist/ui-host.d.ts +80 -0
- package/dist/ui-host.js +439 -0
- package/dist/ui-react.d.ts +35 -0
- package/dist/ui-react.js +225 -0
- package/dist/ui-server.d.ts +32 -0
- package/dist/ui-server.js +123 -0
- package/dist/ui-transport.d.ts +16 -0
- package/dist/ui-transport.js +68 -0
- package/dist/ui-types.d.ts +99 -0
- package/dist/ui-types.js +43 -0
- package/dist/ui.d.ts +20 -0
- package/dist/ui.js +36 -0
- package/package.json +33 -1
package/dist/ui-host.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { createPresentationRequestId } from "./ui.js";
|
|
2
|
+
import { UiError, panelKey, routeWithin } from "./ui-types.js";
|
|
3
|
+
import { canonicalSurfaceRoute, DISCOVERY_TIMEOUT, failure, readUiMessage, uiPath, uiRequest, validId, validTarget, waitForPresentation } from "./ui-transport.js";
|
|
4
|
+
let documentHost = null;
|
|
5
|
+
/** One top-level composition owner. All Core traffic goes through this app's authenticated adapter. */
|
|
6
|
+
export function createUiHost(options = {}) {
|
|
7
|
+
if (typeof window === "undefined" || window.parent !== window)
|
|
8
|
+
throw new Error("A panel host belongs to a top-level browser document.");
|
|
9
|
+
if (documentHost)
|
|
10
|
+
throw new Error("A Hosty panel host already owns this document.");
|
|
11
|
+
const host = new UiHost(uiPath(options.uiPath), options.active);
|
|
12
|
+
documentHost = host;
|
|
13
|
+
return host;
|
|
14
|
+
}
|
|
15
|
+
export class UiHost {
|
|
16
|
+
path;
|
|
17
|
+
active;
|
|
18
|
+
snapshot = { bootstrap: null, catalog: [], tools: [], selected: null, expanded: false, width: 360, error: null };
|
|
19
|
+
environment = { mode: "standalone", connection: "connecting" };
|
|
20
|
+
windowId = createPresentationRequestId();
|
|
21
|
+
listeners = new Set();
|
|
22
|
+
events = new Set();
|
|
23
|
+
frames = new Set();
|
|
24
|
+
ledger = new Map();
|
|
25
|
+
toolQueues = new Map();
|
|
26
|
+
selectionGeneration = 0;
|
|
27
|
+
activityGeneration = 0;
|
|
28
|
+
activityReady;
|
|
29
|
+
disposed = false;
|
|
30
|
+
workspace = null;
|
|
31
|
+
refreshing = null;
|
|
32
|
+
onMessage = (event) => { void this.receive(event); };
|
|
33
|
+
onFocus = () => { void this.refresh(); };
|
|
34
|
+
constructor(path, active = true) {
|
|
35
|
+
this.path = path;
|
|
36
|
+
this.active = active;
|
|
37
|
+
this.activityReady = active;
|
|
38
|
+
window.addEventListener("message", this.onMessage);
|
|
39
|
+
window.addEventListener("focus", this.onFocus);
|
|
40
|
+
if (active)
|
|
41
|
+
void this.refresh();
|
|
42
|
+
else
|
|
43
|
+
this.setEnvironment("blocked", "activity_required");
|
|
44
|
+
}
|
|
45
|
+
getEnvironment = () => this.environment;
|
|
46
|
+
setActive(active) {
|
|
47
|
+
if (this.disposed || this.active === active)
|
|
48
|
+
return;
|
|
49
|
+
this.active = active;
|
|
50
|
+
this.activityReady = false;
|
|
51
|
+
this.activityGeneration++;
|
|
52
|
+
this.selectionGeneration++;
|
|
53
|
+
this.refreshing = null;
|
|
54
|
+
this.toolQueues.clear();
|
|
55
|
+
for (const [key, entry] of this.ledger)
|
|
56
|
+
if (entry.pending || entry.frame)
|
|
57
|
+
this.ledger.delete(key);
|
|
58
|
+
for (const frame of this.frames) {
|
|
59
|
+
frame.connected = false;
|
|
60
|
+
frame.actor = null;
|
|
61
|
+
frame.nonce = null;
|
|
62
|
+
frame.onConnection?.(false);
|
|
63
|
+
frame.generation = createPresentationRequestId();
|
|
64
|
+
}
|
|
65
|
+
this.setEnvironment(active ? "connecting" : "blocked", active ? undefined : "activity_required");
|
|
66
|
+
if (active)
|
|
67
|
+
void this.refresh();
|
|
68
|
+
}
|
|
69
|
+
checkActivity(generation, renewing = false) {
|
|
70
|
+
if (this.disposed || !this.active || generation !== this.activityGeneration || !renewing && !this.activityReady)
|
|
71
|
+
throw new UiError("activity_required", "Renew this app's activity before opening panels.");
|
|
72
|
+
}
|
|
73
|
+
claimWorkspace = (owner) => {
|
|
74
|
+
if (this.workspace && this.workspace !== owner)
|
|
75
|
+
throw new Error("Exactly one panel workspace may occupy this host.");
|
|
76
|
+
this.workspace = owner;
|
|
77
|
+
return () => { if (this.workspace === owner)
|
|
78
|
+
this.workspace = null; };
|
|
79
|
+
};
|
|
80
|
+
getSnapshot = () => this.snapshot;
|
|
81
|
+
subscribeSnapshot = (listener) => { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; };
|
|
82
|
+
subscribe = (listener) => {
|
|
83
|
+
this.events.add(listener);
|
|
84
|
+
listener({ type: "environment", value: this.environment });
|
|
85
|
+
return () => { this.events.delete(listener); };
|
|
86
|
+
};
|
|
87
|
+
emit(event) { if (!this.disposed)
|
|
88
|
+
for (const listener of this.events)
|
|
89
|
+
listener(event); }
|
|
90
|
+
update(patch) {
|
|
91
|
+
if (this.disposed)
|
|
92
|
+
return;
|
|
93
|
+
this.snapshot = { ...this.snapshot, ...patch };
|
|
94
|
+
for (const listener of this.listeners)
|
|
95
|
+
listener();
|
|
96
|
+
}
|
|
97
|
+
setEnvironment(connection, code) {
|
|
98
|
+
this.environment = { mode: "standalone", connection, ...(code ? { code } : {}) };
|
|
99
|
+
this.emit({ type: "environment", value: this.environment });
|
|
100
|
+
}
|
|
101
|
+
async bootstrap(generation = this.activityGeneration, renewing = false) {
|
|
102
|
+
this.checkActivity(generation, renewing);
|
|
103
|
+
const value = await uiRequest(this.path, "bootstrap");
|
|
104
|
+
this.checkActivity(generation, renewing);
|
|
105
|
+
if (value.version !== 1 || !value.app?.installationId || !value.userId)
|
|
106
|
+
throw new UiError("unsupported_app", "Unsupported panel metadata.");
|
|
107
|
+
const previous = this.snapshot.bootstrap;
|
|
108
|
+
if (previous && (previous.userId !== value.userId || previous.coreOrigin !== value.coreOrigin || previous.app.installationId !== value.app.installationId)) {
|
|
109
|
+
this.ledger.clear();
|
|
110
|
+
this.toolQueues.clear();
|
|
111
|
+
for (const frame of this.frames) {
|
|
112
|
+
frame.connected = false;
|
|
113
|
+
frame.actor = null;
|
|
114
|
+
frame.nonce = null;
|
|
115
|
+
frame.onConnection?.(false);
|
|
116
|
+
frame.generation = createPresentationRequestId();
|
|
117
|
+
this.post(frame, { type: "hosty:ui:probe-request" });
|
|
118
|
+
}
|
|
119
|
+
this.update({ tools: [], selected: null, expanded: false });
|
|
120
|
+
}
|
|
121
|
+
this.update({ bootstrap: value });
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
refresh = () => {
|
|
125
|
+
if (!this.active || this.disposed)
|
|
126
|
+
return Promise.resolve();
|
|
127
|
+
if (this.refreshing)
|
|
128
|
+
return this.refreshing;
|
|
129
|
+
const generation = this.activityGeneration;
|
|
130
|
+
const operation = (async () => {
|
|
131
|
+
try {
|
|
132
|
+
const own = await this.bootstrap(generation, true);
|
|
133
|
+
this.checkActivity(generation, true);
|
|
134
|
+
if (!own.canHost) {
|
|
135
|
+
this.setEnvironment("unsupported", "app_permission_required");
|
|
136
|
+
this.update({ catalog: [], tools: [], selected: null, expanded: false });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const catalog = await uiRequest(this.path, "panels");
|
|
140
|
+
this.checkActivity(generation, true);
|
|
141
|
+
if (!Array.isArray(catalog.panels))
|
|
142
|
+
throw new UiError("ui_response_invalid", "Invalid panel catalog.");
|
|
143
|
+
this.activityReady = true;
|
|
144
|
+
this.update({ catalog: catalog.panels, error: null });
|
|
145
|
+
this.setEnvironment("connected");
|
|
146
|
+
// Catalog disappearance/replacement is access loss, while temporary readiness loss retains drafts.
|
|
147
|
+
const allowed = new Map(catalog.panels.map(p => [panelKey(p.panel), p]));
|
|
148
|
+
this.update({ tools: this.snapshot.tools.map(tool => {
|
|
149
|
+
const entry = allowed.get(tool.key);
|
|
150
|
+
const invalid = !entry || !["running", "degraded", "unavailable", "starting", "stopping"].includes(entry.state);
|
|
151
|
+
const surface = tool.resolved.surface;
|
|
152
|
+
const changed = entry && surface && JSON.stringify(entry.framePolicy) !== JSON.stringify(surface.framePolicy);
|
|
153
|
+
const moved = entry && surface && (entry.origin !== surface.origin || entry.endpoint !== surface.endpoint
|
|
154
|
+
|| !routeWithin(surface.route, entry.route));
|
|
155
|
+
return invalid || changed || moved ? { ...tool, resolved: { ...tool.resolved, available: false, surface: null,
|
|
156
|
+
state: !entry ? "access_unavailable" : changed ? "policy_changed" : moved ? "destination_changed" : entry.state } }
|
|
157
|
+
: entry && surface ? { ...tool, resolved: { ...tool.resolved, available: entry.available, state: entry.state } } : tool;
|
|
158
|
+
}) });
|
|
159
|
+
for (const frame of this.frames)
|
|
160
|
+
this.post(frame, { type: "hosty:ui:probe-request" });
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (!this.active || this.disposed || generation !== this.activityGeneration)
|
|
164
|
+
return;
|
|
165
|
+
const code = error instanceof UiError ? error.code : "ui_unavailable";
|
|
166
|
+
this.update({ error: { code, message: error instanceof Error ? error.message : "Panel metadata is unavailable." } });
|
|
167
|
+
this.setEnvironment(code.startsWith("unsupported") ? "unsupported" : "blocked", code);
|
|
168
|
+
}
|
|
169
|
+
})().finally(() => { if (this.refreshing === operation)
|
|
170
|
+
this.refreshing = null; });
|
|
171
|
+
this.refreshing = operation;
|
|
172
|
+
return operation;
|
|
173
|
+
};
|
|
174
|
+
openSurface = (target, options) => waitForPresentation(this.open(target, options, "local"), options.requestId, options.signal);
|
|
175
|
+
open(target, options, source, frame) {
|
|
176
|
+
const id = options.requestId;
|
|
177
|
+
if (!this.active || !this.activityReady || this.disposed)
|
|
178
|
+
return Promise.resolve({ status: "blocked", requestId: id, code: "activity_required" });
|
|
179
|
+
const generation = this.activityGeneration;
|
|
180
|
+
if (!validId(id) || !validTarget(target) || options.placement !== "side")
|
|
181
|
+
return Promise.resolve({ status: "rejected", requestId: id, code: "invalid_presentation" });
|
|
182
|
+
const key = `${source}:${id}`, input = JSON.stringify([target.panel.appId, target.panel.installationId, target.panel.panelId, target.route ?? null]);
|
|
183
|
+
const existing = this.ledger.get(key);
|
|
184
|
+
if (existing && existing.input !== input)
|
|
185
|
+
return Promise.resolve({ status: "rejected", requestId: id, code: "request_conflict" });
|
|
186
|
+
if (existing?.pending)
|
|
187
|
+
return existing.pending;
|
|
188
|
+
if ([...this.ledger.entries()].filter(([k, v]) => k.startsWith(source + ":") && v.pending).length >= 4)
|
|
189
|
+
return Promise.resolve({ status: "blocked", requestId: id, code: "presentation_busy" });
|
|
190
|
+
const entry = { input, frame };
|
|
191
|
+
const selectionGeneration = ++this.selectionGeneration;
|
|
192
|
+
const currentSource = () => !frame || source === `${frame.surface.appId}:${frame.generation}:${frame.nonce}`;
|
|
193
|
+
const toolKey = panelKey(target.panel), previous = this.toolQueues.get(toolKey);
|
|
194
|
+
const operation = (async () => {
|
|
195
|
+
if (previous)
|
|
196
|
+
await previous;
|
|
197
|
+
try {
|
|
198
|
+
const own = await this.bootstrap(generation);
|
|
199
|
+
this.checkActivity(generation);
|
|
200
|
+
if (!own.canHost)
|
|
201
|
+
throw new UiError("app_permission_required", "Allow this app to host panels.");
|
|
202
|
+
if (frame && (!frame.live || !frame.connected || frame.actor !== own.userId || !currentSource()))
|
|
203
|
+
throw new UiError("actor_mismatch", "Reconnect using the same account.");
|
|
204
|
+
const resolved = await uiRequest(this.path, "panels/resolve", target);
|
|
205
|
+
this.checkActivity(generation);
|
|
206
|
+
if (this.disposed || this.snapshot.bootstrap?.userId !== own.userId || this.snapshot.bootstrap?.coreOrigin !== own.coreOrigin || this.snapshot.bootstrap?.app.installationId !== own.app.installationId
|
|
207
|
+
|| frame && (!frame.live || !frame.connected || frame.actor !== own.userId || !currentSource()))
|
|
208
|
+
throw new UiError("actor_mismatch", "The signed-in account changed.");
|
|
209
|
+
if (!resolved.available || !resolved.surface)
|
|
210
|
+
throw new UiError(resolved.state || "destination_unavailable", "The selected panel is unavailable.");
|
|
211
|
+
const insecure = window.location.protocol === "https:" && new URL(resolved.surface.origin).protocol === "http:";
|
|
212
|
+
if (selectionGeneration !== this.selectionGeneration)
|
|
213
|
+
throw new UiError("presentation_superseded", "A newer panel action replaced this opening. Retry to open it again.");
|
|
214
|
+
const old = this.snapshot.tools.find(t => t.key === toolKey);
|
|
215
|
+
const same = old?.resolved.surface && JSON.stringify(old.resolved.surface) === JSON.stringify(resolved.surface);
|
|
216
|
+
const tool = { key: toolKey, target: resolved.target, resolved,
|
|
217
|
+
readiness: insecure ? "unreported" : same ? old.readiness : "loading", attention: old?.attention ?? 0 };
|
|
218
|
+
this.update({ tools: old ? this.snapshot.tools.map(t => t.key === toolKey ? tool : t) : [...this.snapshot.tools, tool], selected: target.panel, expanded: true });
|
|
219
|
+
return { status: "opened", requestId: id, target: resolved.target, readiness: tool.readiness };
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
return failure(id, error);
|
|
223
|
+
}
|
|
224
|
+
})();
|
|
225
|
+
entry.pending = operation;
|
|
226
|
+
this.ledger.set(key, entry);
|
|
227
|
+
this.toolQueues.set(toolKey, operation);
|
|
228
|
+
void operation.then(result => {
|
|
229
|
+
entry.pending = undefined;
|
|
230
|
+
entry.result = result;
|
|
231
|
+
if (this.toolQueues.get(toolKey) === operation)
|
|
232
|
+
this.toolQueues.delete(toolKey);
|
|
233
|
+
if (!frame && this.active && generation === this.activityGeneration)
|
|
234
|
+
this.emit({ type: "presentation", value: result });
|
|
235
|
+
const keys = [...this.ledger.keys()].filter(k => k.startsWith(source + ":"));
|
|
236
|
+
for (const oldKey of keys.slice(0, Math.max(0, keys.length - 256)))
|
|
237
|
+
if (!this.ledger.get(oldKey)?.pending)
|
|
238
|
+
this.ledger.delete(oldKey);
|
|
239
|
+
});
|
|
240
|
+
return operation;
|
|
241
|
+
}
|
|
242
|
+
getPresentation = (id, options) => waitForPresentation(this.status(id, "local"), id, options?.signal);
|
|
243
|
+
async status(id, source, frame) {
|
|
244
|
+
const generation = this.activityGeneration;
|
|
245
|
+
try {
|
|
246
|
+
const own = await this.bootstrap(generation);
|
|
247
|
+
const check = () => {
|
|
248
|
+
this.checkActivity(generation);
|
|
249
|
+
if (!own.canHost)
|
|
250
|
+
throw new UiError("app_permission_required", "Allow this app to host panels.");
|
|
251
|
+
if (this.snapshot.bootstrap?.userId !== own.userId || this.snapshot.bootstrap?.coreOrigin !== own.coreOrigin
|
|
252
|
+
|| this.snapshot.bootstrap?.app.installationId !== own.app.installationId || frame && (!frame.live || !frame.connected || frame.actor !== own.userId
|
|
253
|
+
|| source !== `${frame.surface.appId}:${frame.generation}:${frame.nonce}`))
|
|
254
|
+
throw new UiError("actor_mismatch", "Reconnect using the same account.");
|
|
255
|
+
};
|
|
256
|
+
check();
|
|
257
|
+
const entry = this.ledger.get(`${source}:${id}`);
|
|
258
|
+
const result = entry?.pending ? await entry.pending : entry?.result;
|
|
259
|
+
check();
|
|
260
|
+
if (result?.status === "opened") {
|
|
261
|
+
const current = await uiRequest(this.path, "panels/resolve", result.target);
|
|
262
|
+
check();
|
|
263
|
+
if (!current.available)
|
|
264
|
+
return { status: "blocked", requestId: id, code: current.state };
|
|
265
|
+
const tool = this.snapshot.tools.find(t => t.key === panelKey(result.target.panel));
|
|
266
|
+
if (!tool || tool.target.route !== result.target.route || JSON.stringify(tool.resolved.surface) !== JSON.stringify(current.surface))
|
|
267
|
+
return { status: "blocked", requestId: id, code: "destination_changed" };
|
|
268
|
+
return { ...result, readiness: tool.readiness };
|
|
269
|
+
}
|
|
270
|
+
return result ?? { status: "unknown", requestId: id, code: "presentation_unknown" };
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
return failure(id, error);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
selectPanel = async (ref) => {
|
|
277
|
+
const opened = this.snapshot.tools.find(t => t.key === panelKey(ref));
|
|
278
|
+
return this.openSurface(opened?.resolved.surface ? opened.target : { panel: ref }, { placement: "side", requestId: createPresentationRequestId() });
|
|
279
|
+
};
|
|
280
|
+
setExpanded = (expanded) => {
|
|
281
|
+
if (expanded && (!this.active || !this.activityReady))
|
|
282
|
+
return;
|
|
283
|
+
if (!expanded)
|
|
284
|
+
this.selectionGeneration++;
|
|
285
|
+
this.update({ expanded });
|
|
286
|
+
};
|
|
287
|
+
setWidth = (width) => { if (Number.isFinite(width))
|
|
288
|
+
this.update({ width: Math.min(800, Math.max(280, width)) }); };
|
|
289
|
+
setAttention = (panel, attention) => this.update({ tools: this.snapshot.tools.map(t => t.key === panelKey(panel) ? { ...t, attention: Math.max(0, Math.floor(attention)) } : t) });
|
|
290
|
+
reportSurfaceState = (_state) => { };
|
|
291
|
+
forgetFrameRequests(frame) {
|
|
292
|
+
for (const [key, entry] of this.ledger)
|
|
293
|
+
if (entry.frame === frame)
|
|
294
|
+
this.ledger.delete(key);
|
|
295
|
+
}
|
|
296
|
+
registerFrame = (element, surface, onConnection) => {
|
|
297
|
+
const frame = { element, surface, onConnection, generation: createPresentationRequestId(), nonce: null, connected: false, actor: null, live: true, cleanup: () => { }, waitForReadiness: () => { } };
|
|
298
|
+
let timer;
|
|
299
|
+
const forget = () => this.forgetFrameRequests(frame);
|
|
300
|
+
frame.waitForReadiness = () => {
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
timer = setTimeout(() => {
|
|
303
|
+
const tool = this.snapshot.tools.find(t => t.target.panel.panelId === frame.surface.panelId && t.target.panel.appId === frame.surface.appId);
|
|
304
|
+
if (tool?.readiness === "loading")
|
|
305
|
+
this.reportFrame(frame, "unreported");
|
|
306
|
+
}, DISCOVERY_TIMEOUT);
|
|
307
|
+
};
|
|
308
|
+
const loaded = () => {
|
|
309
|
+
forget();
|
|
310
|
+
frame.generation = createPresentationRequestId();
|
|
311
|
+
frame.nonce = null;
|
|
312
|
+
frame.connected = false;
|
|
313
|
+
frame.actor = null;
|
|
314
|
+
frame.onConnection?.(false);
|
|
315
|
+
this.reportFrame(frame, "loading");
|
|
316
|
+
this.post(frame, { type: "hosty:ui:probe-request" });
|
|
317
|
+
frame.waitForReadiness();
|
|
318
|
+
};
|
|
319
|
+
frame.cleanup = () => { frame.live = false; forget(); clearTimeout(timer); element.removeEventListener("load", loaded); this.frames.delete(frame); };
|
|
320
|
+
this.frames.add(frame);
|
|
321
|
+
element.addEventListener("load", loaded);
|
|
322
|
+
loaded();
|
|
323
|
+
return frame.cleanup;
|
|
324
|
+
};
|
|
325
|
+
post(frame, value) {
|
|
326
|
+
if (!frame.live || this.disposed || !this.active || !this.activityReady)
|
|
327
|
+
return;
|
|
328
|
+
frame.element.contentWindow?.postMessage({ version: 1, hostWindowId: this.windowId, generation: frame.generation,
|
|
329
|
+
childNonce: frame.nonce, ...value }, frame.surface.origin);
|
|
330
|
+
}
|
|
331
|
+
reportFrame(frame, readiness) {
|
|
332
|
+
if (!frame.surface.panelId || !this.active || !this.activityReady)
|
|
333
|
+
return;
|
|
334
|
+
const tools = this.snapshot.tools.map(t => t.resolved.surface?.appId === frame.surface.appId
|
|
335
|
+
&& t.target.panel.panelId === frame.surface.panelId
|
|
336
|
+
&& t.resolved.surface.installationId === frame.surface.installationId && t.resolved.surface.endpoint === frame.surface.endpoint
|
|
337
|
+
&& t.target.route === frame.surface.route ? { ...t, readiness } : t);
|
|
338
|
+
this.update({ tools });
|
|
339
|
+
for (const [key, entry] of this.ledger) {
|
|
340
|
+
const result = entry.result;
|
|
341
|
+
if (result?.status !== "opened")
|
|
342
|
+
continue;
|
|
343
|
+
const tool = tools.find(t => t.key === panelKey(result.target.panel) && t.target.route === result.target.route);
|
|
344
|
+
if (!tool || tool.readiness === result.readiness)
|
|
345
|
+
continue;
|
|
346
|
+
entry.result = { ...result, readiness: tool.readiness };
|
|
347
|
+
if (entry.frame?.live)
|
|
348
|
+
this.post(entry.frame, { type: "hosty:ui:result", requestId: result.requestId, result: entry.result });
|
|
349
|
+
else if (key.startsWith("local:"))
|
|
350
|
+
this.emit({ type: "presentation", value: entry.result });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async receive(event) {
|
|
354
|
+
if (!this.active || !this.activityReady || this.disposed)
|
|
355
|
+
return;
|
|
356
|
+
const msg = readUiMessage(event.data);
|
|
357
|
+
if (!msg)
|
|
358
|
+
return;
|
|
359
|
+
const frame = [...this.frames].find(f => f.live && f.element.contentWindow === event.source && f.surface.origin === event.origin);
|
|
360
|
+
const own = this.snapshot.bootstrap;
|
|
361
|
+
if (!frame || !own?.canHost)
|
|
362
|
+
return;
|
|
363
|
+
if (msg.type === "hosty:ui:probe" && validId(msg.childNonce) && Array.isArray(msg.versions) && msg.versions.includes(1)) {
|
|
364
|
+
if (frame.nonce !== msg.childNonce) {
|
|
365
|
+
this.forgetFrameRequests(frame);
|
|
366
|
+
frame.connected = false;
|
|
367
|
+
frame.actor = null;
|
|
368
|
+
frame.onConnection?.(false);
|
|
369
|
+
}
|
|
370
|
+
frame.nonce = msg.childNonce;
|
|
371
|
+
this.post(frame, { type: "hosty:ui:offer" });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (msg.hostWindowId !== this.windowId || msg.generation !== frame.generation || msg.childNonce !== frame.nonce || !frame.nonce)
|
|
375
|
+
return;
|
|
376
|
+
// This is advisory state, accepted only from the mounted origin/document. It grants no authority.
|
|
377
|
+
if (msg.type === "hosty:ui:auth-required") {
|
|
378
|
+
frame.connected = false;
|
|
379
|
+
frame.actor = null;
|
|
380
|
+
frame.onConnection?.(false);
|
|
381
|
+
this.reportFrame(frame, "auth-required");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (msg.type === "hosty:ui:accept") {
|
|
385
|
+
const wasConnected = frame.connected;
|
|
386
|
+
frame.connected = msg.coreOrigin === own.coreOrigin && msg.userId === own.userId
|
|
387
|
+
&& msg.appId === frame.surface.appId && msg.installationId === frame.surface.installationId;
|
|
388
|
+
frame.actor = frame.connected ? own.userId : null;
|
|
389
|
+
if (frame.connected) {
|
|
390
|
+
this.reportFrame(frame, "loading");
|
|
391
|
+
frame.waitForReadiness();
|
|
392
|
+
}
|
|
393
|
+
this.post(frame, { type: "hosty:ui:connected", coreOrigin: own.coreOrigin, userId: own.userId,
|
|
394
|
+
appId: own.app.appId, installationId: own.app.installationId, code: frame.connected ? null : "actor_mismatch", route: frame.surface.route });
|
|
395
|
+
if (frame.connected !== wasConnected)
|
|
396
|
+
frame.onConnection?.(frame.connected);
|
|
397
|
+
if (!frame.connected)
|
|
398
|
+
this.reportFrame(frame, "auth-required");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (!frame.connected)
|
|
402
|
+
return;
|
|
403
|
+
const source = `${frame.surface.appId}:${frame.generation}:${frame.nonce}`;
|
|
404
|
+
if (msg.type === "hosty:ui:open" && validId(msg.requestId)) {
|
|
405
|
+
const result = await this.open(msg.target, { placement: msg.placement, requestId: msg.requestId }, source, frame);
|
|
406
|
+
if (source === `${frame.surface.appId}:${frame.generation}:${frame.nonce}`)
|
|
407
|
+
this.post(frame, { type: "hosty:ui:result", requestId: msg.requestId, result });
|
|
408
|
+
}
|
|
409
|
+
else if (msg.type === "hosty:ui:status" && validId(msg.requestId)) {
|
|
410
|
+
const result = await this.status(msg.requestId, source, frame);
|
|
411
|
+
if (source === `${frame.surface.appId}:${frame.generation}:${frame.nonce}`)
|
|
412
|
+
this.post(frame, { type: "hosty:ui:result", requestId: msg.requestId, result });
|
|
413
|
+
}
|
|
414
|
+
else if (msg.type === "hosty:ui:surface-state" && canonicalSurfaceRoute(msg.route) !== null
|
|
415
|
+
&& canonicalSurfaceRoute(msg.route) === canonicalSurfaceRoute(frame.surface.route)) {
|
|
416
|
+
if (msg.state === "ready")
|
|
417
|
+
this.reportFrame(frame, "ready");
|
|
418
|
+
else if (msg.state === "auth-required" || msg.state === "blocked")
|
|
419
|
+
this.reportFrame(frame, "auth-required");
|
|
420
|
+
}
|
|
421
|
+
else if (msg.type === "hosty:ui:dismiss") {
|
|
422
|
+
const selected = this.snapshot.tools.find(t => this.snapshot.selected && t.key === panelKey(this.snapshot.selected));
|
|
423
|
+
if (selected?.resolved.surface?.appId === frame.surface.appId && selected.target.panel.panelId === frame.surface.panelId && selected.target.route === frame.surface.route)
|
|
424
|
+
window.dispatchEvent(new CustomEvent("hosty:ui:drawer-dismiss"));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
dispose = () => {
|
|
428
|
+
this.disposed = true;
|
|
429
|
+
for (const frame of this.frames)
|
|
430
|
+
frame.cleanup();
|
|
431
|
+
window.removeEventListener("message", this.onMessage);
|
|
432
|
+
window.removeEventListener("focus", this.onFocus);
|
|
433
|
+
this.listeners.clear();
|
|
434
|
+
this.events.clear();
|
|
435
|
+
this.ledger.clear();
|
|
436
|
+
if (documentHost === this)
|
|
437
|
+
documentHost = null;
|
|
438
|
+
};
|
|
439
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type UiClient } from "./ui.js";
|
|
3
|
+
import { type UiHost } from "./ui-host.js";
|
|
4
|
+
import { type HostyAppFrameProps } from "./ui-frame.js";
|
|
5
|
+
import { type PanelTarget, type UiPanel } from "./ui-types.js";
|
|
6
|
+
export { HostyAppFrame, type HostyAppFrameProps } from "./ui-frame.js";
|
|
7
|
+
export declare function useHostyUi(): UiClient;
|
|
8
|
+
export declare function useHostyUiHost(): UiHost | null;
|
|
9
|
+
/** Internal composition root, owned by HostyOverlay rather than mounted again by domain apps. */
|
|
10
|
+
export declare function HostyUiRoot({ children, uiPath, panelLayout, active }: {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
uiPath?: string;
|
|
13
|
+
panelLayout?: "auto" | "slot" | "none";
|
|
14
|
+
active?: boolean;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
export type HostyPanelWorkspaceProps = {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
railMode?: "opened" | "catalog";
|
|
19
|
+
className?: string;
|
|
20
|
+
initialWidth?: number;
|
|
21
|
+
initialExpanded?: boolean;
|
|
22
|
+
onWidthChange?: (width: number) => void;
|
|
23
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
24
|
+
onStartApp?: (appId: string) => void | Promise<void>;
|
|
25
|
+
onAskAssistant?: HostyAppFrameProps["onAskAssistant"];
|
|
26
|
+
onAuthRequired?: HostyAppFrameProps["onAuthRequired"];
|
|
27
|
+
outbound?: {
|
|
28
|
+
target: PanelTarget;
|
|
29
|
+
message: unknown;
|
|
30
|
+
nonce: number;
|
|
31
|
+
} | null;
|
|
32
|
+
renderIcon?: (panel: UiPanel) => ReactNode;
|
|
33
|
+
};
|
|
34
|
+
/** A single retained frame tree for dock and drawer; only CSS placement changes at the breakpoint. */
|
|
35
|
+
export declare function HostyPanelWorkspace({ children, railMode, className, initialWidth, initialExpanded, onWidthChange, onExpandedChange, onStartApp, onAskAssistant, onAuthRequired, outbound, renderIcon }: HostyPanelWorkspaceProps): import("react").JSX.Element;
|