@ggui-ai/iframe-runtime 0.1.0-rc.1
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/LICENSE +201 -0
- package/README.md +33 -0
- package/dist/bootstrap.d.ts +72 -0
- package/dist/bootstrap.js +500 -0
- package/dist/canvas/animator/animator-host.d.ts +63 -0
- package/dist/canvas/animator/animator-host.js +105 -0
- package/dist/canvas/animator/animator-pill.d.ts +42 -0
- package/dist/canvas/animator/animator-pill.js +119 -0
- package/dist/canvas/animator/state-machine.d.ts +106 -0
- package/dist/canvas/animator/state-machine.js +150 -0
- package/dist/canvas/canvas-shell.d.ts +77 -0
- package/dist/canvas/canvas-shell.js +117 -0
- package/dist/canvas/display-mode-policy.d.ts +104 -0
- package/dist/canvas/display-mode-policy.js +98 -0
- package/dist/canvas/mount.d.ts +150 -0
- package/dist/canvas/mount.js +202 -0
- package/dist/canvas/nav-stack.d.ts +133 -0
- package/dist/canvas/nav-stack.js +172 -0
- package/dist/channel-transport.d.ts +182 -0
- package/dist/channel-transport.js +344 -0
- package/dist/channels/channel-payload.d.ts +20 -0
- package/dist/channels/channel-payload.js +34 -0
- package/dist/channels/data.d.ts +48 -0
- package/dist/channels/data.js +103 -0
- package/dist/channels/drain-ack.d.ts +13 -0
- package/dist/channels/drain-ack.js +15 -0
- package/dist/channels/feedback.d.ts +14 -0
- package/dist/channels/feedback.js +19 -0
- package/dist/channels/index.d.ts +7 -0
- package/dist/channels/index.js +7 -0
- package/dist/channels/props-update.d.ts +53 -0
- package/dist/channels/props-update.js +130 -0
- package/dist/channels/push.d.ts +47 -0
- package/dist/channels/push.js +54 -0
- package/dist/channels/system.d.ts +26 -0
- package/dist/channels/system.js +44 -0
- package/dist/compiled-validators.d.ts +40 -0
- package/dist/compiled-validators.js +74 -0
- package/dist/context-observer.d.ts +205 -0
- package/dist/context-observer.js +312 -0
- package/dist/gadget-loader.d.ts +118 -0
- package/dist/gadget-loader.js +136 -0
- package/dist/globals.d.ts +234 -0
- package/dist/globals.js +101 -0
- package/dist/host-context-emitter.d.ts +91 -0
- package/dist/host-context-emitter.js +213 -0
- package/dist/iframe-runtime.js +261 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +4 -0
- package/dist/lifecycle.d.ts +66 -0
- package/dist/lifecycle.js +49 -0
- package/dist/mcp-app-iframe-host.d.ts +69 -0
- package/dist/mcp-app-iframe-host.js +214 -0
- package/dist/observability.d.ts +225 -0
- package/dist/observability.js +28 -0
- package/dist/protocol-error.d.ts +199 -0
- package/dist/protocol-error.js +116 -0
- package/dist/provisional-renderer.d.ts +62 -0
- package/dist/provisional-renderer.js +323 -0
- package/dist/react-renderer.d.ts +95 -0
- package/dist/react-renderer.js +343 -0
- package/dist/registry-subscribe.d.ts +102 -0
- package/dist/registry-subscribe.js +319 -0
- package/dist/runtime.d.ts +531 -0
- package/dist/runtime.js +2623 -0
- package/dist/server.d.ts +36 -0
- package/dist/server.js +72 -0
- package/dist/stack-item-renderer.d.ts +103 -0
- package/dist/stack-item-renderer.js +270 -0
- package/dist/stack.d.ts +90 -0
- package/dist/stack.js +118 -0
- package/dist/status-dom.d.ts +72 -0
- package/dist/status-dom.js +51 -0
- package/dist/system-cards/NoCredentialsCard.d.ts +33 -0
- package/dist/system-cards/NoCredentialsCard.js +189 -0
- package/dist/system-cards/ProtocolProbeCard.d.ts +64 -0
- package/dist/system-cards/ProtocolProbeCard.js +463 -0
- package/dist/system-cards/QrCode.d.ts +46 -0
- package/dist/system-cards/QrCode.js +55 -0
- package/dist/system-cards/host-detect.d.ts +31 -0
- package/dist/system-cards/host-detect.js +108 -0
- package/dist/system-cards/host-intents.d.ts +85 -0
- package/dist/system-cards/host-intents.js +170 -0
- package/dist/system-cards/index.d.ts +70 -0
- package/dist/system-cards/index.js +134 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.js +3 -0
- package/dist/validation.d.ts +83 -0
- package/dist/validation.js +106 -0
- package/dist/wire-config.d.ts +214 -0
- package/dist/wire-config.js +290 -0
- package/package.json +81 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animator pill.
|
|
3
|
+
*
|
|
4
|
+
* Visual component that renders the current `AnimatorState`. Two
|
|
5
|
+
* layouts:
|
|
6
|
+
*
|
|
7
|
+
* - centered (when canvas navStack is empty): pill at ~40% top
|
|
8
|
+
* - navbar (when canvas has content): pill compressed into top chrome
|
|
9
|
+
*
|
|
10
|
+
* V1 ships PLACEHOLDER visuals per the design doc. Behavioral spec is
|
|
11
|
+
* locked; the designer pass refines exact motion + sizing. Engineering
|
|
12
|
+
* baseline: pill shape, brand accent dot, label text, pulse animation
|
|
13
|
+
* via CSS keyframes.
|
|
14
|
+
*
|
|
15
|
+
* Boundary discipline:
|
|
16
|
+
* - Pure view component. Takes `state` + `layout` props, renders DOM.
|
|
17
|
+
* - No state, no effects, no subscriptions. Parent (animator-host)
|
|
18
|
+
* owns those.
|
|
19
|
+
* - Accessibility: `aria-live="polite"` for routine transitions;
|
|
20
|
+
* `assertive` for error. Reduced-motion honored via CSS.
|
|
21
|
+
*/
|
|
22
|
+
import { type ReactNode } from 'react';
|
|
23
|
+
import type { AnimatorState } from './state-machine.js';
|
|
24
|
+
export interface AnimatorPillProps {
|
|
25
|
+
readonly state: AnimatorState;
|
|
26
|
+
/**
|
|
27
|
+
* Where to draw the pill. The shell decides based on navStack
|
|
28
|
+
* emptiness:
|
|
29
|
+
* - `'centered'` — canvas empty; pill fills (vertically offset
|
|
30
|
+
* above center).
|
|
31
|
+
* - `'navbar'` — canvas has content; pill compressed into the
|
|
32
|
+
* top chrome.
|
|
33
|
+
*/
|
|
34
|
+
readonly layout: 'centered' | 'navbar';
|
|
35
|
+
/**
|
|
36
|
+
* Brand label shown when state is `ready` (otherwise the active
|
|
37
|
+
* label is derived from state). Falls back to `'ggui'` per
|
|
38
|
+
* decisions.md.
|
|
39
|
+
*/
|
|
40
|
+
readonly readyLabel?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function AnimatorPill({ state, layout, readyLabel, }: AnimatorPillProps): ReactNode;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Derive the user-facing label from the state. Centralized so the
|
|
4
|
+
* announcement text + visible text stay in lockstep.
|
|
5
|
+
*/
|
|
6
|
+
function labelForState(state, readyLabel) {
|
|
7
|
+
switch (state.kind) {
|
|
8
|
+
case 'ready':
|
|
9
|
+
return readyLabel;
|
|
10
|
+
case 'handshake':
|
|
11
|
+
return `Negotiating: ${state.intent}`;
|
|
12
|
+
case 'constructing':
|
|
13
|
+
return `Building: ${state.intent}`;
|
|
14
|
+
case 'listening':
|
|
15
|
+
return 'Listening…';
|
|
16
|
+
case 'content':
|
|
17
|
+
switch (state.substate.kind) {
|
|
18
|
+
case 'idle':
|
|
19
|
+
return state.activeItemId;
|
|
20
|
+
case 'handshake':
|
|
21
|
+
return `Negotiating: ${state.substate.intent}`;
|
|
22
|
+
case 'constructing':
|
|
23
|
+
return `Building: ${state.substate.intent}`;
|
|
24
|
+
case 'listening':
|
|
25
|
+
return 'Listening…';
|
|
26
|
+
}
|
|
27
|
+
// eslint-disable-next-line no-fallthrough
|
|
28
|
+
case 'error':
|
|
29
|
+
return `Error: ${state.message}`;
|
|
30
|
+
case 'offline':
|
|
31
|
+
return 'Offline — reconnecting…';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The dot animation depends on outer state OR the active substate
|
|
36
|
+
* when on `content`.
|
|
37
|
+
*/
|
|
38
|
+
function indicatorClassFor(state) {
|
|
39
|
+
switch (state.kind) {
|
|
40
|
+
case 'ready':
|
|
41
|
+
return 'ggui-animator-dot--ready';
|
|
42
|
+
case 'handshake':
|
|
43
|
+
return 'ggui-animator-dot--handshake';
|
|
44
|
+
case 'constructing':
|
|
45
|
+
return 'ggui-animator-dot--constructing';
|
|
46
|
+
case 'listening':
|
|
47
|
+
return 'ggui-animator-dot--listening';
|
|
48
|
+
case 'content':
|
|
49
|
+
switch (state.substate.kind) {
|
|
50
|
+
case 'idle':
|
|
51
|
+
return 'ggui-animator-dot--content-idle';
|
|
52
|
+
case 'handshake':
|
|
53
|
+
return 'ggui-animator-dot--handshake';
|
|
54
|
+
case 'constructing':
|
|
55
|
+
return 'ggui-animator-dot--constructing';
|
|
56
|
+
case 'listening':
|
|
57
|
+
return 'ggui-animator-dot--listening';
|
|
58
|
+
}
|
|
59
|
+
// eslint-disable-next-line no-fallthrough
|
|
60
|
+
case 'error':
|
|
61
|
+
return 'ggui-animator-dot--error';
|
|
62
|
+
case 'offline':
|
|
63
|
+
return 'ggui-animator-dot--offline';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Inline styles for the placeholder visual. Replaced by the designer
|
|
68
|
+
* pass with a real CSS module. Keeping them inline avoids the
|
|
69
|
+
* CSS-loader plumbing question for v1.
|
|
70
|
+
*/
|
|
71
|
+
const pillStyles = {
|
|
72
|
+
centered: {
|
|
73
|
+
position: 'absolute',
|
|
74
|
+
top: '40%',
|
|
75
|
+
left: '50%',
|
|
76
|
+
transform: 'translate(-50%, -50%)',
|
|
77
|
+
display: 'inline-flex',
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
gap: 8,
|
|
80
|
+
padding: '8px 16px',
|
|
81
|
+
borderRadius: 9999,
|
|
82
|
+
background: 'var(--ggui-color-surface, rgba(245, 245, 245, 0.95))',
|
|
83
|
+
border: '1px solid var(--ggui-color-outlineVariant, rgba(148, 163, 184, 0.3))',
|
|
84
|
+
fontSize: 13,
|
|
85
|
+
color: 'var(--ggui-color-onSurface, #1a1a1a)',
|
|
86
|
+
fontFamily: 'inherit',
|
|
87
|
+
},
|
|
88
|
+
navbar: {
|
|
89
|
+
display: 'inline-flex',
|
|
90
|
+
alignItems: 'center',
|
|
91
|
+
gap: 6,
|
|
92
|
+
padding: '4px 12px',
|
|
93
|
+
borderRadius: 9999,
|
|
94
|
+
background: 'var(--ggui-color-surface, rgba(245, 245, 245, 0.95))',
|
|
95
|
+
border: '1px solid var(--ggui-color-outlineVariant, rgba(148, 163, 184, 0.3))',
|
|
96
|
+
fontSize: 12,
|
|
97
|
+
color: 'var(--ggui-color-onSurface, #1a1a1a)',
|
|
98
|
+
fontFamily: 'inherit',
|
|
99
|
+
},
|
|
100
|
+
dot: {
|
|
101
|
+
width: 8,
|
|
102
|
+
height: 8,
|
|
103
|
+
borderRadius: '50%',
|
|
104
|
+
background: 'var(--ggui-color-primary, #2563eb)',
|
|
105
|
+
flexShrink: 0,
|
|
106
|
+
},
|
|
107
|
+
errorDot: {
|
|
108
|
+
background: 'var(--ggui-color-error, #b91c1c)',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
export function AnimatorPill({ state, layout, readyLabel = 'ggui', }) {
|
|
112
|
+
const label = labelForState(state, readyLabel);
|
|
113
|
+
const dotClass = indicatorClassFor(state);
|
|
114
|
+
const dotStyle = {
|
|
115
|
+
...pillStyles.dot,
|
|
116
|
+
...(state.kind === 'error' ? pillStyles.errorDot : {}),
|
|
117
|
+
};
|
|
118
|
+
return (_jsxs("div", { role: "status", "aria-live": state.kind === 'error' ? 'assertive' : 'polite', "aria-label": label, style: layout === 'centered' ? pillStyles.centered : pillStyles.navbar, "data-ggui-animator-state": state.kind, "data-ggui-animator-layout": layout, children: [_jsx("span", { className: dotClass, style: dotStyle, "aria-hidden": "true" }), _jsx("span", { children: label })] }));
|
|
119
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animator state machine.
|
|
3
|
+
*
|
|
4
|
+
* Pure-function reducer driving the ggui-animator pill's state. Inputs:
|
|
5
|
+
* - live-channel `_ggui:lifecycle` envelopes (handshake_started,
|
|
6
|
+
* handshake_completed, push_started, consume_polling)
|
|
7
|
+
* - the existing `push` envelope (stack item appended) → content
|
|
8
|
+
* - the existing `drain_ack` envelope (consume drained) → exit listening
|
|
9
|
+
* - canvas-local events (navStack changes, mount, error overlay)
|
|
10
|
+
*
|
|
11
|
+
* Output: the next `AnimatorState`. No DOM, no timers, no side effects.
|
|
12
|
+
* Visual presentation (pill / navbar) lives in `animator-pill.tsx`.
|
|
13
|
+
* Subscription wiring lives in `animator-host.tsx`. Timer-based
|
|
14
|
+
* flicker suppression lives in the host with a small queue +
|
|
15
|
+
* clearTimeout helper.
|
|
16
|
+
*
|
|
17
|
+
* The machine has five primary states plus a `content` state with
|
|
18
|
+
* substates and error/offline overlays.
|
|
19
|
+
*/
|
|
20
|
+
import type { ConsumePollingPayload, HandshakeCompletedPayload, HandshakeStartedPayload, PushStartedPayload } from '@ggui-ai/protocol';
|
|
21
|
+
export type AnimatorContentSubstate = {
|
|
22
|
+
readonly kind: 'idle';
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: 'handshake';
|
|
25
|
+
readonly intent: string;
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: 'constructing';
|
|
28
|
+
readonly intent: string;
|
|
29
|
+
readonly stackItemId: string;
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: 'listening';
|
|
32
|
+
readonly stackItemId: string;
|
|
33
|
+
};
|
|
34
|
+
export type AnimatorState = {
|
|
35
|
+
readonly kind: 'ready';
|
|
36
|
+
} | {
|
|
37
|
+
readonly kind: 'handshake';
|
|
38
|
+
readonly handshakeId: string;
|
|
39
|
+
readonly intent: string;
|
|
40
|
+
} | {
|
|
41
|
+
readonly kind: 'constructing';
|
|
42
|
+
readonly stackItemId: string;
|
|
43
|
+
readonly intent: string;
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: 'listening';
|
|
46
|
+
readonly stackItemId: string;
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: 'content';
|
|
49
|
+
readonly activeItemId: string;
|
|
50
|
+
readonly substate: AnimatorContentSubstate;
|
|
51
|
+
} | {
|
|
52
|
+
readonly kind: 'error';
|
|
53
|
+
readonly message: string;
|
|
54
|
+
readonly recoveryTo: AnimatorState;
|
|
55
|
+
} | {
|
|
56
|
+
readonly kind: 'offline';
|
|
57
|
+
};
|
|
58
|
+
export declare const INITIAL_STATE: AnimatorState;
|
|
59
|
+
/**
|
|
60
|
+
* Discriminated union of every event the reducer accepts. The host
|
|
61
|
+
* fans these in from live-channel + canvas-local sources. Wire types
|
|
62
|
+
* (HandshakeStartedPayload etc.) are reused verbatim so the reducer
|
|
63
|
+
* doesn't introduce a parallel type vocabulary.
|
|
64
|
+
*/
|
|
65
|
+
export type AnimatorEvent = {
|
|
66
|
+
readonly kind: 'handshake_started';
|
|
67
|
+
readonly payload: HandshakeStartedPayload;
|
|
68
|
+
} | {
|
|
69
|
+
readonly kind: 'handshake_completed';
|
|
70
|
+
readonly payload: HandshakeCompletedPayload;
|
|
71
|
+
} | {
|
|
72
|
+
readonly kind: 'push_started';
|
|
73
|
+
readonly payload: PushStartedPayload;
|
|
74
|
+
} | {
|
|
75
|
+
readonly kind: 'consume_polling';
|
|
76
|
+
readonly payload: ConsumePollingPayload;
|
|
77
|
+
} | {
|
|
78
|
+
readonly kind: 'stack_item_appended';
|
|
79
|
+
readonly stackItemId: string;
|
|
80
|
+
} | {
|
|
81
|
+
readonly kind: 'action_drained';
|
|
82
|
+
readonly stackItemId: string;
|
|
83
|
+
} | {
|
|
84
|
+
readonly kind: 'stack_emptied';
|
|
85
|
+
} | {
|
|
86
|
+
readonly kind: 'contract_error';
|
|
87
|
+
readonly message: string;
|
|
88
|
+
} | {
|
|
89
|
+
readonly kind: 'error_dismissed';
|
|
90
|
+
} | {
|
|
91
|
+
readonly kind: 'transport_offline';
|
|
92
|
+
} | {
|
|
93
|
+
readonly kind: 'transport_online';
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Compute the next animator state for an event. Pure function.
|
|
97
|
+
*
|
|
98
|
+
* Decision-log refs (from canvas-mode-decisions.md):
|
|
99
|
+
*
|
|
100
|
+
* - Substate priority on `content`: constructing > handshake > listening > idle.
|
|
101
|
+
* - drain_ack is the close signal for listening (no separate
|
|
102
|
+
* `consume_polling: closed` envelope today).
|
|
103
|
+
* - Listening during agent reasoning: stay in `ready` post-drain_ack.
|
|
104
|
+
* Honest signal; don't fake activity the server can't observe.
|
|
105
|
+
*/
|
|
106
|
+
export declare function transition(state: AnimatorState, event: AnimatorEvent): AnimatorState;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animator state machine.
|
|
3
|
+
*
|
|
4
|
+
* Pure-function reducer driving the ggui-animator pill's state. Inputs:
|
|
5
|
+
* - live-channel `_ggui:lifecycle` envelopes (handshake_started,
|
|
6
|
+
* handshake_completed, push_started, consume_polling)
|
|
7
|
+
* - the existing `push` envelope (stack item appended) → content
|
|
8
|
+
* - the existing `drain_ack` envelope (consume drained) → exit listening
|
|
9
|
+
* - canvas-local events (navStack changes, mount, error overlay)
|
|
10
|
+
*
|
|
11
|
+
* Output: the next `AnimatorState`. No DOM, no timers, no side effects.
|
|
12
|
+
* Visual presentation (pill / navbar) lives in `animator-pill.tsx`.
|
|
13
|
+
* Subscription wiring lives in `animator-host.tsx`. Timer-based
|
|
14
|
+
* flicker suppression lives in the host with a small queue +
|
|
15
|
+
* clearTimeout helper.
|
|
16
|
+
*
|
|
17
|
+
* The machine has five primary states plus a `content` state with
|
|
18
|
+
* substates and error/offline overlays.
|
|
19
|
+
*/
|
|
20
|
+
export const INITIAL_STATE = { kind: 'ready' };
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Reducer
|
|
23
|
+
// =============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Compute the next animator state for an event. Pure function.
|
|
26
|
+
*
|
|
27
|
+
* Decision-log refs (from canvas-mode-decisions.md):
|
|
28
|
+
*
|
|
29
|
+
* - Substate priority on `content`: constructing > handshake > listening > idle.
|
|
30
|
+
* - drain_ack is the close signal for listening (no separate
|
|
31
|
+
* `consume_polling: closed` envelope today).
|
|
32
|
+
* - Listening during agent reasoning: stay in `ready` post-drain_ack.
|
|
33
|
+
* Honest signal; don't fake activity the server can't observe.
|
|
34
|
+
*/
|
|
35
|
+
export function transition(state, event) {
|
|
36
|
+
// Error + offline overlays — apply first, regardless of current state.
|
|
37
|
+
if (event.kind === 'contract_error') {
|
|
38
|
+
return {
|
|
39
|
+
kind: 'error',
|
|
40
|
+
message: event.message,
|
|
41
|
+
recoveryTo: state.kind === 'error' ? state.recoveryTo : state,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (event.kind === 'error_dismissed') {
|
|
45
|
+
if (state.kind === 'error')
|
|
46
|
+
return state.recoveryTo;
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
if (event.kind === 'transport_offline') {
|
|
50
|
+
if (state.kind === 'offline')
|
|
51
|
+
return state;
|
|
52
|
+
return { kind: 'offline' };
|
|
53
|
+
}
|
|
54
|
+
if (event.kind === 'transport_online') {
|
|
55
|
+
if (state.kind === 'offline')
|
|
56
|
+
return INITIAL_STATE;
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
59
|
+
// From `content`: substate transitions instead of replacing the outer state.
|
|
60
|
+
if (state.kind === 'content') {
|
|
61
|
+
switch (event.kind) {
|
|
62
|
+
case 'handshake_started':
|
|
63
|
+
return {
|
|
64
|
+
...state,
|
|
65
|
+
substate: { kind: 'handshake', intent: event.payload.intent },
|
|
66
|
+
};
|
|
67
|
+
case 'handshake_completed':
|
|
68
|
+
// Drop to idle — `push_started` arrives next when gen is expected.
|
|
69
|
+
return { ...state, substate: { kind: 'idle' } };
|
|
70
|
+
case 'push_started':
|
|
71
|
+
return {
|
|
72
|
+
...state,
|
|
73
|
+
substate: {
|
|
74
|
+
kind: 'constructing',
|
|
75
|
+
intent: event.payload.intent,
|
|
76
|
+
stackItemId: event.payload.stackItemId,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
case 'consume_polling':
|
|
80
|
+
return {
|
|
81
|
+
...state,
|
|
82
|
+
substate: {
|
|
83
|
+
kind: 'listening',
|
|
84
|
+
stackItemId: event.payload.stackItemId,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
case 'stack_item_appended':
|
|
88
|
+
// New item arrived (e.g. push completed). activeItem moves;
|
|
89
|
+
// substate drops to idle.
|
|
90
|
+
return {
|
|
91
|
+
kind: 'content',
|
|
92
|
+
activeItemId: event.stackItemId,
|
|
93
|
+
substate: { kind: 'idle' },
|
|
94
|
+
};
|
|
95
|
+
case 'action_drained':
|
|
96
|
+
// Listening close signal. Drop substate to idle if it was
|
|
97
|
+
// listening for THIS item; otherwise no-op (out-of-order drain
|
|
98
|
+
// for a popped item).
|
|
99
|
+
if (state.substate.kind === 'listening' && state.substate.stackItemId === event.stackItemId) {
|
|
100
|
+
return { ...state, substate: { kind: 'idle' } };
|
|
101
|
+
}
|
|
102
|
+
return state;
|
|
103
|
+
case 'stack_emptied':
|
|
104
|
+
return INITIAL_STATE;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// From any non-content state: outer state transitions.
|
|
108
|
+
switch (event.kind) {
|
|
109
|
+
case 'handshake_started':
|
|
110
|
+
return {
|
|
111
|
+
kind: 'handshake',
|
|
112
|
+
handshakeId: event.payload.handshakeId,
|
|
113
|
+
intent: event.payload.intent,
|
|
114
|
+
};
|
|
115
|
+
case 'handshake_completed':
|
|
116
|
+
// Only honor completion that matches the current handshake's id —
|
|
117
|
+
// a stale completion (from a race) doesn't displace newer state.
|
|
118
|
+
if (state.kind !== 'handshake')
|
|
119
|
+
return state;
|
|
120
|
+
if (state.handshakeId !== event.payload.handshakeId)
|
|
121
|
+
return state;
|
|
122
|
+
// genExpected pre-warms `constructing`? The `push_started`
|
|
123
|
+
// envelope carries the authoritative stackItemId + intent —
|
|
124
|
+
// wait for it.
|
|
125
|
+
return INITIAL_STATE;
|
|
126
|
+
case 'push_started':
|
|
127
|
+
return {
|
|
128
|
+
kind: 'constructing',
|
|
129
|
+
stackItemId: event.payload.stackItemId,
|
|
130
|
+
intent: event.payload.intent,
|
|
131
|
+
};
|
|
132
|
+
case 'consume_polling':
|
|
133
|
+
return { kind: 'listening', stackItemId: event.payload.stackItemId };
|
|
134
|
+
case 'stack_item_appended':
|
|
135
|
+
return {
|
|
136
|
+
kind: 'content',
|
|
137
|
+
activeItemId: event.stackItemId,
|
|
138
|
+
substate: { kind: 'idle' },
|
|
139
|
+
};
|
|
140
|
+
case 'action_drained':
|
|
141
|
+
// Drained while in listening for THIS item → drop to ready.
|
|
142
|
+
// Otherwise no-op.
|
|
143
|
+
if (state.kind === 'listening' && state.stackItemId === event.stackItemId) {
|
|
144
|
+
return INITIAL_STATE;
|
|
145
|
+
}
|
|
146
|
+
return state;
|
|
147
|
+
case 'stack_emptied':
|
|
148
|
+
return INITIAL_STATE;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CanvasShell.
|
|
3
|
+
*
|
|
4
|
+
* Top-level component for canvas-mode iframes. Owns:
|
|
5
|
+
*
|
|
6
|
+
* 1. Layout: animator chrome (centered or navbar) + content area
|
|
7
|
+
* 2. Navigation state via {@link NavStackModel}
|
|
8
|
+
* 3. Active stack-item mount (one at a time — the top of navStack)
|
|
9
|
+
* 4. Display-mode escalation requests (escalation policy applied at
|
|
10
|
+
* navStack transitions + on host-context-changed)
|
|
11
|
+
*
|
|
12
|
+
* Subscriptions/transport are wired by the parent boot path
|
|
13
|
+
* (`bootProduction` branching on `bootstrap.canvasMode`) — the shell
|
|
14
|
+
* receives a pre-built navStack model, an AnimatorEventStream
|
|
15
|
+
* (subscribed to live-channel lifecycle envelopes), and callbacks for
|
|
16
|
+
* the side-effecting concerns (display-mode requests, live-channel
|
|
17
|
+
* `canvas_navigated` envelope emission).
|
|
18
|
+
*
|
|
19
|
+
* Active-item rendering reuses the existing stack-item dispatcher
|
|
20
|
+
* via a render-prop slot.
|
|
21
|
+
*
|
|
22
|
+
* Boundary discipline:
|
|
23
|
+
*
|
|
24
|
+
* - The shell renders the animator pill via `<AnimatorHost>` — but
|
|
25
|
+
* doesn't subscribe to the live channel itself. Caller passes the
|
|
26
|
+
* event stream + nav model in.
|
|
27
|
+
* - Display-mode requests fire through a caller-supplied callback,
|
|
28
|
+
* not directly via `requestDisplayModeInParent` from `runtime.ts`.
|
|
29
|
+
* Keeps the shell testable without DOM/postMessage.
|
|
30
|
+
* - Back-arrow click → caller-supplied `onBackGesture()` (also lets
|
|
31
|
+
* the caller emit the `canvas_navigated` envelope).
|
|
32
|
+
*/
|
|
33
|
+
import { type ReactNode } from 'react';
|
|
34
|
+
import type { McpUiDisplayMode, SessionStackEntry } from '@ggui-ai/protocol';
|
|
35
|
+
import { type AnimatorEventStream } from './animator/animator-host.js';
|
|
36
|
+
import { NavStackModel } from './nav-stack.js';
|
|
37
|
+
export interface CanvasShellProps {
|
|
38
|
+
readonly navStack: NavStackModel;
|
|
39
|
+
readonly events: AnimatorEventStream;
|
|
40
|
+
/**
|
|
41
|
+
* Host's `availableDisplayModes` from `HostContext`. Drives the
|
|
42
|
+
* display-mode escalation policy. Undefined when the host doesn't
|
|
43
|
+
* speak the spec — escalation collapses to a no-op.
|
|
44
|
+
*/
|
|
45
|
+
readonly availableDisplayModes?: readonly McpUiDisplayMode[];
|
|
46
|
+
/** Host's current display mode. Tracks updates from `host-context-changed`. */
|
|
47
|
+
readonly currentDisplayMode?: McpUiDisplayMode;
|
|
48
|
+
/**
|
|
49
|
+
* Fire a `ui/request-display-mode` postMessage to the host. The
|
|
50
|
+
* shell calls this when {@link reconcileDisplayMode} returns a
|
|
51
|
+
* `request` decision. Tests inject a recorder; production wires it
|
|
52
|
+
* to `requestDisplayModeInParent` from `runtime.ts`.
|
|
53
|
+
*/
|
|
54
|
+
readonly onRequestDisplayMode: (mode: McpUiDisplayMode) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Called when the user back-navigates. The shell handles the local
|
|
57
|
+
* `navStack.pop()`; this callback lets the caller emit the
|
|
58
|
+
* `canvas_navigated` envelope on the live channel + any other side effects.
|
|
59
|
+
*/
|
|
60
|
+
readonly onBackGesture: (args: {
|
|
61
|
+
readonly previousActiveItemId: string;
|
|
62
|
+
readonly activeItemId: string | null;
|
|
63
|
+
}) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Render-prop for the active stack item. Called with the current
|
|
66
|
+
* top of navStack (or null when empty); production wires this to
|
|
67
|
+
* `mountStackItem` from `stack-item-renderer.ts`. Tests inject a
|
|
68
|
+
* simple component.
|
|
69
|
+
*
|
|
70
|
+
* Why a render-prop: keeps the shell testable without the full
|
|
71
|
+
* `mountStackItem` dependency graph (WireConfig, StreamBus, theme
|
|
72
|
+
* resolution, ContextStateHost) — those are bootProduction's
|
|
73
|
+
* concern, not the shell's.
|
|
74
|
+
*/
|
|
75
|
+
readonly renderActiveItem: (item: SessionStackEntry | null) => ReactNode;
|
|
76
|
+
}
|
|
77
|
+
export declare function CanvasShell({ navStack, events, onBackGesture, renderActiveItem, }: CanvasShellProps): ReactNode;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* CanvasShell.
|
|
4
|
+
*
|
|
5
|
+
* Top-level component for canvas-mode iframes. Owns:
|
|
6
|
+
*
|
|
7
|
+
* 1. Layout: animator chrome (centered or navbar) + content area
|
|
8
|
+
* 2. Navigation state via {@link NavStackModel}
|
|
9
|
+
* 3. Active stack-item mount (one at a time — the top of navStack)
|
|
10
|
+
* 4. Display-mode escalation requests (escalation policy applied at
|
|
11
|
+
* navStack transitions + on host-context-changed)
|
|
12
|
+
*
|
|
13
|
+
* Subscriptions/transport are wired by the parent boot path
|
|
14
|
+
* (`bootProduction` branching on `bootstrap.canvasMode`) — the shell
|
|
15
|
+
* receives a pre-built navStack model, an AnimatorEventStream
|
|
16
|
+
* (subscribed to live-channel lifecycle envelopes), and callbacks for
|
|
17
|
+
* the side-effecting concerns (display-mode requests, live-channel
|
|
18
|
+
* `canvas_navigated` envelope emission).
|
|
19
|
+
*
|
|
20
|
+
* Active-item rendering reuses the existing stack-item dispatcher
|
|
21
|
+
* via a render-prop slot.
|
|
22
|
+
*
|
|
23
|
+
* Boundary discipline:
|
|
24
|
+
*
|
|
25
|
+
* - The shell renders the animator pill via `<AnimatorHost>` — but
|
|
26
|
+
* doesn't subscribe to the live channel itself. Caller passes the
|
|
27
|
+
* event stream + nav model in.
|
|
28
|
+
* - Display-mode requests fire through a caller-supplied callback,
|
|
29
|
+
* not directly via `requestDisplayModeInParent` from `runtime.ts`.
|
|
30
|
+
* Keeps the shell testable without DOM/postMessage.
|
|
31
|
+
* - Back-arrow click → caller-supplied `onBackGesture()` (also lets
|
|
32
|
+
* the caller emit the `canvas_navigated` envelope).
|
|
33
|
+
*/
|
|
34
|
+
import { useEffect, useState } from 'react';
|
|
35
|
+
import { AnimatorHost, } from './animator/animator-host.js';
|
|
36
|
+
export function CanvasShell({ navStack, events, onBackGesture, renderActiveItem, }) {
|
|
37
|
+
// Subscribe to navStack's version counter so React re-renders when
|
|
38
|
+
// it mutates. Mirrors useSyncExternalStore pattern with a manual
|
|
39
|
+
// useState bridge (avoids a React 18+ requirement for the
|
|
40
|
+
// dependency that's not necessarily satisfied in older callers).
|
|
41
|
+
//
|
|
42
|
+
// Uses `onMutation` (not `onNavChange`) so in-place replaces from
|
|
43
|
+
// `ggui_update` props patches re-render too — otherwise the active
|
|
44
|
+
// item shows stale props after the first turn. The version value
|
|
45
|
+
// itself is unread — only the setVersion side effect matters.
|
|
46
|
+
const [, setVersion] = useState(navStack.version());
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const unsubscribe = navStack.onMutation(() => {
|
|
49
|
+
setVersion(navStack.version());
|
|
50
|
+
});
|
|
51
|
+
return unsubscribe;
|
|
52
|
+
}, [navStack]);
|
|
53
|
+
// Layout selector — centered animator pill (empty) vs. navbar
|
|
54
|
+
// (content present). The version-tracked `useState` above ensures
|
|
55
|
+
// this re-evaluates on every navStack mutation.
|
|
56
|
+
const contentState = navStack.size() === 0 ? 'empty' : 'has-content';
|
|
57
|
+
// Display-mode escalation is policy-driven by `reconcileDisplayMode`,
|
|
58
|
+
// but auto-firing it on every navStack transition is too aggressive:
|
|
59
|
+
// a canvas with one item would silently request `fullscreen`, taking
|
|
60
|
+
// over the host's viewport without user intent. The policy stays in
|
|
61
|
+
// the codebase + remains tested for the eventual user-driven trigger
|
|
62
|
+
// (e.g. a fullscreen-toggle control in the animator chrome), but the
|
|
63
|
+
// shell does NOT auto-fire it. `onRequestDisplayMode` is wired on
|
|
64
|
+
// the props; the call site moves from here to whatever UI affordance
|
|
65
|
+
// gives the user explicit consent. Until then: display mode only
|
|
66
|
+
// changes when the user requests it manually.
|
|
67
|
+
const activeItem = navStack.peek();
|
|
68
|
+
const handleBack = () => {
|
|
69
|
+
const previous = navStack.peek();
|
|
70
|
+
if (!previous)
|
|
71
|
+
return;
|
|
72
|
+
const newActive = navStack.pop();
|
|
73
|
+
onBackGesture({
|
|
74
|
+
previousActiveItemId: previous.id,
|
|
75
|
+
activeItemId: newActive?.id ?? null,
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
// Inline layout styles for the placeholder shell. Designer pass
|
|
79
|
+
// refines exact dimensions / transitions.
|
|
80
|
+
const containerStyle = {
|
|
81
|
+
position: 'relative',
|
|
82
|
+
width: '100%',
|
|
83
|
+
height: '100%',
|
|
84
|
+
display: 'flex',
|
|
85
|
+
flexDirection: 'column',
|
|
86
|
+
overflow: 'hidden',
|
|
87
|
+
};
|
|
88
|
+
const navbarStyle = {
|
|
89
|
+
flexShrink: 0,
|
|
90
|
+
display: 'flex',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
gap: 8,
|
|
93
|
+
padding: '8px 12px',
|
|
94
|
+
borderBottom: '1px solid var(--ggui-color-outlineVariant, rgba(148, 163, 184, 0.2))',
|
|
95
|
+
background: 'var(--ggui-color-surface, rgba(255, 255, 255, 0.98))',
|
|
96
|
+
};
|
|
97
|
+
const contentStyle = {
|
|
98
|
+
flex: 1,
|
|
99
|
+
overflow: 'auto',
|
|
100
|
+
position: 'relative',
|
|
101
|
+
};
|
|
102
|
+
const emptyStateStyle = {
|
|
103
|
+
flex: 1,
|
|
104
|
+
position: 'relative',
|
|
105
|
+
};
|
|
106
|
+
if (contentState === 'empty') {
|
|
107
|
+
return (_jsx("div", { style: containerStyle, "data-ggui-canvas-shell": "empty", children: _jsx("div", { style: emptyStateStyle, children: _jsx(AnimatorHost, { events: events, layout: "centered" }) }) }));
|
|
108
|
+
}
|
|
109
|
+
return (_jsxs("div", { style: containerStyle, "data-ggui-canvas-shell": "content", children: [_jsxs("div", { style: navbarStyle, "data-ggui-canvas-navbar": "true", children: [navStack.size() > 1 ? (_jsx("button", { type: "button", onClick: handleBack, "aria-label": "Go back", "data-ggui-canvas-back": "true", style: {
|
|
110
|
+
background: 'transparent',
|
|
111
|
+
border: 'none',
|
|
112
|
+
cursor: 'pointer',
|
|
113
|
+
padding: '4px 8px',
|
|
114
|
+
fontSize: 16,
|
|
115
|
+
color: 'var(--ggui-color-onSurface, #1a1a1a)',
|
|
116
|
+
}, children: "\u2190" })) : null, _jsx(AnimatorHost, { events: events, layout: "navbar" })] }), _jsx("div", { style: contentStyle, "data-ggui-canvas-content": "true", children: renderActiveItem(activeItem) })] }));
|
|
117
|
+
}
|