@ggui-ai/iframe-runtime 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel-transport.d.ts +76 -19
- package/dist/channel-transport.js +137 -29
- package/dist/channels/index.d.ts +0 -1
- package/dist/channels/index.js +0 -1
- package/dist/host-capabilities.d.ts +52 -0
- package/dist/host-capabilities.js +54 -0
- package/dist/iframe-runtime.js +81 -81
- package/dist/iframe-runtime.meta.json +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/mcp-app-iframe-host.js +8 -1
- package/dist/observability.d.ts +76 -52
- package/dist/relay-incapability.d.ts +64 -0
- package/dist/relay-incapability.js +69 -0
- package/dist/runtime.d.ts +123 -10
- package/dist/runtime.js +706 -52
- package/package.json +7 -7
- package/dist/channels/system.d.ts +0 -26
- package/dist/channels/system.js +0 -44
|
@@ -54,17 +54,30 @@
|
|
|
54
54
|
*/
|
|
55
55
|
import type { JsonObject, JsonValue, StreamSpec } from '@ggui-ai/protocol';
|
|
56
56
|
import type { WebSocketMessage } from '@ggui-ai/protocol/transport/websocket';
|
|
57
|
-
import type { ChannelTransportFallbackEvent, ChannelTransportPickedEvent, ChannelTransportResubscribedEvent } from './observability.js';
|
|
57
|
+
import type { ChannelPollDegradedEvent, ChannelPollRecoveredEvent, ChannelTransportFallbackEvent, ChannelTransportPickedEvent, ChannelTransportResubscribedEvent } from './observability.js';
|
|
58
58
|
import type { StreamBus } from './wire-config.js';
|
|
59
59
|
/**
|
|
60
60
|
* Default polling cadence for the iframe-polling transport (ms).
|
|
61
|
-
* Mirrors the server-side default
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* host's `tools/call` quota.
|
|
61
|
+
* Mirrors the server-side default (`DEFAULT_CHANNEL_POLL_DEFAULT_MS`
|
|
62
|
+
* in `mcp-server/src/ggui-session-channel/channel-subscriptions.ts`).
|
|
63
|
+
* Conservative: 10s matches a "data is fresh enough" bar without
|
|
64
|
+
* burning the parent host's `tools/call` quota.
|
|
66
65
|
*/
|
|
67
66
|
export declare const DEFAULT_IFRAME_POLL_INTERVAL_MS = 10000;
|
|
67
|
+
/**
|
|
68
|
+
* Cadence (ms) a channel falls back to once its `tools/call` has
|
|
69
|
+
* failed STRUCTURALLY — a failure the router knows will repeat
|
|
70
|
+
* identically on the next tick (see {@link ToolsCallInvoker}).
|
|
71
|
+
*
|
|
72
|
+
* Deliberately a slower cadence and NOT a stop. A structural failure
|
|
73
|
+
* is a statement about the host's current capability, not a permanent
|
|
74
|
+
* property of the universe: the relay-incapability latch upstream
|
|
75
|
+
* clears the moment a real call succeeds, and the only thing that can
|
|
76
|
+
* observe that is an attempt. A stopped channel could never recover
|
|
77
|
+
* on its own; a slow one recovers within one probe interval, for
|
|
78
|
+
* ~1/3 the cost of a healthy channel at the 10s default.
|
|
79
|
+
*/
|
|
80
|
+
export declare const DEFAULT_IFRAME_PROBE_POLL_INTERVAL_MS = 30000;
|
|
68
81
|
/**
|
|
69
82
|
* Sender for outbound WS frames. Any structurally-fitting send fn
|
|
70
83
|
* works — post-B3b the registry wires the `@ggui-ai/live-channel`
|
|
@@ -74,8 +87,29 @@ export type WsSender = (msg: WebSocketMessage) => void;
|
|
|
74
87
|
/**
|
|
75
88
|
* Iframe → parent `tools/call` invoker. Resolves with the tool's
|
|
76
89
|
* structured-content output (parsed `JsonValue`) or rejects on
|
|
77
|
-
*
|
|
78
|
-
*
|
|
90
|
+
* failure.
|
|
91
|
+
*
|
|
92
|
+
* **Rejection contract.** The router classifies every rejection into
|
|
93
|
+
* exactly two kinds, and the invoker chooses which by the TYPE of the
|
|
94
|
+
* value it throws:
|
|
95
|
+
*
|
|
96
|
+
* - **Structural** — reject with `RelayIncapableError` (see
|
|
97
|
+
* `relay-incapability.ts`). Says: this failed for a reason that
|
|
98
|
+
* will repeat identically on the next tick, so polling at full
|
|
99
|
+
* cadence buys nothing. The router drops the channel to
|
|
100
|
+
* {@link ChannelTransportRouterOptions.probePollIntervalMs} and
|
|
101
|
+
* emits `channel-poll-degraded` once.
|
|
102
|
+
* - **Transient** — reject with anything else. Says: the next tick
|
|
103
|
+
* may well succeed. The router stays quiet and keeps the current
|
|
104
|
+
* cadence; per-tick network jitter must not turn into bus noise
|
|
105
|
+
* or a cadence change.
|
|
106
|
+
*
|
|
107
|
+
* Classification is by type only. An invoker cannot signal structural
|
|
108
|
+
* failure through an error MESSAGE, and re-wording a message cannot
|
|
109
|
+
* change how a channel is treated.
|
|
110
|
+
*
|
|
111
|
+
* Neither kind ever stops the loop — see
|
|
112
|
+
* {@link DEFAULT_IFRAME_PROBE_POLL_INTERVAL_MS}.
|
|
79
113
|
*/
|
|
80
114
|
export type ToolsCallInvoker = (args: {
|
|
81
115
|
readonly toolName: string;
|
|
@@ -102,15 +136,38 @@ export interface ChannelTransportRouterOptions {
|
|
|
102
136
|
/** StreamBus the router emits envelopes onto. */
|
|
103
137
|
readonly streamBus: StreamBus;
|
|
104
138
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
139
|
+
* Poll cadence (ms) shared by EVERY iframe-polled channel on this
|
|
140
|
+
* router. Uniform by construction: `StreamChannelEntry.source`
|
|
141
|
+
* declares `tool` + `args` only, so a contract author has no way to
|
|
142
|
+
* ask one channel to poll faster than another. Giving channels
|
|
143
|
+
* individual cadences would be a protocol change (a new contract
|
|
144
|
+
* field plus server-side clamping — placement, on the channel entry
|
|
145
|
+
* or on `source`, is still open), not a router option — the
|
|
146
|
+
* per-subscription `pollIntervalMs` that does exist belongs to the
|
|
147
|
+
* WS transport's `channel_subscribe` payload, which this polling
|
|
148
|
+
* fallback never sends.
|
|
149
|
+
*
|
|
150
|
+
* Falls back to {@link DEFAULT_IFRAME_POLL_INTERVAL_MS} when
|
|
151
|
+
* omitted. The only cadence that varies between channels at runtime
|
|
152
|
+
* is the degraded probe cadence ({@link
|
|
153
|
+
* ChannelTransportRouterOptions.probePollIntervalMs}), which follows
|
|
154
|
+
* observed poll health rather than authored config.
|
|
109
155
|
*/
|
|
110
156
|
readonly defaultPollIntervalMs?: number;
|
|
111
157
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
158
|
+
* Cadence (ms) a channel probes at after a structural poll failure.
|
|
159
|
+
* Falls back to {@link DEFAULT_IFRAME_PROBE_POLL_INTERVAL_MS}.
|
|
160
|
+
*
|
|
161
|
+
* Clamped UP to the channel's normal cadence: degrading must never
|
|
162
|
+
* make a channel poll MORE often than it did while healthy, which a
|
|
163
|
+
* naive value would do for any deployment whose normal cadence is
|
|
164
|
+
* already slower than the probe default.
|
|
165
|
+
*/
|
|
166
|
+
readonly probePollIntervalMs?: number;
|
|
167
|
+
/**
|
|
168
|
+
* Observability sink — fires once per transport pick, once per
|
|
169
|
+
* disconnect/reconnect-fallback transition, and once per
|
|
170
|
+
* poll-health transition in each direction. Optional; absent ⇒
|
|
114
171
|
* silent.
|
|
115
172
|
*/
|
|
116
173
|
readonly onObserve?: (event: ChannelTransportEvent) => void;
|
|
@@ -118,12 +175,12 @@ export interface ChannelTransportRouterOptions {
|
|
|
118
175
|
/**
|
|
119
176
|
* Observability events the router emits. Hosts that wire `onObserve`
|
|
120
177
|
* see these alongside the renderer's other observability events
|
|
121
|
-
* (`subscribe-failed`, `
|
|
122
|
-
* `observability.ts` (they are members of the
|
|
123
|
-
* `ObservabilityEvent` union); this alias is the
|
|
124
|
-
* so the two surfaces cannot drift.
|
|
178
|
+
* (`subscribe-failed`, `schema-version-mismatch`, …). The arm
|
|
179
|
+
* interfaces live in `observability.ts` (they are members of the
|
|
180
|
+
* exported `ObservabilityEvent` union); this alias is the
|
|
181
|
+
* router-scoped subset, so the two surfaces cannot drift.
|
|
125
182
|
*/
|
|
126
|
-
export type ChannelTransportEvent = ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent;
|
|
183
|
+
export type ChannelTransportEvent = ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | ChannelPollDegradedEvent | ChannelPollRecoveredEvent;
|
|
127
184
|
/**
|
|
128
185
|
* Router handle returned by {@link createChannelTransportRouter}.
|
|
129
186
|
*/
|
|
@@ -1,12 +1,26 @@
|
|
|
1
|
+
import { isRelayIncapableError } from './relay-incapability.js';
|
|
1
2
|
/**
|
|
2
3
|
* Default polling cadence for the iframe-polling transport (ms).
|
|
3
|
-
* Mirrors the server-side default
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* host's `tools/call` quota.
|
|
4
|
+
* Mirrors the server-side default (`DEFAULT_CHANNEL_POLL_DEFAULT_MS`
|
|
5
|
+
* in `mcp-server/src/ggui-session-channel/channel-subscriptions.ts`).
|
|
6
|
+
* Conservative: 10s matches a "data is fresh enough" bar without
|
|
7
|
+
* burning the parent host's `tools/call` quota.
|
|
8
8
|
*/
|
|
9
9
|
export const DEFAULT_IFRAME_POLL_INTERVAL_MS = 10_000;
|
|
10
|
+
/**
|
|
11
|
+
* Cadence (ms) a channel falls back to once its `tools/call` has
|
|
12
|
+
* failed STRUCTURALLY — a failure the router knows will repeat
|
|
13
|
+
* identically on the next tick (see {@link ToolsCallInvoker}).
|
|
14
|
+
*
|
|
15
|
+
* Deliberately a slower cadence and NOT a stop. A structural failure
|
|
16
|
+
* is a statement about the host's current capability, not a permanent
|
|
17
|
+
* property of the universe: the relay-incapability latch upstream
|
|
18
|
+
* clears the moment a real call succeeds, and the only thing that can
|
|
19
|
+
* observe that is an attempt. A stopped channel could never recover
|
|
20
|
+
* on its own; a slow one recovers within one probe interval, for
|
|
21
|
+
* ~1/3 the cost of a healthy channel at the 10s default.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_IFRAME_PROBE_POLL_INTERVAL_MS = 30_000;
|
|
10
24
|
/**
|
|
11
25
|
* Factory.
|
|
12
26
|
*
|
|
@@ -23,6 +37,9 @@ export function createChannelTransportRouter(opts) {
|
|
|
23
37
|
const channels = new Map();
|
|
24
38
|
const allowlist = new Set(opts.streamWebSocketLocalTools ?? []);
|
|
25
39
|
const defaultPollMs = opts.defaultPollIntervalMs ?? DEFAULT_IFRAME_POLL_INTERVAL_MS;
|
|
40
|
+
// Clamped up — a degraded channel must never poll more eagerly than
|
|
41
|
+
// a healthy one. See `probePollIntervalMs`.
|
|
42
|
+
const probePollMs = Math.max(defaultPollMs, opts.probePollIntervalMs ?? DEFAULT_IFRAME_PROBE_POLL_INTERVAL_MS);
|
|
26
43
|
/**
|
|
27
44
|
* WS lifecycle flag. We start in `'connected'` because the router
|
|
28
45
|
* is created AFTER the subscribe ack — the runtime's bootSequence
|
|
@@ -53,40 +70,130 @@ export function createChannelTransportRouter(opts) {
|
|
|
53
70
|
};
|
|
54
71
|
opts.streamBus.emit(envelope);
|
|
55
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* (Re-)arm a channel's recurring timer at `intervalMs`. Only ever
|
|
75
|
+
* called for a channel that is already polling — never resurrects a
|
|
76
|
+
* stopped loop.
|
|
77
|
+
*/
|
|
78
|
+
function armTimer(state, intervalMs) {
|
|
79
|
+
if (state.pollTimer !== null)
|
|
80
|
+
clearInterval(state.pollTimer);
|
|
81
|
+
state.pollTimer = setInterval(() => {
|
|
82
|
+
void tick(state);
|
|
83
|
+
}, intervalMs);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Healthy → degraded. Slows the channel to the probe cadence and
|
|
87
|
+
* announces the transition ONCE.
|
|
88
|
+
*
|
|
89
|
+
* No-ops when the channel isn't polling: an in-flight call can
|
|
90
|
+
* resolve after `stopPolling` (WS payload arrived, channel removed,
|
|
91
|
+
* router disposed), and a degraded channel with no loop to slow
|
|
92
|
+
* down is not a transition worth announcing.
|
|
93
|
+
*/
|
|
94
|
+
function degradePolling(state) {
|
|
95
|
+
if (state.pollDegraded)
|
|
96
|
+
return;
|
|
97
|
+
if (state.pollTimer === null)
|
|
98
|
+
return;
|
|
99
|
+
state.pollDegraded = true;
|
|
100
|
+
armTimer(state, probePollMs);
|
|
101
|
+
observe({
|
|
102
|
+
kind: 'channel-poll-degraded',
|
|
103
|
+
sessionId: state.sessionId,
|
|
104
|
+
channelName: state.channelName,
|
|
105
|
+
reason: 'relay-incapable',
|
|
106
|
+
probeIntervalMs: probePollMs,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Degraded → healthy. Any successful poll proves the structural
|
|
111
|
+
* condition has lifted, so the channel returns to its normal
|
|
112
|
+
* cadence. Announced ONCE per recovery, not per subsequent success.
|
|
113
|
+
*/
|
|
114
|
+
function restorePolling(state) {
|
|
115
|
+
if (!state.pollDegraded)
|
|
116
|
+
return;
|
|
117
|
+
state.pollDegraded = false;
|
|
118
|
+
if (state.pollTimer !== null)
|
|
119
|
+
armTimer(state, defaultPollMs);
|
|
120
|
+
observe({
|
|
121
|
+
kind: 'channel-poll-recovered',
|
|
122
|
+
sessionId: state.sessionId,
|
|
123
|
+
channelName: state.channelName,
|
|
124
|
+
pollIntervalMs: defaultPollMs,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* One poll of one channel: call the tool, classify the outcome,
|
|
129
|
+
* emit the payload.
|
|
130
|
+
*
|
|
131
|
+
* The two failure kinds are the `ToolsCallInvoker` rejection
|
|
132
|
+
* contract, classified by TYPE (never by message text):
|
|
133
|
+
*
|
|
134
|
+
* - Structural (`RelayIncapableError`) — the next tick would fail
|
|
135
|
+
* identically, so slow to the probe cadence and say so once.
|
|
136
|
+
* - Transient (anything else) — stay quiet and keep the cadence.
|
|
137
|
+
* The renderer's observability path already surfaces transport
|
|
138
|
+
* failures via the WS error route; per-tool poll failures do
|
|
139
|
+
* not also become bus noise during a passing outage.
|
|
140
|
+
*
|
|
141
|
+
* Neither kind stops the loop. That is the fail-safe: a channel
|
|
142
|
+
* whose host regains the ability to relay recovers on its own,
|
|
143
|
+
* within one probe interval, with no external nudge.
|
|
144
|
+
*/
|
|
145
|
+
const tick = async (state) => {
|
|
146
|
+
if (disposed)
|
|
147
|
+
return;
|
|
148
|
+
try {
|
|
149
|
+
const payload = await opts.toolsCall({
|
|
150
|
+
toolName: state.toolName,
|
|
151
|
+
args: { ...(state.args ?? {}) },
|
|
152
|
+
});
|
|
153
|
+
if (disposed)
|
|
154
|
+
return;
|
|
155
|
+
// Restore BEFORE emitting: a throwing bus subscriber must not
|
|
156
|
+
// be mistaken for a failed poll (it would land in the catch
|
|
157
|
+
// below and read as transient), and it must not cost this
|
|
158
|
+
// channel its recovery.
|
|
159
|
+
restorePolling(state);
|
|
160
|
+
emitToBus(state, payload);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
if (disposed)
|
|
164
|
+
return;
|
|
165
|
+
if (isRelayIncapableError(err))
|
|
166
|
+
degradePolling(state);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
56
169
|
/**
|
|
57
170
|
* Start an iframe-polling loop for one channel. Idempotent — if a
|
|
58
171
|
* timer is already running we leave it alone. Fires the first poll
|
|
59
172
|
* IMMEDIATELY (no `setInterval` lead-in delay) so the first
|
|
60
173
|
* `useStream` render isn't blocked on a 10-second wait.
|
|
174
|
+
*
|
|
175
|
+
* A channel that was already degraded when its loop last stopped
|
|
176
|
+
* re-arms at the probe cadence — the condition that degraded it
|
|
177
|
+
* belongs to the host, and a WS reconnect cycle is no evidence it
|
|
178
|
+
* has lifted. Only a successful poll is.
|
|
61
179
|
*/
|
|
62
180
|
function startPolling(state) {
|
|
63
181
|
if (state.pollTimer !== null)
|
|
64
182
|
return;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (disposed)
|
|
74
|
-
return;
|
|
75
|
-
emitToBus(state, payload);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// Swallow — next tick may succeed. The renderer's
|
|
79
|
-
// observability path already surfaces transport failures
|
|
80
|
-
// via the WS error route; per-tool poll failures stay quiet
|
|
81
|
-
// to avoid a noisy bus during transient outages.
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
// Fire-and-forget the leading tick + schedule the recurring
|
|
85
|
-
// ticks. Both are wrapped in the `disposed` guard above.
|
|
86
|
-
void tick();
|
|
183
|
+
// Arm BEFORE firing the leading tick. `ToolsCallInvoker` is
|
|
184
|
+
// declared to return a promise, but a non-async implementation
|
|
185
|
+
// that throws satisfies that type, and its failure reaches the
|
|
186
|
+
// tick's catch during THIS function's own synchronous execution.
|
|
187
|
+
// Arming second would leave `pollTimer` null at that moment, and
|
|
188
|
+
// `degradePolling`'s stopped-channel guard would (correctly, for
|
|
189
|
+
// what it can see) ignore it — costing the channel a full degrade
|
|
190
|
+
// cycle. Order here is the only thing that closes that window.
|
|
87
191
|
state.pollTimer = setInterval(() => {
|
|
88
|
-
void tick();
|
|
89
|
-
}, defaultPollMs);
|
|
192
|
+
void tick(state);
|
|
193
|
+
}, state.pollDegraded ? probePollMs : defaultPollMs);
|
|
194
|
+
// Fire-and-forget the leading tick. Wrapped in the `disposed`
|
|
195
|
+
// guard above.
|
|
196
|
+
void tick(state);
|
|
90
197
|
}
|
|
91
198
|
/**
|
|
92
199
|
* Send a `channel_subscribe` WS frame. Server-side bookkeeping is
|
|
@@ -204,6 +311,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
204
311
|
pollTimer: null,
|
|
205
312
|
hasReceivedWsPayload: false,
|
|
206
313
|
permanentPollFallback: false,
|
|
314
|
+
pollDegraded: false,
|
|
207
315
|
};
|
|
208
316
|
channels.set(k, state);
|
|
209
317
|
activate(state);
|
package/dist/channels/index.d.ts
CHANGED
|
@@ -3,4 +3,3 @@ export { createDrainAckHandler, type DrainAckHandlerDeps, } from './drain-ack.js
|
|
|
3
3
|
export { createRenderHandler, type RenderHandlerDeps } from './render-frame.js';
|
|
4
4
|
export { createDataHandler, type DataHandlerDeps } from './data.js';
|
|
5
5
|
export { createChannelPayloadHandler, createChannelErrorHandler, type ChannelRouterHandlerDeps, } from './channel-payload.js';
|
|
6
|
-
export { createSystemHandler, type SystemHandlerDeps } from './system.js';
|
package/dist/channels/index.js
CHANGED
|
@@ -3,4 +3,3 @@ export { createDrainAckHandler, } from './drain-ack.js';
|
|
|
3
3
|
export { createRenderHandler } from './render-frame.js';
|
|
4
4
|
export { createDataHandler } from './data.js';
|
|
5
5
|
export { createChannelPayloadHandler, createChannelErrorHandler, } from './channel-payload.js';
|
|
6
|
-
export { createSystemHandler } from './system.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What can the host we are embedded in actually do? (ggui#440)
|
|
3
|
+
*
|
|
4
|
+
* MCP Apps hosts advertise their capabilities on the `ui/initialize`
|
|
5
|
+
* result. The `App` class captures them; this module is where the
|
|
6
|
+
* runtime keeps that capture so the two decision points that care —
|
|
7
|
+
* the gesture-failure explanation and the `ui/message` doorbell — can
|
|
8
|
+
* consult one source instead of re-deriving it.
|
|
9
|
+
*
|
|
10
|
+
* FAIL-SAFE BY CONTRACT: absence of a capability MUST NOT stop the
|
|
11
|
+
* runtime from attempting the operation. Hosts under-advertise in
|
|
12
|
+
* practice — ggui's own embed host (`./mcp-app-iframe-host.ts`) proxies
|
|
13
|
+
* `tools/call` while advertising nothing — so treating silence as
|
|
14
|
+
* "cannot" would break working paths. These accessors exist to explain
|
|
15
|
+
* a failure that already happened, never to pre-empt an attempt.
|
|
16
|
+
*
|
|
17
|
+
* PRESENCE, NOT VALUE: on `McpUiHostCapabilities` both fields are
|
|
18
|
+
* optional OBJECTS (`serverTools?: { listChanged?: boolean }`,
|
|
19
|
+
* `message?: McpUiSupportedContentBlockModalities`). An empty object is
|
|
20
|
+
* a positive advertisement; `=== true` is never correct here.
|
|
21
|
+
*/
|
|
22
|
+
import type { McpUiHostCapabilities } from "@modelcontextprotocol/ext-apps";
|
|
23
|
+
/**
|
|
24
|
+
* Record the host's advertised capabilities. Called once at boot, from
|
|
25
|
+
* `bootSequence`, immediately after `connectApp` resolves — before that
|
|
26
|
+
* point `App.getHostCapabilities()` returns `undefined`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function setHostCapabilities(caps: McpUiHostCapabilities | undefined): void;
|
|
29
|
+
/**
|
|
30
|
+
* Did the host advertise that it can proxy a guest `tools/call` to the
|
|
31
|
+
* MCP server? `false` also covers "we have not connected yet" and "the
|
|
32
|
+
* host advertised nothing" — callers must treat it as *unconfirmed*,
|
|
33
|
+
* not as *impossible*.
|
|
34
|
+
*/
|
|
35
|
+
export declare function hostCanRelayToolCalls(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Did the host advertise that it accepts `ui/message` from the view?
|
|
38
|
+
* This is the only view→host method that can start an agent turn, so a
|
|
39
|
+
* host without it cannot be woken by the doorbell — the user has to
|
|
40
|
+
* send a message themselves.
|
|
41
|
+
*/
|
|
42
|
+
export declare function hostCanReceiveMessages(): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Has the handshake resolved capabilities at all yet? See {@link captured}.
|
|
45
|
+
* Callers that treat capability absence as confirmed incapability (as
|
|
46
|
+
* opposed to merely unconfirmed) must gate on this first — otherwise
|
|
47
|
+
* the pre-handshake window (where absence is just "not asked yet") is
|
|
48
|
+
* indistinguishable from a host that answered and advertised nothing.
|
|
49
|
+
*/
|
|
50
|
+
export declare function hostCapabilitiesCaptured(): boolean;
|
|
51
|
+
/** @internal — exported for unit tests to reset module state. */
|
|
52
|
+
export declare function __resetHostCapabilitiesForTest(): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
let hostCapabilities;
|
|
2
|
+
/**
|
|
3
|
+
* Has {@link setHostCapabilities} run yet? Distinct from "the host
|
|
4
|
+
* advertised nothing" — before boot's `connectApp` resolves, this is
|
|
5
|
+
* `false` AND `hostCanRelayToolCalls()` is `false`, which look
|
|
6
|
+
* identical to a silent host unless a caller checks this too. A
|
|
7
|
+
* gesture that fails in that mount-to-handshake window is a timing
|
|
8
|
+
* artifact, not evidence about the host, so callers that latch on
|
|
9
|
+
* confirmed incapability (ggui#440) MUST require this before trusting
|
|
10
|
+
* capability absence.
|
|
11
|
+
*/
|
|
12
|
+
let captured = false;
|
|
13
|
+
/**
|
|
14
|
+
* Record the host's advertised capabilities. Called once at boot, from
|
|
15
|
+
* `bootSequence`, immediately after `connectApp` resolves — before that
|
|
16
|
+
* point `App.getHostCapabilities()` returns `undefined`.
|
|
17
|
+
*/
|
|
18
|
+
export function setHostCapabilities(caps) {
|
|
19
|
+
hostCapabilities = caps;
|
|
20
|
+
captured = true;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Did the host advertise that it can proxy a guest `tools/call` to the
|
|
24
|
+
* MCP server? `false` also covers "we have not connected yet" and "the
|
|
25
|
+
* host advertised nothing" — callers must treat it as *unconfirmed*,
|
|
26
|
+
* not as *impossible*.
|
|
27
|
+
*/
|
|
28
|
+
export function hostCanRelayToolCalls() {
|
|
29
|
+
return hostCapabilities?.serverTools !== undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Did the host advertise that it accepts `ui/message` from the view?
|
|
33
|
+
* This is the only view→host method that can start an agent turn, so a
|
|
34
|
+
* host without it cannot be woken by the doorbell — the user has to
|
|
35
|
+
* send a message themselves.
|
|
36
|
+
*/
|
|
37
|
+
export function hostCanReceiveMessages() {
|
|
38
|
+
return hostCapabilities?.message !== undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Has the handshake resolved capabilities at all yet? See {@link captured}.
|
|
42
|
+
* Callers that treat capability absence as confirmed incapability (as
|
|
43
|
+
* opposed to merely unconfirmed) must gate on this first — otherwise
|
|
44
|
+
* the pre-handshake window (where absence is just "not asked yet") is
|
|
45
|
+
* indistinguishable from a host that answered and advertised nothing.
|
|
46
|
+
*/
|
|
47
|
+
export function hostCapabilitiesCaptured() {
|
|
48
|
+
return captured;
|
|
49
|
+
}
|
|
50
|
+
/** @internal — exported for unit tests to reset module state. */
|
|
51
|
+
export function __resetHostCapabilitiesForTest() {
|
|
52
|
+
hostCapabilities = undefined;
|
|
53
|
+
captured = false;
|
|
54
|
+
}
|