@ai-matrx/applets 0.2.7

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/frame.js ADDED
@@ -0,0 +1,958 @@
1
+ "use client";
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
7
+
8
+ // src/frame/index.tsx
9
+ import * as React7 from "react";
10
+ import { compile, prepareScope, provideScopeModules } from "@ai-matrx/code-runtime";
11
+
12
+ // src/react/index.ts
13
+ var react_exports = {};
14
+ __export(react_exports, {
15
+ Applet: () => Applet,
16
+ AppletFrame: () => AppletFrame,
17
+ DataPage: () => DataPage,
18
+ Kind: () => Kind,
19
+ Link: () => Link,
20
+ Pages: () => Pages,
21
+ defineActions: () => defineActions,
22
+ matchPage: () => matchPage,
23
+ navigate: () => navigate,
24
+ newQueryClient: () => newQueryClient,
25
+ useAction: () => useAction,
26
+ useColumns: () => useColumns,
27
+ useJob: () => useJob,
28
+ useJobs: () => useJobs,
29
+ usePage: () => usePage,
30
+ useRow: () => useRow,
31
+ useRows: () => useRows,
32
+ useSources: () => useSources,
33
+ useValue: () => useValue
34
+ });
35
+
36
+ // src/react/context.tsx
37
+ import * as React from "react";
38
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
39
+ import { DataPortProvider } from "@ai-matrx/records/data/react";
40
+
41
+ // src/types.ts
42
+ var PAGE_ACTION_STREAM = "actions";
43
+ var NAV_STREAM = "nav";
44
+ var runStream = (requestId) => `run:${requestId}`;
45
+ var valueStream = (name) => `value:${name}`;
46
+ function fail(code, message, retryable = false, diagnostic) {
47
+ const error = { code, message, retryable };
48
+ if (diagnostic !== void 0) error.diagnostic = diagnostic;
49
+ return { ok: false, error };
50
+ }
51
+ function fromRecordsError(error) {
52
+ const out = { code: error.code, message: error.message, retryable: error.code === "unreachable" || error.code === "timed_out" };
53
+ if (error.diagnostic !== void 0) out.diagnostic = error.diagnostic;
54
+ return out;
55
+ }
56
+ function toAppletError(err, code = "internal") {
57
+ if (err && typeof err === "object") {
58
+ const o = err;
59
+ if (typeof o.code === "string" && typeof o.message === "string") {
60
+ const out = { code: o.code, message: o.message, retryable: o.retryable === true };
61
+ if (typeof o.diagnostic === "string") out.diagnostic = o.diagnostic;
62
+ return out;
63
+ }
64
+ if (err instanceof Error) return { code, message: err.message, retryable: false };
65
+ }
66
+ return { code, message: String(err), retryable: false };
67
+ }
68
+
69
+ // src/react/context.tsx
70
+ import { jsx } from "react/jsx-runtime";
71
+ var EventHub = class {
72
+ constructor(channel) {
73
+ this.channel = channel;
74
+ }
75
+ channel;
76
+ streams = /* @__PURE__ */ new Map();
77
+ bySubscription = /* @__PURE__ */ new Map();
78
+ detach = null;
79
+ subscribe(stream, listener) {
80
+ if (!this.detach) this.detach = this.channel.receive((m) => this.deliver(m));
81
+ let entry = this.streams.get(stream);
82
+ if (!entry) {
83
+ const created = { listeners: /* @__PURE__ */ new Set(), subscriptionId: null, ready: Promise.resolve() };
84
+ created.ready = this.channel.events.on(stream, null).then(
85
+ ({ subscriptionId }) => {
86
+ if (this.streams.get(stream) !== created) {
87
+ void this.channel.events.off(subscriptionId);
88
+ return;
89
+ }
90
+ created.subscriptionId = subscriptionId;
91
+ this.bySubscription.set(subscriptionId, stream);
92
+ },
93
+ (err) => {
94
+ void this.channel.reportError({ ...toAppletError(err), where: `events.on(${stream})` });
95
+ }
96
+ );
97
+ this.streams.set(stream, created);
98
+ entry = created;
99
+ }
100
+ const current = entry;
101
+ current.listeners.add(listener);
102
+ return {
103
+ ready: current.ready,
104
+ unsubscribe: () => {
105
+ current.listeners.delete(listener);
106
+ if (current.listeners.size > 0 || this.streams.get(stream) !== current) return;
107
+ this.streams.delete(stream);
108
+ if (current.subscriptionId) {
109
+ this.bySubscription.delete(current.subscriptionId);
110
+ void this.channel.events.off(current.subscriptionId);
111
+ }
112
+ if (this.streams.size === 0 && this.detach) {
113
+ this.detach();
114
+ this.detach = null;
115
+ }
116
+ }
117
+ };
118
+ }
119
+ deliver(message) {
120
+ const stream = this.bySubscription.get(message.subscriptionId);
121
+ if (!stream) return;
122
+ for (const l of [...this.streams.get(stream)?.listeners ?? []]) l(message.payload);
123
+ }
124
+ };
125
+ function frameDataEvents(hub) {
126
+ return {
127
+ on(event, listener) {
128
+ return hub.subscribe(event, (payload) => listener(payload)).unsubscribe;
129
+ }
130
+ };
131
+ }
132
+ function dataScopeFor(channel, hub) {
133
+ return { port: channel.data, portKey: channel.hostId, events: frameDataEvents(hub) };
134
+ }
135
+ var CONTEXT_KEY = /* @__PURE__ */ Symbol.for("ai-matrx.applets.frameContext");
136
+ function frameContext() {
137
+ const g = globalThis;
138
+ let ctx = g[CONTEXT_KEY];
139
+ if (!ctx) {
140
+ ctx = React.createContext(null);
141
+ ctx.displayName = "AppletFrame";
142
+ g[CONTEXT_KEY] = ctx;
143
+ }
144
+ return ctx;
145
+ }
146
+ function newQueryClient() {
147
+ return new QueryClient({
148
+ defaultOptions: {
149
+ // Events keep the cache true (rows:<alias>, value:<name>, run:<id>), so a key is requested once.
150
+ queries: { staleTime: Number.POSITIVE_INFINITY, retry: false, refetchOnWindowFocus: false, refetchOnReconnect: false },
151
+ mutations: { retry: false }
152
+ }
153
+ });
154
+ }
155
+ var DETACHED_KEY = /* @__PURE__ */ Symbol.for("ai-matrx.applets.detached");
156
+ var NO_FRAME = {
157
+ code: "no_frame",
158
+ message: "This part of the Applet is not inside an AppletFrame.",
159
+ retryable: false
160
+ };
161
+ var NO_FRAME_DATA = { code: "not_supported", message: NO_FRAME.message, diagnostic: NO_FRAME.code };
162
+ function detachedChannel() {
163
+ const refuse = async () => ({ ok: false, error: NO_FRAME });
164
+ const refuseData = async () => ({ ok: false, error: NO_FRAME_DATA });
165
+ const reject = async () => {
166
+ throw NO_FRAME;
167
+ };
168
+ return {
169
+ hostId: "detached",
170
+ viewer: reject,
171
+ record: reject,
172
+ child: reject,
173
+ kinds: { definition: refuse, component: refuse },
174
+ pages: { definition: refuse },
175
+ data: {
176
+ sources: refuseData,
177
+ columns: refuseData,
178
+ list: refuseData,
179
+ get: refuseData,
180
+ create: refuseData,
181
+ update: refuseData,
182
+ archive: refuseData,
183
+ restore: refuseData
184
+ },
185
+ intelligence: {
186
+ describe: refuse,
187
+ run: refuse,
188
+ result: refuse,
189
+ answer: refuse,
190
+ rejoin: refuse,
191
+ listRuns: refuse,
192
+ createSoft: refuse
193
+ },
194
+ surface: {
195
+ mount: refuse,
196
+ unmount: async () => void 0,
197
+ getValue: async () => void 0,
198
+ setValue: reject,
199
+ listActions: async () => [],
200
+ runAction: refuse,
201
+ completeAction: async () => void 0
202
+ },
203
+ nav: { go: reject, current: async () => ({ path: [], params: {} }) },
204
+ events: { on: reject, off: async () => void 0 },
205
+ reportError: async () => void 0,
206
+ receive: () => () => void 0,
207
+ forHost: () => null
208
+ };
209
+ }
210
+ function detachedFrame() {
211
+ const g = globalThis;
212
+ let frame = g[DETACHED_KEY];
213
+ if (!frame) {
214
+ const channel = detachedChannel();
215
+ const hub = new EventHub(channel);
216
+ frame = { channel, hub, queryClient: newQueryClient(), dataScope: dataScopeFor(channel, hub), actions: /* @__PURE__ */ new Map(), parent: null, attached: false };
217
+ g[DETACHED_KEY] = frame;
218
+ }
219
+ return frame;
220
+ }
221
+ function useFrame() {
222
+ return React.useContext(frameContext()) ?? detachedFrame();
223
+ }
224
+ function useOptionalFrame() {
225
+ return React.useContext(frameContext());
226
+ }
227
+ var keys = {
228
+ root: (hostId) => ["applets", hostId],
229
+ record: (hostId) => ["applets", hostId, "record"],
230
+ viewer: (hostId) => ["applets", hostId, "viewer"],
231
+ nav: (hostId) => ["applets", hostId, "nav"]
232
+ };
233
+ function recordQuery(channel) {
234
+ return { queryKey: keys.record(channel.hostId), queryFn: () => channel.record() };
235
+ }
236
+ function viewerQuery(channel) {
237
+ return { queryKey: keys.viewer(channel.hostId), queryFn: () => channel.viewer() };
238
+ }
239
+ var FRAMES_KEY = /* @__PURE__ */ Symbol.for("ai-matrx.applets.frames");
240
+ function liveFrames() {
241
+ const g = globalThis;
242
+ let list = g[FRAMES_KEY];
243
+ if (!list) {
244
+ list = [];
245
+ g[FRAMES_KEY] = list;
246
+ }
247
+ return list;
248
+ }
249
+ function AppletFrame({ host, queryClient, children, ...renderers }) {
250
+ const parent = useOptionalFrame();
251
+ const Context = frameContext();
252
+ const [frame] = React.useState(() => {
253
+ const hub = new EventHub(host);
254
+ const value = {
255
+ channel: host,
256
+ hub,
257
+ queryClient: queryClient ?? parent?.queryClient ?? newQueryClient(),
258
+ dataScope: dataScopeFor(host, hub),
259
+ actions: /* @__PURE__ */ new Map(),
260
+ parent,
261
+ attached: true
262
+ };
263
+ const inherit = (key) => {
264
+ const own = renderers[key] ?? (key === "renderKind" || key === "renderDataPage" ? parent?.[key] : void 0);
265
+ if (own !== void 0) value[key] = own;
266
+ };
267
+ inherit("renderKind");
268
+ inherit("renderDataPage");
269
+ inherit("renderApplet");
270
+ inherit("pageComponent");
271
+ return value;
272
+ });
273
+ React.useEffect(() => {
274
+ const frames = liveFrames();
275
+ frames.push(frame);
276
+ let cancelled = false;
277
+ let mountId = null;
278
+ void (async () => {
279
+ try {
280
+ const record = await frame.queryClient.fetchQuery(recordQuery(host));
281
+ const parentName = parent ? (await parent.queryClient.fetchQuery(recordQuery(parent.channel))).surfaceName : null;
282
+ if (cancelled) return;
283
+ const mounted = await host.surface.mount(record.surfaceName, parentName);
284
+ if (!mounted.ok) {
285
+ void host.reportError({ ...mounted.error, where: "surface.mount" });
286
+ return;
287
+ }
288
+ if (cancelled) void host.surface.unmount(mounted.data.mountId);
289
+ else mountId = mounted.data.mountId;
290
+ } catch (err) {
291
+ void host.reportError({ ...toAppletError(err), where: "AppletFrame mount" });
292
+ }
293
+ })();
294
+ const { unsubscribe } = frame.hub.subscribe(PAGE_ACTION_STREAM, (payload) => {
295
+ void routePageAction(frame, payload);
296
+ });
297
+ return () => {
298
+ cancelled = true;
299
+ unsubscribe();
300
+ if (mountId) void host.surface.unmount(mountId);
301
+ const i = frames.indexOf(frame);
302
+ if (i >= 0) frames.splice(i, 1);
303
+ };
304
+ }, [frame, host, parent]);
305
+ const { port, events, portKey } = frame.dataScope;
306
+ const body = /* @__PURE__ */ jsx(Context.Provider, { value: frame, children: /* @__PURE__ */ jsx(DataPortProvider, { port, portKey, queryClient: frame.queryClient, ...events ? { events } : {}, children }) });
307
+ return parent?.queryClient === frame.queryClient ? body : /* @__PURE__ */ jsx(QueryClientProvider, { client: frame.queryClient, children: body });
308
+ }
309
+ async function routePageAction(frame, event) {
310
+ const { channel } = frame;
311
+ const handler = frame.actions.get(event.action);
312
+ if (!handler) {
313
+ await channel.surface.completeAction(event.runId, fail("action_unhandled", `This page has no handler for "${event.action}".`));
314
+ return;
315
+ }
316
+ let viewerId = null;
317
+ try {
318
+ viewerId = (await frame.queryClient.fetchQuery(viewerQuery(channel))).userId;
319
+ } catch {
320
+ viewerId = null;
321
+ }
322
+ try {
323
+ const data = await handler(event.input, { runId: event.runId, by: event.by ?? "applet", viewerId });
324
+ await channel.surface.completeAction(event.runId, { ok: true, data });
325
+ } catch (err) {
326
+ await channel.surface.completeAction(event.runId, { ok: false, error: toAppletError(err) });
327
+ }
328
+ }
329
+
330
+ // src/react/data.ts
331
+ import * as React2 from "react";
332
+ import {
333
+ useDataColumns,
334
+ useDataMutations,
335
+ useDataRow,
336
+ useDataRows,
337
+ useDataSources
338
+ } from "@ai-matrx/records/data/react";
339
+ async function unwrap(answer) {
340
+ const r = await answer;
341
+ if (!r.ok) throw r.error;
342
+ return r.data;
343
+ }
344
+ function asAppletError(err) {
345
+ return err == null ? null : toAppletError(err);
346
+ }
347
+ function queryStatus(q, saving) {
348
+ if (q.status === "error") return "error";
349
+ if (saving) return "saving";
350
+ if (q.status === "pending") return q.fetchStatus === "idle" ? "idle" : "loading";
351
+ return "ready";
352
+ }
353
+ function toResult(answer) {
354
+ return answer.ok ? answer : { ok: false, error: fromRecordsError(answer.error) };
355
+ }
356
+ function scopeOf(frame) {
357
+ return frame.attached ? void 0 : frame.dataScope;
358
+ }
359
+ function readStatus(loading, failed, saving) {
360
+ if (failed) return "error";
361
+ if (saving) return "saving";
362
+ return loading ? "loading" : "ready";
363
+ }
364
+ function useWriter(frame, alias) {
365
+ const mutations = useDataMutations(alias, scopeOf(frame));
366
+ const [pending, setPending] = React2.useState(0);
367
+ const [writeError, setWriteError] = React2.useState(null);
368
+ const track = async (work) => {
369
+ setPending((n) => n + 1);
370
+ try {
371
+ const answer = toResult(await work());
372
+ setWriteError(answer.ok ? null : answer.error);
373
+ return answer;
374
+ } catch (err) {
375
+ const error = toAppletError(err);
376
+ setWriteError(error);
377
+ return { ok: false, error };
378
+ } finally {
379
+ setPending((n) => n - 1);
380
+ }
381
+ };
382
+ return {
383
+ saving: pending > 0,
384
+ error: writeError,
385
+ create: (values) => track(() => mutations.create(values)),
386
+ update: (id, patch, baseVersion) => track(async () => {
387
+ if (baseVersion !== void 0) return mutations.update(id, patch, { baseVersion });
388
+ const first = await mutations.update(id, patch);
389
+ if (first.ok || first.error.code !== "stale_read") return first;
390
+ const read = await frame.channel.data.get(alias, id);
391
+ if (!read.ok) return read;
392
+ if (!read.data) return { ok: false, error: { code: "not_found", message: "This record is not here any more." } };
393
+ return mutations.update(id, patch, { baseVersion: read.data._version });
394
+ }),
395
+ archive: (id) => track(() => mutations.archive(id)),
396
+ restore: (id) => track(() => mutations.restore(id))
397
+ };
398
+ }
399
+ function useRows(source, q) {
400
+ const frame = useFrame();
401
+ const list = useDataRows(source, q ?? {}, scopeOf(frame));
402
+ const writer = useWriter(frame, source);
403
+ return {
404
+ rows: list.rows,
405
+ total: list.total,
406
+ status: readStatus(list.loading, list.error !== null, writer.saving),
407
+ error: (list.error ? fromRecordsError(list.error) : null) ?? writer.error,
408
+ hasMore: list.hasMore,
409
+ loadMore: list.loadMore,
410
+ newer: list.newer,
411
+ refresh: list.refresh,
412
+ create: writer.create,
413
+ update: (id, patch) => writer.update(id, patch, list.rows.find((r) => r._id === id)?._version),
414
+ archive: writer.archive,
415
+ restore: writer.restore
416
+ };
417
+ }
418
+ function useRow(source, id) {
419
+ const frame = useFrame();
420
+ const one = useDataRow(source, id, scopeOf(frame));
421
+ const writer = useWriter(frame, source);
422
+ const row = one.data ?? null;
423
+ return {
424
+ row,
425
+ status: readStatus(one.loading, one.error !== null, writer.saving),
426
+ error: (one.error ? fromRecordsError(one.error) : null) ?? writer.error,
427
+ update: (patch) => writer.update(id, patch, row?._version),
428
+ archive: () => writer.archive(id),
429
+ refresh: one.refresh
430
+ };
431
+ }
432
+ function useColumns(source) {
433
+ const frame = useFrame();
434
+ const q = useDataColumns(source, scopeOf(frame));
435
+ return { columns: q.data ?? [], status: readStatus(q.loading, q.error !== null, false), error: q.error ? fromRecordsError(q.error) : null };
436
+ }
437
+ function useSources() {
438
+ const frame = useFrame();
439
+ const q = useDataSources(scopeOf(frame));
440
+ return { sources: q.data ?? [], status: readStatus(q.loading, q.error !== null, false), error: q.error ? fromRecordsError(q.error) : null };
441
+ }
442
+
443
+ // src/react/jobs.ts
444
+ import * as React3 from "react";
445
+ import { useQuery } from "@tanstack/react-query";
446
+ var IDLE = { status: "idle", error: null, text: "", result: null, inputs: [] };
447
+ var TERMINAL = /* @__PURE__ */ new Set(["done", "complete", "completed", "succeeded", "success"]);
448
+ function reduce(state, action) {
449
+ switch (action.type) {
450
+ case "reset":
451
+ return { ...IDLE, status: action.status };
452
+ case "fail":
453
+ return { ...state, status: "error", error: action.error };
454
+ case "status":
455
+ return { ...state, status: action.status };
456
+ case "catch-up":
457
+ if (state.status === "done") return state;
458
+ return { ...state, status: "done", result: action.envelope, text: state.text || action.text };
459
+ case "event": {
460
+ const e = action.event;
461
+ switch (e.type) {
462
+ case "text":
463
+ return { ...state, text: state.text + e.delta };
464
+ case "status":
465
+ if (TERMINAL.has(e.status)) return { ...state, status: "done" };
466
+ if (e.status === "error" || e.status === "failed") return { ...state, status: "error" };
467
+ return state.status === "done" ? state : { ...state, status: "running" };
468
+ case "needs_input":
469
+ return { ...state, status: "needs_input", inputs: e.inputs };
470
+ case "result":
471
+ return { ...state, status: "done", result: e.envelope };
472
+ case "error":
473
+ return { ...state, status: "error", error: e.error };
474
+ }
475
+ }
476
+ }
477
+ return state;
478
+ }
479
+ var runsKey = (hostId, key) => ["applets", hostId, "runs", key];
480
+ function useJob(alias) {
481
+ const frame = useFrame();
482
+ const { channel, queryClient: qc, hub } = frame;
483
+ const [state, dispatch] = React3.useReducer(reduce, IDLE);
484
+ const refRef = React3.useRef(null);
485
+ const unsubscribeRef = React3.useRef(null);
486
+ React3.useEffect(
487
+ () => () => {
488
+ unsubscribeRef.current?.();
489
+ unsubscribeRef.current = null;
490
+ },
491
+ []
492
+ );
493
+ const record = useQuery({ ...recordQuery(channel) }, qc);
494
+ const key = record.data?.mandates.find((m) => m.alias === alias)?.key ?? null;
495
+ const appletId = record.data?.id;
496
+ const runs = useQuery(
497
+ {
498
+ queryKey: runsKey(channel.hostId, key ?? ""),
499
+ queryFn: () => unwrap(channel.intelligence.listRuns(key ?? "", appletId)),
500
+ enabled: key !== null
501
+ },
502
+ qc
503
+ );
504
+ const run = async (variables, opts) => {
505
+ unsubscribeRef.current?.();
506
+ unsubscribeRef.current = null;
507
+ dispatch({ type: "reset", status: "resolving" });
508
+ try {
509
+ const rec = await qc.fetchQuery(recordQuery(channel));
510
+ const mandate = rec.mandates.find((m) => m.alias === alias);
511
+ if (!mandate) {
512
+ const error = { code: "job_not_found", message: `This Applet has no job named "${alias}".`, retryable: false };
513
+ dispatch({ type: "fail", error });
514
+ return { ok: false, error };
515
+ }
516
+ const viewer = await qc.fetchQuery(viewerQuery(channel));
517
+ const req = {
518
+ key: mandate.key,
519
+ organizationId: viewer.activeOrganizationId,
520
+ appletId: rec.id,
521
+ surfaceName: rec.surfaceName
522
+ };
523
+ if (variables !== void 0) req.variables = variables;
524
+ if (opts?.userInput !== void 0) req.userInput = opts.userInput;
525
+ const started = await channel.intelligence.run(req);
526
+ if (!started.ok) {
527
+ dispatch({ type: "fail", error: started.error });
528
+ return started;
529
+ }
530
+ const ref = started.data;
531
+ refRef.current = ref;
532
+ dispatch({ type: "status", status: "running" });
533
+ let finished = false;
534
+ const sub = hub.subscribe(runStream(ref.requestId), (payload) => {
535
+ const event = payload;
536
+ dispatch({ type: "event", event });
537
+ if (!finished && (event.type === "result" || event.type === "error" || event.type === "status" && TERMINAL.has(event.status))) {
538
+ finished = true;
539
+ void qc.invalidateQueries({ queryKey: runsKey(channel.hostId, mandate.key) });
540
+ }
541
+ });
542
+ unsubscribeRef.current = sub.unsubscribe;
543
+ await sub.ready;
544
+ const now = await channel.intelligence.result(ref);
545
+ if (now.ok && !finished && (now.data.envelope !== null || TERMINAL.has(now.data.status))) {
546
+ finished = true;
547
+ dispatch({ type: "catch-up", text: now.data.text, envelope: now.data.envelope });
548
+ }
549
+ void qc.invalidateQueries({ queryKey: runsKey(channel.hostId, mandate.key) });
550
+ return started;
551
+ } catch (err) {
552
+ const error = toAppletError(err);
553
+ dispatch({ type: "fail", error });
554
+ return { ok: false, error };
555
+ }
556
+ };
557
+ const answer = async (values) => {
558
+ const ref = refRef.current;
559
+ if (!ref) return { ok: false, error: { code: "no_run", message: "There is no run waiting for an answer.", retryable: false } };
560
+ dispatch({ type: "status", status: "running" });
561
+ const answered = await channel.intelligence.answer(ref, values);
562
+ if (!answered.ok) dispatch({ type: "fail", error: answered.error });
563
+ return answered;
564
+ };
565
+ return { run, answer, runs: runs.data ?? [], ...state };
566
+ }
567
+ function useJobs() {
568
+ const { channel, queryClient } = useFrame();
569
+ const q = useQuery(
570
+ {
571
+ queryKey: ["applets", channel.hostId, "jobs"],
572
+ queryFn: async () => {
573
+ const rec = await queryClient.fetchQuery(recordQuery(channel));
574
+ return Promise.all(
575
+ rec.mandates.map(async (m) => {
576
+ const d = await channel.intelligence.describe(m.key);
577
+ if (!d.ok) throw d.error;
578
+ return { alias: m.alias, key: m.key, label: d.data.label, goal: d.data.goal, outputKind: d.data.outputKind };
579
+ })
580
+ );
581
+ }
582
+ },
583
+ queryClient
584
+ );
585
+ return { jobs: q.data ?? [], status: queryStatus(q, false), error: asAppletError(q.error) };
586
+ }
587
+
588
+ // src/react/surface.ts
589
+ import * as React4 from "react";
590
+ import { useQuery as useQuery2 } from "@tanstack/react-query";
591
+ var valueKey = (hostId, name) => ["applets", hostId, "value", name];
592
+ function useValue(name) {
593
+ const frame = useFrame();
594
+ const { channel, queryClient: qc, hub } = frame;
595
+ const q = useQuery2({ queryKey: valueKey(channel.hostId, name), queryFn: async () => await channel.surface.getValue(name) ?? null }, qc);
596
+ React4.useEffect(() => {
597
+ const { unsubscribe } = hub.subscribe(valueStream(name), (payload) => {
598
+ qc.setQueryData(valueKey(channel.hostId, name), payload ?? null);
599
+ });
600
+ return unsubscribe;
601
+ }, [hub, qc, channel, name]);
602
+ const set = (next) => {
603
+ qc.setQueryData(valueKey(channel.hostId, name), next);
604
+ channel.surface.setValue(name, next).catch((err) => {
605
+ void channel.reportError({ ...toAppletError(err), where: `setValue(${name})` });
606
+ });
607
+ };
608
+ return [q.data ?? void 0, set];
609
+ }
610
+ function useAction(name, options) {
611
+ const { channel } = useFrame();
612
+ const [status, setStatus] = React4.useState("idle");
613
+ const [error, setError] = React4.useState(null);
614
+ const run = async (input) => {
615
+ setStatus("loading");
616
+ try {
617
+ const answer = await channel.surface.runAction(name, input ?? null, options?.by ?? "person");
618
+ if (answer.ok) {
619
+ setError(null);
620
+ setStatus("ready");
621
+ return answer;
622
+ }
623
+ const err = { code: answer.error.code, message: answer.error.message, retryable: answer.error.retryable };
624
+ if (answer.error.diagnostic !== void 0) err.diagnostic = answer.error.diagnostic;
625
+ setError(err);
626
+ setStatus("error");
627
+ return { ok: false, error: err };
628
+ } catch (thrown) {
629
+ const err = toAppletError(thrown);
630
+ setError(err);
631
+ setStatus("error");
632
+ return { ok: false, error: err };
633
+ }
634
+ };
635
+ return { run, status, error };
636
+ }
637
+ function defineActions(handlers) {
638
+ const frame = useFrame();
639
+ const latest = React4.useRef(handlers);
640
+ latest.current = handlers;
641
+ const names = Object.keys(handlers).sort().join("\0");
642
+ React4.useEffect(() => {
643
+ const registered = names.split("\0").filter(Boolean);
644
+ for (const name of registered) {
645
+ frame.actions.set(name, (input, ctx) => {
646
+ const handler = latest.current[name];
647
+ if (!handler) throw { code: "action_unhandled", message: `This page has no handler for "${name}".`, retryable: false };
648
+ return handler(input, ctx);
649
+ });
650
+ }
651
+ return () => {
652
+ for (const name of registered) frame.actions.delete(name);
653
+ };
654
+ }, [frame, names]);
655
+ }
656
+
657
+ // src/react/nav.tsx
658
+ import * as React5 from "react";
659
+ import { useQuery as useQuery3 } from "@tanstack/react-query";
660
+ import { Select, Tabs } from "@ai-matrx/design-system/controls";
661
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
662
+ function parse(to) {
663
+ const [pathPart = "", queryPart = ""] = to.split("?");
664
+ const params = {};
665
+ for (const pair of queryPart.split("&").filter(Boolean)) {
666
+ const [k = "", v = ""] = pair.split("=");
667
+ params[decodeURIComponent(k)] = decodeURIComponent(v);
668
+ }
669
+ return { path: pathPart.split("/").filter(Boolean), params };
670
+ }
671
+ function go(frame, to) {
672
+ frame.queryClient.setQueryData(keys.nav(frame.channel.hostId), parse(to));
673
+ frame.channel.nav.go(to).catch((err) => {
674
+ void frame.channel.reportError({ ...toAppletError(err), where: `nav.go(${to})` });
675
+ });
676
+ }
677
+ function navigate(to) {
678
+ const roots = liveFrames().filter((f) => f.parent === null);
679
+ const frame = roots[roots.length - 1];
680
+ if (!frame) {
681
+ console.error(`[applets] navigate("${to}") was called with no AppletFrame mounted.`);
682
+ return;
683
+ }
684
+ go(frame, to);
685
+ }
686
+ function useLocation(frame) {
687
+ const { channel, queryClient: qc, hub } = frame;
688
+ const q = useQuery3({ queryKey: keys.nav(channel.hostId), queryFn: () => channel.nav.current() }, qc);
689
+ React5.useEffect(() => {
690
+ const { unsubscribe } = hub.subscribe(NAV_STREAM, (payload) => {
691
+ qc.setQueryData(keys.nav(channel.hostId), payload);
692
+ });
693
+ return unsubscribe;
694
+ }, [hub, qc, channel]);
695
+ return q.data ?? { path: [], params: {} };
696
+ }
697
+ function segments(path) {
698
+ return path.split("/").filter(Boolean);
699
+ }
700
+ function matchPage(pages, path) {
701
+ let best = null;
702
+ for (const page of pages) {
703
+ const want = segments(page.path);
704
+ if (want.length !== path.length) continue;
705
+ const params = {};
706
+ let score = 0;
707
+ let hit = true;
708
+ want.forEach((w, i) => {
709
+ const got = path[i] ?? "";
710
+ if (w.startsWith(":")) params[w.slice(1)] = got;
711
+ else if (w === got) score += 1;
712
+ else hit = false;
713
+ });
714
+ if (hit && (!best || score > best.score)) best = { page, params, score };
715
+ }
716
+ return best ? { page: best.page, params: best.params } : null;
717
+ }
718
+ function usePage() {
719
+ const frame = useFrame();
720
+ const location = useLocation(frame);
721
+ const record = useQuery3({ ...recordQuery(frame.channel) }, frame.queryClient);
722
+ const pages = record.data?.pages ?? [];
723
+ const matched = matchPage(pages, location.path) ?? (location.path.length === 0 && pages[0] ? { page: pages[0], params: {} } : null);
724
+ return {
725
+ path: location.path,
726
+ page: matched ? { path: matched.page.path, title: matched.page.title } : { path: `/${location.path.join("/")}`, title: "" },
727
+ params: { ...location.params, ...matched?.params ?? {} },
728
+ navigate: (to) => go(frame, to)
729
+ };
730
+ }
731
+ function Link({ to, onClick, children, ...rest }) {
732
+ const frame = useFrame();
733
+ return /* @__PURE__ */ jsx2(
734
+ "a",
735
+ {
736
+ ...rest,
737
+ href: to,
738
+ onClick: (event) => {
739
+ onClick?.(event);
740
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
741
+ event.preventDefault();
742
+ go(frame, to);
743
+ },
744
+ children
745
+ }
746
+ );
747
+ }
748
+ function topOf(pages, page) {
749
+ let current = page;
750
+ for (let guard = 0; current.parent && guard < pages.length; guard += 1) {
751
+ const up = pages.find((p) => p.path === current.parent);
752
+ if (!up) break;
753
+ current = up;
754
+ }
755
+ return current;
756
+ }
757
+ function Pages({ layout = "tabs", components }) {
758
+ const frame = useFrame();
759
+ const location = useLocation(frame);
760
+ const record = useQuery3({ ...recordQuery(frame.channel) }, frame.queryClient);
761
+ const pages = record.data?.pages ?? [];
762
+ const top = pages.filter((p) => !p.parent);
763
+ const matched = matchPage(pages, location.path)?.page ?? (location.path.length === 0 ? top[0] : void 0);
764
+ const selected = matched ? topOf(pages, matched).path : null;
765
+ const open = (path) => go(frame, path);
766
+ const Component = matched ? components?.[matched.file] ?? frame.pageComponent?.(matched.file) ?? null : null;
767
+ const body = matched ? Component ? /* @__PURE__ */ jsx2(Component, {}) : /* @__PURE__ */ jsxs("div", { "data-applet-stand-in": "page", className: "text-sm text-muted-foreground", children: [
768
+ "Page file ",
769
+ matched.file,
770
+ " is not compiled in this frame"
771
+ ] }) : record.isPending ? null : /* @__PURE__ */ jsxs("div", { "data-applet-stand-in": "no-page", className: "text-sm text-muted-foreground", children: [
772
+ "No page at /",
773
+ location.path.join("/")
774
+ ] });
775
+ let navigation = null;
776
+ if (layout === "tabs" && top.length > 0) {
777
+ navigation = /* @__PURE__ */ jsx2(Tabs, { "aria-label": "Pages", value: selected, onValueChange: open, data: top.map((p) => ({ value: p.path, label: p.title })) });
778
+ } else if (layout === "menu" && top.length > 0) {
779
+ navigation = /* @__PURE__ */ jsx2(Select, { "aria-label": "Pages", value: selected ?? top[0]?.path ?? "", onValueChange: open, options: top.map((p) => ({ value: p.path, label: p.title })) });
780
+ } else if (layout === "sidebar") {
781
+ navigation = /* @__PURE__ */ jsx2("nav", { "aria-label": "Pages", className: "flex w-48 shrink-0 flex-col gap-1 border-r border-border p-2", children: top.map((p) => /* @__PURE__ */ jsx2(
782
+ "button",
783
+ {
784
+ type: "button",
785
+ "aria-current": p.path === selected ? "page" : void 0,
786
+ "data-state": p.path === selected ? "on" : "off",
787
+ className: "rounded-md px-2 py-1 text-left text-sm text-foreground data-[state=off]:text-muted-foreground data-[state=on]:bg-muted",
788
+ onClick: () => open(p.path),
789
+ children: p.title
790
+ },
791
+ p.path
792
+ )) });
793
+ }
794
+ return /* @__PURE__ */ jsxs("div", { "data-applet-pages": layout, className: layout === "sidebar" ? "flex h-full min-h-0" : "flex h-full min-h-0 flex-col", children: [
795
+ navigation,
796
+ /* @__PURE__ */ jsx2("div", { className: "min-h-0 flex-1", children: body })
797
+ ] });
798
+ }
799
+
800
+ // src/react/nesting.tsx
801
+ import * as React6 from "react";
802
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
803
+ function StandIn({ what, children }) {
804
+ return /* @__PURE__ */ jsx3("div", { "data-applet-stand-in": what, className: "rounded-md border border-dashed border-border p-2 text-sm text-muted-foreground", children });
805
+ }
806
+ function Applet({ id }) {
807
+ const frame = useFrame();
808
+ const [child, setChild] = React6.useState(null);
809
+ const [error, setError] = React6.useState(null);
810
+ React6.useEffect(() => {
811
+ let cancelled = false;
812
+ frame.channel.child(id).then(
813
+ ({ hostId }) => {
814
+ if (cancelled) return;
815
+ const scoped = frame.channel.forHost(hostId);
816
+ if (scoped) setChild(scoped);
817
+ else setError({ code: "host_not_found", message: `The host for Applet ${id} did not answer.`, retryable: true });
818
+ },
819
+ (err) => {
820
+ if (!cancelled) setError(toAppletError(err));
821
+ }
822
+ );
823
+ return () => {
824
+ cancelled = true;
825
+ };
826
+ }, [frame, id]);
827
+ if (error) return /* @__PURE__ */ jsx3(StandIn, { what: "applet-error", children: error.message });
828
+ if (!child) return /* @__PURE__ */ jsx3("div", { "data-applet-loading": id, "aria-busy": "true" });
829
+ if (!frame.renderApplet) return /* @__PURE__ */ jsxs2(StandIn, { what: "applet", children: [
830
+ "Applet ",
831
+ id,
832
+ " needs mountApplet to render"
833
+ ] });
834
+ return /* @__PURE__ */ jsx3(Fragment, { children: frame.renderApplet(child) });
835
+ }
836
+ function Kind({ kind, value }) {
837
+ const frame = useFrame();
838
+ if (frame.renderKind) return /* @__PURE__ */ jsx3(Fragment, { children: frame.renderKind(kind, value) });
839
+ return /* @__PURE__ */ jsxs2(StandIn, { what: `kind:${kind}`, children: [
840
+ "No renderer for kind ",
841
+ kind,
842
+ " in this frame"
843
+ ] });
844
+ }
845
+ function DataPage({ id }) {
846
+ const frame = useFrame();
847
+ const [state, setState] = React6.useState(null);
848
+ React6.useEffect(() => {
849
+ let cancelled = false;
850
+ void frame.channel.pages.definition(id).then((r) => {
851
+ if (!cancelled) setState(r.ok ? { definition: r.data } : { error: r.error });
852
+ });
853
+ return () => {
854
+ cancelled = true;
855
+ };
856
+ }, [frame, id]);
857
+ if (!state) return /* @__PURE__ */ jsx3("div", { "data-applet-loading": id, "aria-busy": "true" });
858
+ if ("error" in state) return /* @__PURE__ */ jsx3(StandIn, { what: "data-page-error", children: state.error.message });
859
+ if (!frame.renderDataPage) return /* @__PURE__ */ jsxs2(StandIn, { what: "data-page", children: [
860
+ "Page ",
861
+ id,
862
+ " needs a host page renderer"
863
+ ] });
864
+ return /* @__PURE__ */ jsx3(Fragment, { children: frame.renderDataPage(id, state.definition) });
865
+ }
866
+
867
+ // src/frame/index.tsx
868
+ import { jsx as jsx4 } from "react/jsx-runtime";
869
+ var APPLETS_REACT_ENTRY = "@ai-matrx/applets/react";
870
+ function appletScope(record, scope) {
871
+ const entries = [.../* @__PURE__ */ new Set([...record.scope.entries, ...scope.entries, "react", APPLETS_REACT_ENTRY])];
872
+ const overrides = { ...record.scope.overrides ?? {}, ...scope.overrides ?? {} };
873
+ return { entries, overrides, shadowDangerousGlobals: record.scope.shadowDangerousGlobals || scope.shadowDangerousGlobals };
874
+ }
875
+ function provideHooks() {
876
+ provideScopeModules({ [APPLETS_REACT_ENTRY]: () => react_exports });
877
+ }
878
+ function mountApplet(record, host, scope, options = {}) {
879
+ provideHooks();
880
+ const spec = appletScope(record, scope);
881
+ const origin = `applet:${record.id}@${record.version}`;
882
+ const unresolvedSeen = [];
883
+ const compileOptions = {
884
+ origin,
885
+ onUnresolved: (o, list) => {
886
+ const lines = list.map((u) => u.importPath ? `${u.identifier} from "${u.importPath}"` : u.identifier);
887
+ unresolvedSeen.push(...lines);
888
+ if (options.onUnresolved) options.onUnresolved(o, lines);
889
+ else if (lines.length) {
890
+ void host.reportError({ code: "unresolved_import", message: `Missing in this Applet: ${lines.join(", ")}`, retryable: false, where: o });
891
+ }
892
+ }
893
+ };
894
+ let compiled;
895
+ try {
896
+ compiled = compile(record.files, record.entry, spec, compileOptions);
897
+ } catch (err) {
898
+ return { ok: false, error: toAppletError(err, "compile_failed") };
899
+ }
900
+ if (!compiled.ok) return { ok: false, error: compiled.error };
901
+ const Entry = compiled.Component;
902
+ const pageCache = /* @__PURE__ */ new Map([[record.entry, Entry]]);
903
+ const pageComponent = (file) => {
904
+ if (!pageCache.has(file)) {
905
+ if (!(file in record.files)) pageCache.set(file, null);
906
+ else {
907
+ const page = compile(record.files, file, spec, { ...compileOptions, origin: `${origin}#${file}` });
908
+ if (!page.ok) void host.reportError({ ...page.error, where: `${origin}#${file}` });
909
+ pageCache.set(file, page.ok ? page.Component : null);
910
+ }
911
+ }
912
+ return pageCache.get(file) ?? null;
913
+ };
914
+ const renderApplet = (child) => /* @__PURE__ */ jsx4(NestedApplet, { channel: child, scope, options });
915
+ function MountedApplet() {
916
+ const renderers = { pageComponent, renderApplet };
917
+ if (options.renderKind) renderers.renderKind = options.renderKind;
918
+ if (options.renderDataPage) renderers.renderDataPage = options.renderDataPage;
919
+ return /* @__PURE__ */ jsx4(AppletFrame, { host, ...renderers, children: /* @__PURE__ */ jsx4(Entry, {}) });
920
+ }
921
+ MountedApplet.displayName = `Applet(${record.slug})`;
922
+ return { ok: true, Component: MountedApplet, unresolved: compiled.unresolved };
923
+ }
924
+ async function mountAppletAsync(record, host, scope, options = {}) {
925
+ provideHooks();
926
+ await prepareScope(appletScope(record, scope));
927
+ return mountApplet(record, host, scope, options);
928
+ }
929
+ function NestedApplet({ channel, scope, options }) {
930
+ const [mounted, setMounted] = React7.useState(null);
931
+ React7.useEffect(() => {
932
+ let cancelled = false;
933
+ channel.record().then(
934
+ (record) => {
935
+ if (!cancelled) setMounted(mountApplet(record, channel, scope, options));
936
+ },
937
+ (err) => {
938
+ if (!cancelled) setMounted({ ok: false, error: toAppletError(err) });
939
+ }
940
+ );
941
+ return () => {
942
+ cancelled = true;
943
+ };
944
+ }, [channel, scope, options]);
945
+ if (!mounted) return /* @__PURE__ */ jsx4("div", { "data-applet-loading": channel.hostId, "aria-busy": "true" });
946
+ if (!mounted.ok) {
947
+ return /* @__PURE__ */ jsx4("div", { "data-applet-stand-in": "applet-error", className: "rounded-md border border-dashed border-border p-2 text-sm text-muted-foreground", children: mounted.error.message });
948
+ }
949
+ const Component = mounted.Component;
950
+ return /* @__PURE__ */ jsx4(Component, {});
951
+ }
952
+ export {
953
+ APPLETS_REACT_ENTRY,
954
+ appletScope,
955
+ mountApplet,
956
+ mountAppletAsync
957
+ };
958
+ //# sourceMappingURL=frame.js.map