@astrale-os/adapter-cloudflare 0.1.8 → 0.1.10
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/assets-pack.d.ts +1 -1
- package/dist/assets-pack.js +1 -1
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +15 -0
- package/dist/build.js.map +1 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -1
- package/dist/client.js.map +1 -1
- package/dist/cloudflare.d.ts +15 -3
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +52 -18
- package/dist/cloudflare.js.map +1 -1
- package/dist/codegen/worker.d.ts +26 -6
- package/dist/codegen/worker.d.ts.map +1 -1
- package/dist/codegen/worker.js +67 -54
- package/dist/codegen/worker.js.map +1 -1
- package/dist/codegen/wrangler.d.ts +11 -2
- package/dist/codegen/wrangler.d.ts.map +1 -1
- package/dist/codegen/wrangler.js +11 -5
- package/dist/codegen/wrangler.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/params.d.ts +30 -30
- package/dist/params.d.ts.map +1 -1
- package/dist/parse-output.d.ts +1 -1
- package/dist/parse-output.js +1 -1
- package/package.json +6 -2
- package/src/assets-pack.ts +1 -1
- package/src/build.ts +15 -0
- package/src/client.ts +11 -1
- package/src/cloudflare.ts +53 -18
- package/src/codegen/worker.ts +76 -59
- package/src/codegen/wrangler.ts +15 -5
- package/src/index.ts +6 -3
- package/src/params.ts +32 -31
- package/src/parse-output.ts +1 -1
- package/template/.agents/skills/astrale-cli/SKILL.md +26 -12
- package/template/.agents/skills/astrale-domain/SKILL.md +46 -29
- package/template/.env.example +6 -0
- package/template/README.md +25 -10
- package/template/astrale.config.ts +27 -33
- package/template/client/README.md +80 -63
- package/template/client/__tests__/app.test.tsx +188 -99
- package/template/client/__tests__/harness.ts +67 -12
- package/template/client/__tests__/kernel.test.ts +65 -50
- package/template/client/__tests__/seam.test.tsx +111 -0
- package/template/client/index.html +1 -1
- package/template/client/package.json +1 -0
- package/template/client/src/app.tsx +40 -83
- package/template/client/src/main.tsx +2 -2
- package/template/client/src/monitor/components/MonitorCard.tsx +50 -0
- package/template/client/src/monitor/components/index.ts +1 -0
- package/template/client/src/monitor/hooks/index.ts +3 -0
- package/template/client/src/monitor/hooks/useCheck.mutation.ts +16 -0
- package/template/client/src/monitor/hooks/useMonitor.query.ts +64 -0
- package/template/client/src/monitor/index.ts +6 -0
- package/template/client/src/monitor/monitor.api.ts +11 -0
- package/template/client/src/monitor/monitor.mappers.ts +38 -0
- package/template/client/src/monitor/monitor.types.ts +23 -0
- package/template/client/src/monitor/ui/MonitorDetails.UI.tsx +38 -0
- package/template/client/src/monitor/ui/StatusBadge.UI.tsx +14 -0
- package/template/client/src/monitor/ui/index.ts +8 -0
- package/template/client/src/shell/client.ts +67 -0
- package/template/client/src/shell/index.ts +20 -0
- package/template/client/src/shell/invoke.ts +35 -0
- package/template/client/src/shell/transformers.ts +72 -0
- package/template/client/src/shell/use-async.ts +56 -0
- package/template/client/src/shell/use-capability.ts +61 -0
- package/template/client/src/shell/use-node.ts +61 -0
- package/template/client/src/shell/use-shell.ts +91 -0
- package/template/client/src/shell/view-router.tsx +98 -0
- package/template/client/src/styles.css +177 -4
- package/template/client/src/ui/format.ts +24 -0
- package/template/client/src/ui/index.ts +9 -0
- package/template/client/src/ui/surface.tsx +56 -0
- package/template/client/src/ui/value.tsx +32 -0
- package/template/client/src/views/monitor.tsx +30 -0
- package/template/client/tsconfig.json +3 -2
- package/template/client/vite.config.ts +14 -15
- package/template/client/vitest.config.ts +12 -5
- package/template/core/monitor/health.ts +19 -0
- package/template/core/monitor/index.ts +9 -0
- package/template/core/monitor/keys.ts +29 -0
- package/template/core/monitor/node.ts +51 -0
- package/template/deps.ts +25 -0
- package/template/domain.ts +33 -0
- package/template/env.ts +4 -0
- package/template/integrations/prober/http.ts +43 -0
- package/template/integrations/prober/mock.ts +22 -0
- package/template/integrations/prober/port.ts +28 -0
- package/template/integrations/prober/registry.ts +66 -0
- package/template/package.json +2 -3
- package/template/pnpm-lock.yaml +2766 -0
- package/template/runtime/index.ts +79 -0
- package/template/runtime/monitor/check.ts +29 -0
- package/template/runtime/monitor/dependsOn.ts +16 -0
- package/template/runtime/monitor/index.ts +12 -0
- package/template/runtime/monitor/seed.ts +74 -0
- package/template/runtime/monitor/shared.ts +17 -0
- package/template/runtime/monitor/watch.ts +37 -0
- package/template/schema/index.ts +13 -4
- package/template/schema/monitor.ts +80 -0
- package/template/tsconfig.json +13 -2
- package/template/views/index.ts +9 -2
- package/template/views/monitor.ts +22 -0
- package/dist/astrale.d.ts +0 -27
- package/dist/astrale.d.ts.map +0 -1
- package/dist/astrale.js +0 -222
- package/dist/astrale.js.map +0 -1
- package/src/astrale.ts +0 -259
- package/template/client/src/lib/kernel.ts +0 -135
- package/template/client/src/lib/shell.ts +0 -197
- package/template/client/src/lib/use-node.ts +0 -66
- package/template/client/src/lib/use-shell.ts +0 -85
- package/template/methods/index.ts +0 -66
- package/template/methods/note.ts +0 -131
- package/template/schema/compiled.ts +0 -14
- package/template/schema/note.ts +0 -64
- package/template/views/note.ts +0 -21
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel invocation primitives — the single place a view reaches the kernel,
|
|
3
|
+
* over the shell's proper `KernelClient` (`session.call`, wired by the sandboxed
|
|
4
|
+
* handshake: fresh delegation token per call, redirects followed). `callMethod`
|
|
5
|
+
* is the generic method-ref call; `invokeNode` is instance dispatch by node id
|
|
6
|
+
* (`@<id>::<method>` — the shell hands views node IDs, not paths); `nodeAddr`
|
|
7
|
+
* picks the stable address for a node.
|
|
8
|
+
*/
|
|
9
|
+
import type { KernelClient } from '@astrale-os/shell'
|
|
10
|
+
|
|
11
|
+
import type { KernelNode } from './client'
|
|
12
|
+
|
|
13
|
+
/** One kernel call through the live session (proper client: fresh token, redirects). */
|
|
14
|
+
export function callMethod(
|
|
15
|
+
session: KernelClient,
|
|
16
|
+
method: string,
|
|
17
|
+
params: Record<string, unknown> = {},
|
|
18
|
+
): Promise<unknown> {
|
|
19
|
+
return session.call(method, params)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Instance-method dispatch by node id. */
|
|
23
|
+
export function invokeNode(
|
|
24
|
+
session: KernelClient,
|
|
25
|
+
nodeId: string,
|
|
26
|
+
method: string,
|
|
27
|
+
params: Record<string, unknown> = {},
|
|
28
|
+
): Promise<unknown> {
|
|
29
|
+
return callMethod(session, `@${nodeId}::${method}`, params)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Stable instance address: tree path when known, else `@id`. */
|
|
33
|
+
export function nodeAddr(node: KernelNode): string {
|
|
34
|
+
return node.path && node.path.startsWith('/') ? node.path : `@${node.id}`
|
|
35
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph-response transformers at the kernel boundary — turn raw `::get` /
|
|
3
|
+
* `::listChildren` / `::getLinks` payloads into the shapes features consume.
|
|
4
|
+
* Domain-neutral: per-class node→record mappers live in each feature's
|
|
5
|
+
* `*.mappers.ts` and build on `qualifiedString` here.
|
|
6
|
+
*
|
|
7
|
+
* Props are stored under QUALIFIED keys
|
|
8
|
+
* (`monitors.astrale.ai:class.Monitor.property.url`). The client must not import
|
|
9
|
+
* server schema, so values resolve by KEY SUFFIX (`.property.<name>`),
|
|
10
|
+
* preferring domain-qualified keys over kernel ones
|
|
11
|
+
* (`kernel.astrale.ai:interface.Named.property.name` would otherwise shadow
|
|
12
|
+
* `Monitor.property.name` — both end in `.property.name`).
|
|
13
|
+
*/
|
|
14
|
+
import type { KernelNode } from './client'
|
|
15
|
+
|
|
16
|
+
/** Resolve a prop by `.property.<name>` suffix; domain keys win over kernel ones. */
|
|
17
|
+
export function qualifiedProp(props: Record<string, unknown>, name: string): unknown {
|
|
18
|
+
const suffix = `.property.${name}`
|
|
19
|
+
let kernelFallback: unknown
|
|
20
|
+
for (const [key, value] of Object.entries(props)) {
|
|
21
|
+
if (!key.endsWith(suffix)) continue
|
|
22
|
+
if (key.startsWith('kernel.astrale.ai:')) {
|
|
23
|
+
if (kernelFallback === undefined) kernelFallback = value
|
|
24
|
+
} else {
|
|
25
|
+
return value
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return kernelFallback
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function qualifiedString(props: Record<string, unknown>, name: string): string | undefined {
|
|
32
|
+
const v = qualifiedProp(props, name)
|
|
33
|
+
return typeof v === 'string' && v !== '' ? v : undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type RawLink = { class?: unknown; edgeClass?: unknown; target?: unknown; to?: unknown }
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Targets of a `::getLinks` response whose edge class ends with `classTail`
|
|
40
|
+
* (e.g. `'monitor_on_host'`). Tolerates bare-array and `{ links }` envelopes,
|
|
41
|
+
* and both `target`/`to` + `class`/`edgeClass`.
|
|
42
|
+
*/
|
|
43
|
+
export function linkTargets(raw: unknown, classTail: string): string[] {
|
|
44
|
+
const links: RawLink[] = Array.isArray(raw)
|
|
45
|
+
? (raw as RawLink[])
|
|
46
|
+
: ((raw as { links?: RawLink[] } | null)?.links ?? [])
|
|
47
|
+
const out: string[] = []
|
|
48
|
+
for (const link of links) {
|
|
49
|
+
const cls =
|
|
50
|
+
typeof link.class === 'string'
|
|
51
|
+
? link.class
|
|
52
|
+
: typeof link.edgeClass === 'string'
|
|
53
|
+
? link.edgeClass
|
|
54
|
+
: ''
|
|
55
|
+
if (!cls.endsWith(classTail)) continue
|
|
56
|
+
const target =
|
|
57
|
+
typeof link.target === 'string'
|
|
58
|
+
? link.target
|
|
59
|
+
: typeof link.to === 'string'
|
|
60
|
+
? link.to
|
|
61
|
+
: undefined
|
|
62
|
+
if (target) out.push(target)
|
|
63
|
+
}
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A `::listChildren` response as a node array (bare array or enveloped). */
|
|
68
|
+
export function asNodeArray(raw: unknown): KernelNode[] {
|
|
69
|
+
if (Array.isArray(raw)) return raw as KernelNode[]
|
|
70
|
+
const obj = raw as { children?: KernelNode[]; nodes?: KernelNode[] } | null
|
|
71
|
+
return obj?.children ?? obj?.nodes ?? []
|
|
72
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny async-resource hook — load on mount / when `deps` change, expose a
|
|
3
|
+
* `reload()` for refresh buttons and post-mutation refetches. Mirrors the
|
|
4
|
+
* cancellation discipline of `use-node.ts` (stale resolutions are dropped).
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
7
|
+
|
|
8
|
+
import { errorMessage } from './client'
|
|
9
|
+
|
|
10
|
+
export type AsyncState<T> =
|
|
11
|
+
| { status: 'loading' }
|
|
12
|
+
| { status: 'ok'; data: T }
|
|
13
|
+
| { status: 'error'; message: string }
|
|
14
|
+
|
|
15
|
+
export type AsyncResource<T> = {
|
|
16
|
+
state: AsyncState<T>
|
|
17
|
+
/** Refetch. Keeps showing the previous data while the reload is in flight. */
|
|
18
|
+
reload: () => void
|
|
19
|
+
/** True while a reload is in flight on top of an `ok` state. */
|
|
20
|
+
reloading: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useAsync<T>(fn: () => Promise<T>, deps: unknown[]): AsyncResource<T> {
|
|
24
|
+
const [state, setState] = useState<AsyncState<T>>({ status: 'loading' })
|
|
25
|
+
const [reloading, setReloading] = useState(false)
|
|
26
|
+
const [epoch, setEpoch] = useState(0)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let cancelled = false
|
|
30
|
+
setState((prev) => {
|
|
31
|
+
if (prev.status === 'ok') {
|
|
32
|
+
setReloading(true)
|
|
33
|
+
return prev // keep stale data visible during a reload
|
|
34
|
+
}
|
|
35
|
+
return { status: 'loading' }
|
|
36
|
+
})
|
|
37
|
+
fn()
|
|
38
|
+
.then((data) => {
|
|
39
|
+
if (cancelled) return
|
|
40
|
+
setReloading(false)
|
|
41
|
+
setState({ status: 'ok', data })
|
|
42
|
+
})
|
|
43
|
+
.catch((err: unknown) => {
|
|
44
|
+
if (cancelled) return
|
|
45
|
+
setReloading(false)
|
|
46
|
+
setState({ status: 'error', message: errorMessage(err) })
|
|
47
|
+
})
|
|
48
|
+
return () => {
|
|
49
|
+
cancelled = true
|
|
50
|
+
}
|
|
51
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- deps are the caller's cache key
|
|
52
|
+
}, [...deps, epoch])
|
|
53
|
+
|
|
54
|
+
const reload = useCallback(() => setEpoch((e) => e + 1), [])
|
|
55
|
+
return { state, reload, reloading }
|
|
56
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useCapability` — the convention's WRITE primitive: a node method made
|
|
3
|
+
* interactive, wrapped in one uniform lifecycle (idle → running → done/failed).
|
|
4
|
+
* Every actuator (a form submit, a danger button) shares this machine instead
|
|
5
|
+
* of re-implementing busy/error flags by hand.
|
|
6
|
+
*
|
|
7
|
+
* const check = useCapability(() => invokeNode(session, nodeId, 'check', {}))
|
|
8
|
+
* …
|
|
9
|
+
* if (await check.run()) reload()
|
|
10
|
+
*
|
|
11
|
+
* (This is the single seam where permission-gating would later land — a
|
|
12
|
+
* `permitted` flag computed from the node's affordances — without touching any
|
|
13
|
+
* presentational component.)
|
|
14
|
+
*/
|
|
15
|
+
import { useCallback, useState } from 'react'
|
|
16
|
+
|
|
17
|
+
import { errorMessage } from './client'
|
|
18
|
+
|
|
19
|
+
export type Phase = 'idle' | 'running' | 'done' | 'failed'
|
|
20
|
+
|
|
21
|
+
export interface Capability<P = void> {
|
|
22
|
+
/** Phase of the most recent invocation. */
|
|
23
|
+
phase: Phase
|
|
24
|
+
/** Failure message when `phase === 'failed'`, else null. */
|
|
25
|
+
error: string | null
|
|
26
|
+
/** Invoke; resolves true on success, false on failure (error then set). */
|
|
27
|
+
run: [P] extends [void] ? () => Promise<boolean> : (params: P) => Promise<boolean>
|
|
28
|
+
/** Return to idle, clearing any error. */
|
|
29
|
+
reset: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useCapability<P = void>(
|
|
33
|
+
perform: (params: P) => Promise<unknown>,
|
|
34
|
+
): Capability<P> {
|
|
35
|
+
const [phase, setPhase] = useState<Phase>('idle')
|
|
36
|
+
const [error, setError] = useState<string | null>(null)
|
|
37
|
+
|
|
38
|
+
const run = useCallback(
|
|
39
|
+
async (params: P): Promise<boolean> => {
|
|
40
|
+
setPhase('running')
|
|
41
|
+
setError(null)
|
|
42
|
+
try {
|
|
43
|
+
await perform(params)
|
|
44
|
+
setPhase('done')
|
|
45
|
+
return true
|
|
46
|
+
} catch (err) {
|
|
47
|
+
setPhase('failed')
|
|
48
|
+
setError(errorMessage(err))
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
[perform],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const reset = useCallback(() => {
|
|
56
|
+
setPhase('idle')
|
|
57
|
+
setError(null)
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
return { phase, error, run, reset } as Capability<P>
|
|
61
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { KernelClient } from '@astrale-os/shell'
|
|
4
|
+
|
|
5
|
+
import { errorMessage, type KernelNode } from './client'
|
|
6
|
+
|
|
7
|
+
export type NodeState =
|
|
8
|
+
| { status: 'idle' }
|
|
9
|
+
| { status: 'loading' }
|
|
10
|
+
| { status: 'ok'; node: KernelNode }
|
|
11
|
+
| { status: 'error'; message: string }
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fetch a node by id via `@<id>::get`. Re-fetches when the target node id
|
|
15
|
+
* changes (`setTarget` hot-swap); stays `idle` until a target node id exists.
|
|
16
|
+
* `session` is a stable reference for the view's lifetime, so listing it as a
|
|
17
|
+
* dep never re-fires on its own — and the shell's client reads the fresh token
|
|
18
|
+
* at call time, so a `ctrl:tokenRefresh` needs no re-fetch.
|
|
19
|
+
*
|
|
20
|
+
* Returns the node state plus a `reload()` that re-fetches the current node —
|
|
21
|
+
* the view calls it after an instance method (e.g. `::check`) mutates the node
|
|
22
|
+
* so the fresh props render. It bumps an internal `nonce` dep, re-firing the
|
|
23
|
+
* effect.
|
|
24
|
+
*/
|
|
25
|
+
export function useNode(
|
|
26
|
+
session: KernelClient,
|
|
27
|
+
nodeId: string | undefined,
|
|
28
|
+
): NodeState & { reload(): void } {
|
|
29
|
+
const [state, setState] = useState<NodeState>({ status: 'idle' })
|
|
30
|
+
const [nonce, setNonce] = useState(0)
|
|
31
|
+
|
|
32
|
+
const reload = useCallback(() => {
|
|
33
|
+
setNonce((n) => n + 1)
|
|
34
|
+
}, [])
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!nodeId) {
|
|
38
|
+
setState({ status: 'idle' })
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let cancelled = false
|
|
43
|
+
setState({ status: 'loading' })
|
|
44
|
+
session
|
|
45
|
+
.call(`@${nodeId}::get`, {})
|
|
46
|
+
.then((result) => {
|
|
47
|
+
if (!cancelled) setState({ status: 'ok', node: result as KernelNode })
|
|
48
|
+
})
|
|
49
|
+
.catch((err: unknown) => {
|
|
50
|
+
if (!cancelled) {
|
|
51
|
+
setState({ status: 'error', message: errorMessage(err) })
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return () => {
|
|
56
|
+
cancelled = true
|
|
57
|
+
}
|
|
58
|
+
}, [session, nodeId, nonce])
|
|
59
|
+
|
|
60
|
+
return { ...state, reload }
|
|
61
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createShell,
|
|
3
|
+
type IntentMessage,
|
|
4
|
+
type IntentRegistry,
|
|
5
|
+
type KernelClient,
|
|
6
|
+
type Shell,
|
|
7
|
+
} from '@astrale-os/shell'
|
|
8
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
9
|
+
|
|
10
|
+
export type ShellStatus = 'loading' | 'ready' | 'standalone'
|
|
11
|
+
|
|
12
|
+
export type ShellState = {
|
|
13
|
+
status: ShellStatus
|
|
14
|
+
/** The kernel handle once `ready` — view hooks call through `session.call`. */
|
|
15
|
+
session: KernelClient | null
|
|
16
|
+
/** Current target node id — updates on `setTarget` hot-swaps. */
|
|
17
|
+
nodeId: string | undefined
|
|
18
|
+
/** Why we fell back to `standalone` (no parent / handshake timeout). */
|
|
19
|
+
reason: string | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Boots the real Astrale shell in sandboxed (child) mode: runs the init
|
|
24
|
+
* handshake with the parent window to obtain the kernel session + target node,
|
|
25
|
+
* then tracks `setTarget` hot-swaps. The shell's `KernelClient` owns the
|
|
26
|
+
* delegation token (refreshed by the parent) and the kernel transport, so view
|
|
27
|
+
* hooks just call `session.call('@<id>::<method>')`.
|
|
28
|
+
*
|
|
29
|
+
* Three outcomes:
|
|
30
|
+
* - `loading` — handshake in flight
|
|
31
|
+
* - `ready` — parent completed the handshake; `session`/`nodeId` populated
|
|
32
|
+
* - `standalone` — no parent or it timed out (opened directly in a tab); the
|
|
33
|
+
* view renders a self-describing fallback
|
|
34
|
+
*/
|
|
35
|
+
export function useShell(): ShellState {
|
|
36
|
+
const [status, setStatus] = useState<ShellStatus>('loading')
|
|
37
|
+
const [session, setSession] = useState<KernelClient | null>(null)
|
|
38
|
+
const [nodeId, setNodeId] = useState<string | undefined>(undefined)
|
|
39
|
+
const [reason, setReason] = useState<string | null>(null)
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
// Not framed (opened directly in a tab): skip the handshake entirely rather
|
|
43
|
+
// than waiting out the init timeout — there is no parent to answer it.
|
|
44
|
+
if (!window.parent || window.parent === window) {
|
|
45
|
+
setReason('not running inside a parent frame')
|
|
46
|
+
setStatus('standalone')
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let cancelled = false
|
|
51
|
+
let shell: Shell | null = null
|
|
52
|
+
|
|
53
|
+
void (async () => {
|
|
54
|
+
try {
|
|
55
|
+
const built = createShell({ mode: 'sandboxed', initTimeoutMs: 5000 })
|
|
56
|
+
await built.init()
|
|
57
|
+
if (cancelled) {
|
|
58
|
+
void built.dispose()
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
shell = built
|
|
62
|
+
|
|
63
|
+
// Hand views the shell's real `KernelClient` directly. It reads the live
|
|
64
|
+
// (parent-refreshed) token on each call and owns the transport, so view
|
|
65
|
+
// hooks just call `session.call('@<id>::<method>', params)`.
|
|
66
|
+
setSession(built.kernel)
|
|
67
|
+
setNodeId(built.targetNodeId)
|
|
68
|
+
setStatus('ready')
|
|
69
|
+
|
|
70
|
+
// Hot-swap: the parent pushes a new target via the typed `setTarget`
|
|
71
|
+
// intent; the iframe stays mounted, only `nodeId` updates.
|
|
72
|
+
built.parent?.on('intent', (msg: IntentMessage) => {
|
|
73
|
+
if (msg.envelope.name !== 'setTarget') return
|
|
74
|
+
const payload = msg.envelope.payload as IntentRegistry['setTarget']['payload']
|
|
75
|
+
if (typeof payload.nodeId === 'string') setNodeId(payload.nodeId)
|
|
76
|
+
})
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (cancelled) return
|
|
79
|
+
setReason(err instanceof Error ? err.message : String(err))
|
|
80
|
+
setStatus('standalone')
|
|
81
|
+
}
|
|
82
|
+
})()
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
cancelled = true
|
|
86
|
+
if (shell) void shell.dispose()
|
|
87
|
+
}
|
|
88
|
+
}, [])
|
|
89
|
+
|
|
90
|
+
return useMemo(() => ({ status, session, nodeId, reason }), [status, session, nodeId, reason])
|
|
91
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny multi-view router for the domain's one SPA bundle.
|
|
3
|
+
*
|
|
4
|
+
* The domain declares several SPA Views, each mounted by the shell at its own
|
|
5
|
+
* path under `/ui/*` (e.g. `/ui/monitor`). The shell mounts ONE iframe per view,
|
|
6
|
+
* so inside the iframe `window.location.pathname` is that view's mount path. This
|
|
7
|
+
* bundle reads that path and renders the matching view — one build, many views.
|
|
8
|
+
*
|
|
9
|
+
* Two pieces:
|
|
10
|
+
* - `resolveView` — map `window.location.pathname` → a view component.
|
|
11
|
+
* - `ViewFrame` — DRYs the shell-handshake gating (`loading`/`standalone`/
|
|
12
|
+
* `ready`) every view repeats, so a view only writes its `ready` body.
|
|
13
|
+
*
|
|
14
|
+
* To add a view: write a `ViewComponent` (usually wrapping `ViewFrame`), add a
|
|
15
|
+
* `ROUTES` entry keyed by its mount path in `src/app.tsx`, and register a
|
|
16
|
+
* matching `defineView({ mount })` in the domain's `views/`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ReactNode } from 'react'
|
|
20
|
+
|
|
21
|
+
import type { KernelClient } from '@astrale-os/shell'
|
|
22
|
+
|
|
23
|
+
import type { ShellState } from './use-shell'
|
|
24
|
+
|
|
25
|
+
/** A view: given the shell state, render its tree. */
|
|
26
|
+
export type ViewComponent = (shell: ShellState) => ReactNode
|
|
27
|
+
|
|
28
|
+
/** Mount path → view component (key e.g. `/ui/monitor`). */
|
|
29
|
+
export type ViewRoutes = Record<string, ViewComponent>
|
|
30
|
+
|
|
31
|
+
/** Strip a single trailing slash (but keep a bare `/`). */
|
|
32
|
+
function normalizePath(pathname: string): string {
|
|
33
|
+
return pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the view for a mount path — exact match, tolerating a trailing slash.
|
|
38
|
+
* Returns `undefined` when no route is registered (caller renders a fallback).
|
|
39
|
+
*/
|
|
40
|
+
export function resolveView(
|
|
41
|
+
routes: ViewRoutes,
|
|
42
|
+
pathname: string = window.location.pathname,
|
|
43
|
+
): ViewComponent | undefined {
|
|
44
|
+
return routes[normalizePath(pathname)]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Shared shell-handshake gate. Renders the `.wrap`/`.card` shell and:
|
|
49
|
+
* - `loading` → title + subline + "Waiting for the shell handshake…"
|
|
50
|
+
* - `standalone` → title + subline + the "No parent shell" banner copy
|
|
51
|
+
* - `ready` → `children(session, nodeId)` (session is non-null here)
|
|
52
|
+
*
|
|
53
|
+
* Mirrors the markup/classes the original single-view app used, so styling is
|
|
54
|
+
* unchanged across views.
|
|
55
|
+
*/
|
|
56
|
+
export function ViewFrame({
|
|
57
|
+
shell,
|
|
58
|
+
title,
|
|
59
|
+
subline,
|
|
60
|
+
children,
|
|
61
|
+
}: {
|
|
62
|
+
shell: ShellState
|
|
63
|
+
title: string
|
|
64
|
+
subline?: string
|
|
65
|
+
children: (session: KernelClient, nodeId: string | undefined) => ReactNode
|
|
66
|
+
}) {
|
|
67
|
+
const { status, session, nodeId, reason } = shell
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="wrap">
|
|
71
|
+
<div className="card">
|
|
72
|
+
{status === 'loading' && (
|
|
73
|
+
<>
|
|
74
|
+
<h1 className="title">{title}</h1>
|
|
75
|
+
{subline && <p className="subline">{subline}</p>}
|
|
76
|
+
<p className="muted">Waiting for the shell handshake…</p>
|
|
77
|
+
</>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{status === 'standalone' && (
|
|
81
|
+
<>
|
|
82
|
+
<h1 className="title">{title}</h1>
|
|
83
|
+
{subline && <p className="subline">{subline}</p>}
|
|
84
|
+
<div className="banner">
|
|
85
|
+
No parent shell ({reason}). This view is meant to be mounted by the Astrale GUI, which
|
|
86
|
+
hands it a target node and a kernel session. Showing a standalone preview.
|
|
87
|
+
</div>
|
|
88
|
+
<p className="body muted">
|
|
89
|
+
When mounted by the shell, this card renders the node you opened.
|
|
90
|
+
</p>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{status === 'ready' && session && children(session, nodeId)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|