@guuey/chat 0.6.0 → 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/plan.d.ts.map +1 -1
- package/dist/plan.js +18 -3
- package/dist/react/guuey-chat.d.ts +47 -3
- package/dist/react/guuey-chat.d.ts.map +1 -1
- package/dist/react/guuey-chat.js +63 -4
- package/package.json +3 -3
- package/src/corpus/README.md +13 -1
- package/src/corpus/__snapshots__/corpus.test.ts.snap +103 -24
- package/src/corpus/fixtures.ts +97 -1
- package/src/plan.ts +25 -4
- package/src/react/guuey-chat.tsx +118 -6
package/dist/plan.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AAkCA,OAAO,
|
|
1
|
+
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AAkCA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,KAAK,EASV,gBAAgB,EAChB,mBAAmB,EACnB,cAAc,EAIf,MAAM,YAAY,CAAC;AAqmBpB,uCAAuC;AACvC,wBAAgB,cAAc,CAC5B,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,gBAAgB,EACxB,SAAS,GAAE,mBAAwB,GAClC,cAAc,CA0QhB;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,QAAQ,GAAG,cAAc,CAAC,GACxD;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAAE,GAAG,SAAS,CAiB5D"}
|
package/dist/plan.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { snapshotViewMount, toolResultViewMount,
|
|
1
|
+
import { snapshotViewMount, toolResultLocator, toolResultViewMount, uiResourceChannel, } from "@guuey/mcp-apps-host";
|
|
2
2
|
import { grantModeDisplay } from "./hitl.js";
|
|
3
3
|
/** R5's giant threshold: above this byte count the state is `giant`. */
|
|
4
4
|
const GIANT_RESULT_BYTES = 16_384;
|
|
@@ -164,6 +164,19 @@ function toolFailed(result) {
|
|
|
164
164
|
return result.errorText !== undefined && result.errorText !== "";
|
|
165
165
|
return false;
|
|
166
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* The sandbox-trust channel a view item will MOUNT on. For a resolved mount
|
|
169
|
+
* that is the mount's own channel; for the `"locator"` arm it is what the
|
|
170
|
+
* reader will assign at resolution — derived from the requested uri
|
|
171
|
+
* (`uiResourceChannel`), the same rule the reader itself applies. Since the
|
|
172
|
+
* ggui vendor arm retired (guuey#209) every live ggui card is a locator at
|
|
173
|
+
* plan time, so the channel-aware label must look through the arm.
|
|
174
|
+
*/
|
|
175
|
+
function trustChannel(item) {
|
|
176
|
+
if (item.mount === null)
|
|
177
|
+
return item.channel === "ggui" || item.channel === "inline" ? item.channel : null;
|
|
178
|
+
return item.mount.channel === "locator" ? uiResourceChannel(item.mount.resourceUri) : item.mount.channel;
|
|
179
|
+
}
|
|
167
180
|
function viewLabel(item, policy) {
|
|
168
181
|
const s = policy.strings;
|
|
169
182
|
switch (item.phase) {
|
|
@@ -176,7 +189,9 @@ function viewLabel(item, policy) {
|
|
|
176
189
|
case "no-handshake":
|
|
177
190
|
// Channel-aware (R6): a ggui shell that never handshakes is a boot
|
|
178
191
|
// failure; inline tenant HTML may legitimately be a non-App document.
|
|
179
|
-
|
|
192
|
+
// Keyed on the TRUST channel (locators resolve to it by uri), not the
|
|
193
|
+
// mount arm — a live ggui card is a locator until its read lands.
|
|
194
|
+
return trustChannel(item) === "ggui" ? s.viewBootFailure : s.viewInlineFallback;
|
|
180
195
|
}
|
|
181
196
|
}
|
|
182
197
|
function unknownFromValue(key, typeName, value, policy, overrides) {
|
|
@@ -308,7 +323,7 @@ function planAssistantSource(source, slot, inputs, policy, overrides) {
|
|
|
308
323
|
// `result` is narrowed by `mount !== undefined` above; the scope
|
|
309
324
|
// is the PERSISTED locator (`uiData.resourceUri`), never the
|
|
310
325
|
// mount payload's own uri (synthetic for a ggui shell).
|
|
311
|
-
actionScope: result ? (
|
|
326
|
+
actionScope: result ? (toolResultLocator(result) ?? null) : null,
|
|
312
327
|
};
|
|
313
328
|
view.label = viewLabel(view, policy);
|
|
314
329
|
items.push(view);
|
|
@@ -83,16 +83,48 @@ export interface GuueyChatHandle {
|
|
|
83
83
|
}): void;
|
|
84
84
|
/** Focus the composer input. */
|
|
85
85
|
focusComposer(): void;
|
|
86
|
+
/**
|
|
87
|
+
* The CURRENT persisted thread id, or `null` before the first turn is
|
|
88
|
+
* admitted (it hydrates from storage on mount and from the pod's
|
|
89
|
+
* `session` frame on the first send). Read on demand — a live value, not
|
|
90
|
+
* a mount-time snapshot — so a host can key its own per-thread state
|
|
91
|
+
* without wrapping `adapters.storage`. For a push-style notification use
|
|
92
|
+
* {@link GuueyChatProps.onThread}.
|
|
93
|
+
*/
|
|
94
|
+
readonly threadId: string | null;
|
|
86
95
|
}
|
|
87
96
|
export interface GuueyChatProps {
|
|
88
97
|
/** Pod base URL (with or without `/agent/invoke`). `null` disables chat. */
|
|
89
98
|
endpointUrl: string | null;
|
|
90
99
|
/** Owning app id — namespaces the persisted threadId. */
|
|
91
100
|
appId?: string;
|
|
101
|
+
/**
|
|
102
|
+
* The guuey public API base (`…/v1`). Enables the batteries-included
|
|
103
|
+
* read paths without hand-wiring: when set and no `adapters` are given,
|
|
104
|
+
* the default `createWebAdapters` gains transcript history; when set and
|
|
105
|
+
* no `reader` is given, a `UiResourceReader` is built over the same
|
|
106
|
+
* identity so generative-UI locators resolve (guuey#221 — a guest kit
|
|
107
|
+
* user has no bearer to construct one with). Explicit `adapters` /
|
|
108
|
+
* `reader` always win. Absent → today's behavior (no history, no
|
|
109
|
+
* default reader; locators render as expired, labeled).
|
|
110
|
+
*/
|
|
111
|
+
apiBaseUrl?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Identity for the default adapters + default reader — the same two
|
|
114
|
+
* resolvers `createWebAdapters` takes (bearer wins; guest secret next;
|
|
115
|
+
* neither → cookie identity). Ignored when explicit `adapters` and
|
|
116
|
+
* `reader` are both supplied.
|
|
117
|
+
*/
|
|
118
|
+
getAccessToken?: (opts?: {
|
|
119
|
+
forceRefresh?: boolean;
|
|
120
|
+
}) => Promise<string | null>;
|
|
121
|
+
getGuestSecret?: () => string | null;
|
|
92
122
|
/**
|
|
93
123
|
* Host couplings (storage / id / transport / history). Default:
|
|
94
|
-
* `createWebAdapters(
|
|
95
|
-
* transport (cookie/guest
|
|
124
|
+
* `createWebAdapters({ apiBaseUrl, getAccessToken, getGuestSecret })` —
|
|
125
|
+
* localStorage thread persistence + the web SSE transport (cookie/guest
|
|
126
|
+
* identity, saturation + cold-start retries), plus history when
|
|
127
|
+
* `apiBaseUrl` is set.
|
|
96
128
|
*/
|
|
97
129
|
adapters?: AgentInvokeAdapters;
|
|
98
130
|
/** Policy preset (spec §5). Default `"calm"`. */
|
|
@@ -107,7 +139,11 @@ export interface GuueyChatProps {
|
|
|
107
139
|
mode?: ThemeMode;
|
|
108
140
|
/** DOM windowing (§3.2). `false` renders everything. */
|
|
109
141
|
window?: TranscriptWindowing | false;
|
|
110
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* R6 locator resolution (history cards + meta-less live renders). See
|
|
144
|
+
* `useTranscript`. Defaults from `apiBaseUrl` (+ `endpointUrl` for the
|
|
145
|
+
* pod door) when omitted; an explicit reader always wins.
|
|
146
|
+
*/
|
|
111
147
|
reader?: UiResourceReader;
|
|
112
148
|
/** The debug sink (spec §5) — fires only under the debug policy. */
|
|
113
149
|
onDebugEvent?: (event: ChatDebugEvent) => void;
|
|
@@ -135,6 +171,14 @@ export interface GuueyChatProps {
|
|
|
135
171
|
* the same stable handle the ref receives.
|
|
136
172
|
*/
|
|
137
173
|
onReady?: (handle: GuueyChatHandle) => void;
|
|
174
|
+
/**
|
|
175
|
+
* Fires with the thread id when it first hydrates and again whenever it
|
|
176
|
+
* changes to a DIFFERENT id (a new chat, a reset that starts a fresh
|
|
177
|
+
* thread). Never fires for `null`, and never re-fires for the same id
|
|
178
|
+
* across re-renders or StrictMode's remount cycle. Pair with
|
|
179
|
+
* {@link GuueyChatHandle.threadId} for the pull-style read.
|
|
180
|
+
*/
|
|
181
|
+
onThread?: (threadId: string) => void;
|
|
138
182
|
className?: string;
|
|
139
183
|
style?: CSSProperties;
|
|
140
184
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guuey-chat.d.ts","sourceRoot":"","sources":["../../src/react/guuey-chat.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,OAAO,EAQL,KAAK,aAAa,EAEnB,MAAM,OAAO,CAAC;AACf,OAAO,
|
|
1
|
+
{"version":3,"file":"guuey-chat.d.ts","sourceRoot":"","sources":["../../src/react/guuey-chat.tsx"],"names":[],"mappings":"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"}
|
package/dist/react/guuey-chat.js
CHANGED
|
@@ -46,7 +46,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
46
46
|
* native GuueyChat, so there is no native surface to put a handle on yet.
|
|
47
47
|
*/
|
|
48
48
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
49
|
-
import { createWebAdapters } from "@guuey/agent-client";
|
|
49
|
+
import { createUiResourceReader, createWebAdapters, } from "@guuey/agent-client";
|
|
50
50
|
import { useAgentInvoke } from "@guuey/agent-client/react";
|
|
51
51
|
import { calmPolicy, debugPolicy } from "../policy.js";
|
|
52
52
|
import { defaultChatStrings } from "../strings.js";
|
|
@@ -59,9 +59,47 @@ const PROMPT_STATE = {
|
|
|
59
59
|
dismiss: "dismissed",
|
|
60
60
|
};
|
|
61
61
|
export const GuueyChat = forwardRef(function GuueyChat(props, ref) {
|
|
62
|
-
const { endpointUrl, appId, adapters: adaptersProp, preset = "calm", policy: policyOverrides, components, strings: stringOverrides, theme = DEFAULT_CHAT_THEME, mode = "light", window: windowing, reader, onDebugEvent, viewProps, onPromptAction, onHitlAnswer, onErrorAction, onReady, className, style, } = props;
|
|
63
|
-
const adapters = useMemo(() => adaptersProp ??
|
|
62
|
+
const { endpointUrl, appId, apiBaseUrl, getAccessToken, getGuestSecret, adapters: adaptersProp, preset = "calm", policy: policyOverrides, components, strings: stringOverrides, theme = DEFAULT_CHAT_THEME, mode = "light", window: windowing, reader, onDebugEvent, viewProps, onPromptAction, onHitlAnswer, onErrorAction, onReady, onThread, className, style, } = props;
|
|
63
|
+
const adapters = useMemo(() => adaptersProp ??
|
|
64
|
+
createWebAdapters({
|
|
65
|
+
...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}),
|
|
66
|
+
...(getAccessToken !== undefined ? { getAccessToken } : {}),
|
|
67
|
+
...(getGuestSecret !== undefined ? { getGuestSecret } : {}),
|
|
68
|
+
}), [adaptersProp, apiBaseUrl, getAccessToken, getGuestSecret]);
|
|
64
69
|
const invoke = useAgentInvoke({ endpointUrl, ...(appId !== undefined ? { appId } : {}), adapters, preserveBlocks: true });
|
|
70
|
+
// Default reader (guuey#221): built over the SAME identity as the
|
|
71
|
+
// transport/history, targeting the pod door (live turns) then the
|
|
72
|
+
// platform door (persisted). The threadId hydrates after mount and can
|
|
73
|
+
// change on reset, so the reader is a stable function that reads the
|
|
74
|
+
// CURRENT thread through a ref — `useTranscript` sees one reader for the
|
|
75
|
+
// component's life and never re-resolves every mount on a thread flip.
|
|
76
|
+
const threadIdRef = useRef(invoke.threadId);
|
|
77
|
+
threadIdRef.current = invoke.threadId;
|
|
78
|
+
const defaultReader = useMemo(() => {
|
|
79
|
+
if (apiBaseUrl === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
return async (resourceUri) => {
|
|
82
|
+
const threadId = threadIdRef.current;
|
|
83
|
+
// No thread yet ⇒ nothing persisted to scope a read to; a live-turn
|
|
84
|
+
// locator always arrives with the thread already admitted (the
|
|
85
|
+
// `session` frame precedes tool results).
|
|
86
|
+
if (threadId === null)
|
|
87
|
+
return undefined;
|
|
88
|
+
// Assembled per read: `createUiResourceReader` is a cheap closure, and
|
|
89
|
+
// the guest secret is re-resolved each call so a rotation takes
|
|
90
|
+
// effect immediately — the same per-request property
|
|
91
|
+
// `createWebAdapters` documents for its own resolvers.
|
|
92
|
+
const read = createUiResourceReader({
|
|
93
|
+
apiBaseUrl,
|
|
94
|
+
threadId,
|
|
95
|
+
endpointUrl,
|
|
96
|
+
...(getAccessToken !== undefined ? { getAccessToken } : {}),
|
|
97
|
+
guestSecret: getGuestSecret ? getGuestSecret() : null,
|
|
98
|
+
});
|
|
99
|
+
return read(resourceUri);
|
|
100
|
+
};
|
|
101
|
+
}, [apiBaseUrl, endpointUrl, getAccessToken, getGuestSecret]);
|
|
102
|
+
const effectiveReader = reader ?? defaultReader;
|
|
65
103
|
const policy = useMemo(() => {
|
|
66
104
|
const factory = preset === "debug" ? debugPolicy : calmPolicy;
|
|
67
105
|
const strings = {
|
|
@@ -75,7 +113,7 @@ export const GuueyChat = forwardRef(function GuueyChat(props, ref) {
|
|
|
75
113
|
const { plan, toggle, resolvedMounts, onViewPhase } = useTranscript({
|
|
76
114
|
inputs,
|
|
77
115
|
policy,
|
|
78
|
-
...(
|
|
116
|
+
...(effectiveReader !== undefined ? { reader: effectiveReader } : {}),
|
|
79
117
|
...(onDebugEvent !== undefined ? { onDebugEvent } : {}),
|
|
80
118
|
});
|
|
81
119
|
// ── Composer ─────────────────────────────────────────────────────────
|
|
@@ -115,8 +153,29 @@ export const GuueyChat = forwardRef(function GuueyChat(props, ref) {
|
|
|
115
153
|
focusComposer: () => {
|
|
116
154
|
inputRef.current?.focus();
|
|
117
155
|
},
|
|
156
|
+
// A getter, not a captured value: the handle is created once, but the
|
|
157
|
+
// thread hydrates after mount and can change — reads go through the
|
|
158
|
+
// same ref the default reader uses, so it is always the live id.
|
|
159
|
+
get threadId() {
|
|
160
|
+
return threadIdRef.current;
|
|
161
|
+
},
|
|
118
162
|
}), []);
|
|
119
163
|
useImperativeHandle(ref, () => handle, [handle]);
|
|
164
|
+
// `onThread` fires on the first hydrated id and on each DISTINCT change —
|
|
165
|
+
// never for null, never twice for the same id (a StrictMode remount
|
|
166
|
+
// re-runs the effect with the same value; the last-notified ref absorbs
|
|
167
|
+
// it). Read the callback through the live ref so an inline arrow prop
|
|
168
|
+
// doesn't churn the effect.
|
|
169
|
+
const lastNotifiedThreadRef = useRef(null);
|
|
170
|
+
const onThreadRef = useRef(onThread);
|
|
171
|
+
onThreadRef.current = onThread;
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
const id = invoke.threadId;
|
|
174
|
+
if (id === null || id === lastNotifiedThreadRef.current)
|
|
175
|
+
return;
|
|
176
|
+
lastNotifiedThreadRef.current = id;
|
|
177
|
+
onThreadRef.current?.(id);
|
|
178
|
+
}, [invoke.threadId]);
|
|
120
179
|
// `onReady` fires once per instance, on mount, with the stable handle
|
|
121
180
|
// (guarded ref: StrictMode's remount cycle must not double-fire it).
|
|
122
181
|
const readyFiredRef = useRef(false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "The default end-user transcript UI for guuey agents — the headless view-model (planTranscript), calm/debug presets, the GuueyChatTheme token schema, the ChatStrings i18n seam, and (at ./react) the component kit that renders it: per-category components, <Transcript>, the live + history input assemblers, and stick-to-bottom/windowed/accessible transcript behavior; at ./native, the React Native renderer over the same view-model.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@silverprotocol/core": "0.5.0",
|
|
40
40
|
"@silverprotocol/richtext": "0.5.0",
|
|
41
41
|
"zod": "^4.3.6",
|
|
42
|
-
"@guuey/agent-client": "0.
|
|
43
|
-
"@guuey/mcp-apps-host": "0.
|
|
42
|
+
"@guuey/agent-client": "0.7.0",
|
|
43
|
+
"@guuey/mcp-apps-host": "0.7.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"react": ">=18",
|
package/src/corpus/README.md
CHANGED
|
@@ -44,7 +44,19 @@ witness.
|
|
|
44
44
|
paths, labeled and never agent-voiced, provenance under debug only),
|
|
45
45
|
and family 22 (`promoted-view` — guuey#204 "promote and reference": the
|
|
46
46
|
one mount a host stage shows chips via `promotedViewKey`, every other
|
|
47
|
-
mount stays full, no/stale key is byte-identical to today)
|
|
47
|
+
mount stays full, no/stale key is byte-identical to today), and family
|
|
48
|
+
23 (`meta-less-locator` — guuey#209 route-A: a `_meta`-withholding
|
|
49
|
+
producer's `ui://` locator rides `structuredContent` (AgJSON §2.1 model-
|
|
50
|
+
channel routing) and STILL mounts as a locator — before this it rendered
|
|
51
|
+
nothing at all, the corpus's first "dark, not degraded" shape), and
|
|
52
|
+
family 24 (`prod-wire-ggui-render` — the vendor arm's retirement,
|
|
53
|
+
guuey#209: the EXACT shape ggui's read-plane-only PRODUCTION posture
|
|
54
|
+
emits (no `_meta`, no `uiData`, a `structuredContent` locator), planned as
|
|
55
|
+
a locator and RESOLVED through the kit's real reader assembly over a
|
|
56
|
+
C2-shaped read shell (`gguiReadShell`) to the `"ggui"` channel — the
|
|
57
|
+
whole converged path in plain Node; 24c pins that a bootstrap-carrying
|
|
58
|
+
result (fixture 7's shape) rides the same arm to the same outcome, and
|
|
59
|
+
family 18's production capture asserts the same).
|
|
48
60
|
- `capture.ts` / `captures/` — family 18: production-capture replay
|
|
49
61
|
through the real `invokeTurn` (provenance in the module header).
|
|
50
62
|
- `corpus.test.ts` — the runner: per-fixture assertions + snapshots, plain
|
|
@@ -1115,23 +1115,14 @@ exports[`corpus > 7. view-never-handshakes — channel-aware labels on both chan
|
|
|
1115
1115
|
{
|
|
1116
1116
|
"actionScope": "ui://ggui/render/render_00000000-0000-4000-8000-300000000001/c0ffee",
|
|
1117
1117
|
"attribution": "via ggui render",
|
|
1118
|
-
"channel": "
|
|
1118
|
+
"channel": "locator",
|
|
1119
1119
|
"expanded": true,
|
|
1120
1120
|
"key": "view.tGgui",
|
|
1121
1121
|
"kind": "view",
|
|
1122
1122
|
"label": "This view couldn't start",
|
|
1123
1123
|
"mount": {
|
|
1124
|
-
"channel": "
|
|
1125
|
-
"
|
|
1126
|
-
"mimeType": "text/html",
|
|
1127
|
-
"text": "<!doctype html>
|
|
1128
|
-
<html lang="en" style="background:transparent"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light dark"><title>ggui render</title></head>
|
|
1129
|
-
<body style="margin:0;background:transparent">
|
|
1130
|
-
<script>globalThis.__GGUI_META__ = {"ai.ggui/render":{"sessionId":"render_00000000-0000-4000-8000-300000000001","appId":"APP00000","runtimeUrl":"https://dev.mcp.sandbox.ggui.ai/_ggui/iframe-runtime.js","wsUrl":"wss://dev.mcp.sandbox.ggui.ai/ws","wsToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","expiresAt":"2026-07-29T11:07:58.000Z","lastSequence":0,"propsJson":"{}"}};</script>
|
|
1131
|
-
<script type="module" crossorigin="anonymous" src="https://dev.mcp.sandbox.ggui.ai/_ggui/iframe-runtime.js"></script>
|
|
1132
|
-
</body></html>",
|
|
1133
|
-
"uri": "ui://ggui/render/render_00000000-0000-4000-8000-300000000001/c0ffee",
|
|
1134
|
-
},
|
|
1124
|
+
"channel": "locator",
|
|
1125
|
+
"resourceUri": "ui://ggui/render/render_00000000-0000-4000-8000-300000000001/c0ffee",
|
|
1135
1126
|
},
|
|
1136
1127
|
"phase": "no-handshake",
|
|
1137
1128
|
"toolTitle": "ggui render",
|
|
@@ -1641,23 +1632,14 @@ exports[`corpus > 19. persisted-plus-live — the fold owns its trailing turns;
|
|
|
1641
1632
|
{
|
|
1642
1633
|
"actionScope": "ui://ggui/render/render_00000000-0000-4000-8000-300000000001/c0ffee",
|
|
1643
1634
|
"attribution": "via ggui render",
|
|
1644
|
-
"channel": "
|
|
1635
|
+
"channel": "locator",
|
|
1645
1636
|
"expanded": true,
|
|
1646
1637
|
"key": "view.tA",
|
|
1647
1638
|
"kind": "view",
|
|
1648
1639
|
"label": "Loading view…",
|
|
1649
1640
|
"mount": {
|
|
1650
|
-
"channel": "
|
|
1651
|
-
"
|
|
1652
|
-
"mimeType": "text/html",
|
|
1653
|
-
"text": "<!doctype html>
|
|
1654
|
-
<html lang="en" style="background:transparent"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light dark"><title>ggui render</title></head>
|
|
1655
|
-
<body style="margin:0;background:transparent">
|
|
1656
|
-
<script>globalThis.__GGUI_META__ = {"ai.ggui/render":{"sessionId":"render_00000000-0000-4000-8000-300000000001","appId":"APP00000","runtimeUrl":"https://dev.mcp.sandbox.ggui.ai/_ggui/iframe-runtime.js","wsUrl":"wss://dev.mcp.sandbox.ggui.ai/ws","wsToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","expiresAt":"2026-07-29T11:07:58.000Z","lastSequence":0,"propsJson":"{}"}};</script>
|
|
1657
|
-
<script type="module" crossorigin="anonymous" src="https://dev.mcp.sandbox.ggui.ai/_ggui/iframe-runtime.js"></script>
|
|
1658
|
-
</body></html>",
|
|
1659
|
-
"uri": "ui://ggui/render/render_00000000-0000-4000-8000-300000000001/c0ffee",
|
|
1660
|
-
},
|
|
1641
|
+
"channel": "locator",
|
|
1642
|
+
"resourceUri": "ui://ggui/render/render_00000000-0000-4000-8000-300000000001/c0ffee",
|
|
1661
1643
|
},
|
|
1662
1644
|
"phase": "negotiating",
|
|
1663
1645
|
"toolTitle": "ggui render",
|
|
@@ -1892,3 +1874,100 @@ exports[`corpus > 22. promoted-view — the stage's mount chips (guuey#204); oth
|
|
|
1892
1874
|
"status": null,
|
|
1893
1875
|
}
|
|
1894
1876
|
`;
|
|
1877
|
+
|
|
1878
|
+
exports[`corpus > 23. meta-less-locator — a \`_meta\`-withholding producer's render still mounts (as a locator), never dark (guuey#209) 1`] = `
|
|
1879
|
+
{
|
|
1880
|
+
"items": [
|
|
1881
|
+
{
|
|
1882
|
+
"expanded": true,
|
|
1883
|
+
"key": "u0",
|
|
1884
|
+
"kind": "user",
|
|
1885
|
+
"retry": false,
|
|
1886
|
+
"state": "sent",
|
|
1887
|
+
"text": "render a slot picker",
|
|
1888
|
+
},
|
|
1889
|
+
{
|
|
1890
|
+
"argsPreview": null,
|
|
1891
|
+
"attribution": true,
|
|
1892
|
+
"expanded": false,
|
|
1893
|
+
"key": "tool.t1",
|
|
1894
|
+
"kind": "tool",
|
|
1895
|
+
"name": "ggui_render",
|
|
1896
|
+
"result": null,
|
|
1897
|
+
"state": "done",
|
|
1898
|
+
"title": "ggui render",
|
|
1899
|
+
"toolCallId": "t1",
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
"actionScope": "ui://ggui/render/render_dark/h1",
|
|
1903
|
+
"attribution": "via ggui render",
|
|
1904
|
+
"channel": "locator",
|
|
1905
|
+
"expanded": true,
|
|
1906
|
+
"key": "view.t1",
|
|
1907
|
+
"kind": "view",
|
|
1908
|
+
"label": "Loading view…",
|
|
1909
|
+
"mount": {
|
|
1910
|
+
"channel": "locator",
|
|
1911
|
+
"resourceUri": "ui://ggui/render/render_dark/h1",
|
|
1912
|
+
},
|
|
1913
|
+
"phase": "negotiating",
|
|
1914
|
+
"toolTitle": "ggui render",
|
|
1915
|
+
},
|
|
1916
|
+
],
|
|
1917
|
+
"recovery": null,
|
|
1918
|
+
"status": null,
|
|
1919
|
+
}
|
|
1920
|
+
`;
|
|
1921
|
+
|
|
1922
|
+
exports[`corpus > 24. prod-wire-ggui-render — ggui's production posture: no _meta, no uiData, a structuredContent locator → mounts as a locator (guuey#209) 1`] = `
|
|
1923
|
+
{
|
|
1924
|
+
"items": [
|
|
1925
|
+
{
|
|
1926
|
+
"expanded": true,
|
|
1927
|
+
"key": "u0",
|
|
1928
|
+
"kind": "user",
|
|
1929
|
+
"retry": false,
|
|
1930
|
+
"state": "sent",
|
|
1931
|
+
"text": "show me the board",
|
|
1932
|
+
},
|
|
1933
|
+
{
|
|
1934
|
+
"argsPreview": null,
|
|
1935
|
+
"attribution": true,
|
|
1936
|
+
"expanded": false,
|
|
1937
|
+
"key": "tool.t1",
|
|
1938
|
+
"kind": "tool",
|
|
1939
|
+
"name": "ggui_render",
|
|
1940
|
+
"result": null,
|
|
1941
|
+
"state": "done",
|
|
1942
|
+
"title": "ggui render",
|
|
1943
|
+
"toolCallId": "t1",
|
|
1944
|
+
},
|
|
1945
|
+
{
|
|
1946
|
+
"actionScope": "ui://ggui/render/render_5d11b1b4-75ba-44e5-81bb-0b420b86bb8e/7328555b44e72bd7",
|
|
1947
|
+
"attribution": "via ggui render",
|
|
1948
|
+
"channel": "locator",
|
|
1949
|
+
"expanded": true,
|
|
1950
|
+
"key": "view.t1",
|
|
1951
|
+
"kind": "view",
|
|
1952
|
+
"label": "Loading view…",
|
|
1953
|
+
"mount": {
|
|
1954
|
+
"channel": "locator",
|
|
1955
|
+
"resourceUri": "ui://ggui/render/render_5d11b1b4-75ba-44e5-81bb-0b420b86bb8e/7328555b44e72bd7",
|
|
1956
|
+
},
|
|
1957
|
+
"phase": "negotiating",
|
|
1958
|
+
"toolTitle": "ggui render",
|
|
1959
|
+
},
|
|
1960
|
+
{
|
|
1961
|
+
"expanded": true,
|
|
1962
|
+
"key": "a0.t0",
|
|
1963
|
+
"kind": "text",
|
|
1964
|
+
"markdown": true,
|
|
1965
|
+
"stopped": false,
|
|
1966
|
+
"streaming": false,
|
|
1967
|
+
"text": "Here is your board.",
|
|
1968
|
+
},
|
|
1969
|
+
],
|
|
1970
|
+
"recovery": null,
|
|
1971
|
+
"status": null,
|
|
1972
|
+
}
|
|
1973
|
+
`;
|
package/src/corpus/fixtures.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
|
|
25
25
|
// The ggui render shapes below mirror mcp-apps-host's own capture-derived
|
|
26
26
|
// test fixtures (synthetic ids, production shape — ggui-render.test.ts).
|
|
27
|
-
const GGUI_RESOURCE_URI = "ui://ggui/render/render_00000000-0000-4000-8000-300000000001/c0ffee";
|
|
27
|
+
export const GGUI_RESOURCE_URI = "ui://ggui/render/render_00000000-0000-4000-8000-300000000001/c0ffee";
|
|
28
28
|
const GGUI_META_KEY = "ai.ggui/render";
|
|
29
29
|
const GGUI_SLICE = {
|
|
30
30
|
sessionId: "render_00000000-0000-4000-8000-300000000001",
|
|
@@ -555,3 +555,99 @@ export function promotedView(): { inputs: TranscriptInputs; promotedKey: string
|
|
|
555
555
|
const inputs = driveTurn(events, { userText: "show me two cards" });
|
|
556
556
|
return { inputs, promotedKey: "view.t2" };
|
|
557
557
|
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 23. meta-less-locator — guuey#209 route-A finding: a producer that
|
|
561
|
+
* withholds tool-result `_meta` (ggui's non-prod posture; any plain-locator
|
|
562
|
+
* MCP server) yields a fold block with NO uiData and NO _meta — AgJSON §2.1
|
|
563
|
+
* routes the sibling structuredContent to the MODEL channel — and the
|
|
564
|
+
* `ui://` locator rides `structuredContent.resourceUri`. Wire keys per the
|
|
565
|
+
* 2026-08-16 dev dump: type,toolCallId,content,outcome,isError,
|
|
566
|
+
* structuredContent. Before #209 this rendered NOTHING (dark, not
|
|
567
|
+
* "expired") and persisted no placeholder row.
|
|
568
|
+
*/
|
|
569
|
+
export function metaLessLocator(): TranscriptInputs {
|
|
570
|
+
const s = seqSource();
|
|
571
|
+
const events: InvokeTurnEvent[] = [
|
|
572
|
+
session,
|
|
573
|
+
frame(boot(s), { status: "thinking" }),
|
|
574
|
+
frame(
|
|
575
|
+
toolPart(s, "t1", "ggui_render", {
|
|
576
|
+
content: [{ type: "text", text: "rendered" }],
|
|
577
|
+
outcome: "ok",
|
|
578
|
+
isError: false,
|
|
579
|
+
structuredContent: { resourceUri: "ui://ggui/render/render_dark/h1", sessionId: "render_dark" },
|
|
580
|
+
}),
|
|
581
|
+
{ status: "using-tool", activeTool: "ggui_render" },
|
|
582
|
+
),
|
|
583
|
+
doneEvent,
|
|
584
|
+
];
|
|
585
|
+
return driveTurn(events, { userText: "render a slot picker" });
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* 24. prod-wire-ggui-render — the EXACT shape ggui's read-plane-only PRODUCTION
|
|
590
|
+
* posture emits (guuey#209 prod probe, 2026-08-16): a `ggui_render` tool.done
|
|
591
|
+
* with NO `_meta["ai.ggui/render"]`, NO `uiData`, only
|
|
592
|
+
* `structuredContent.resourceUri = ui://ggui/render/<renderId>/<hash>`. The
|
|
593
|
+
* vendor arm is retired: this mounts as a LOCATOR, and the "ggui" trust
|
|
594
|
+
* channel is assigned at RESOLUTION from the uri — the corpus resolves it
|
|
595
|
+
* through the kit's real reader assembly over a C2-shaped read shell
|
|
596
|
+
* ({@link gguiReadShell}) to prove the whole converged path in plain Node.
|
|
597
|
+
*/
|
|
598
|
+
export const PROD_WIRE_RENDER_URI =
|
|
599
|
+
"ui://ggui/render/render_5d11b1b4-75ba-44e5-81bb-0b420b86bb8e/7328555b44e72bd7";
|
|
600
|
+
export const PROD_WIRE_RUNTIME_URL = "https://mcp.ggui.ai/_ggui/iframe-runtime.js";
|
|
601
|
+
export const PROD_WIRE_WS_URL = "wss://mcp.ggui.ai/ws";
|
|
602
|
+
|
|
603
|
+
export function prodWireGguiRender(): TranscriptInputs {
|
|
604
|
+
const s = seqSource();
|
|
605
|
+
const events: InvokeTurnEvent[] = [
|
|
606
|
+
session,
|
|
607
|
+
frame(boot(s), { status: "thinking" }),
|
|
608
|
+
frame(
|
|
609
|
+
toolPart(s, "t1", "ggui_render", {
|
|
610
|
+
content: [{ type: "text", text: PROD_WIRE_RENDER_URI }],
|
|
611
|
+
outcome: "ok",
|
|
612
|
+
isError: false,
|
|
613
|
+
structuredContent: {
|
|
614
|
+
resourceUri: PROD_WIRE_RENDER_URI,
|
|
615
|
+
sessionId: "render_5d11b1b4-75ba-44e5-81bb-0b420b86bb8e",
|
|
616
|
+
},
|
|
617
|
+
}),
|
|
618
|
+
{ status: "using-tool", activeTool: "ggui_render" },
|
|
619
|
+
),
|
|
620
|
+
frame(textPart(s, "x", "Here is your board."), {
|
|
621
|
+
status: "responding",
|
|
622
|
+
activeTool: null,
|
|
623
|
+
text: "Here is your board.",
|
|
624
|
+
}),
|
|
625
|
+
doneEvent,
|
|
626
|
+
];
|
|
627
|
+
return driveTurn(events, { userText: "show me the board" });
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* What ggui's `resources/read` answers for a render locator (guuey#209 C2):
|
|
632
|
+
* the shell that boots ggui's runtime with the live-channel material
|
|
633
|
+
* (`wsUrl`, a `wsToken` minted FRESH at read time, `expiresAt`) inlined as
|
|
634
|
+
* `__GGUI_META__` — the same envelope the retired bootstrap arm used to get
|
|
635
|
+
* on the tool result, now younger than any inlined copy could be.
|
|
636
|
+
*/
|
|
637
|
+
export function gguiReadShell(opts: { runtimeUrl: string; wsUrl: string }): string {
|
|
638
|
+
const meta = {
|
|
639
|
+
"ai.ggui/render": {
|
|
640
|
+
runtimeUrl: opts.runtimeUrl,
|
|
641
|
+
wsUrl: opts.wsUrl,
|
|
642
|
+
wsToken: "minted-fresh-at-read",
|
|
643
|
+
expiresAt: "2026-08-16T00:03:00.000Z",
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
return (
|
|
647
|
+
`<!doctype html><html><head><meta charset="utf-8">` +
|
|
648
|
+
`<script>globalThis.__GGUI_META__ = ${JSON.stringify(meta)};</script>` +
|
|
649
|
+
`<script type="module" crossorigin="anonymous" src="${opts.runtimeUrl}"></script>` +
|
|
650
|
+
`</head><body style="margin:0;background:transparent"></body></html>`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
package/src/plan.ts
CHANGED
|
@@ -32,7 +32,13 @@
|
|
|
32
32
|
* append ordinals, so every existing key survives each re-plan.
|
|
33
33
|
*/
|
|
34
34
|
import type { AgBlock, AgReduceResult, JsonValue } from "@silverprotocol/core";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
snapshotViewMount,
|
|
37
|
+
toolResultLocator,
|
|
38
|
+
toolResultViewMount,
|
|
39
|
+
uiResourceChannel,
|
|
40
|
+
type ViewMount,
|
|
41
|
+
} from "@guuey/mcp-apps-host";
|
|
36
42
|
import type { TranscriptPolicy } from "./policy.js";
|
|
37
43
|
import { grantModeDisplay } from "./hitl.js";
|
|
38
44
|
import type {
|
|
@@ -252,8 +258,21 @@ function toolFailed(result: ToolResultBlock): boolean {
|
|
|
252
258
|
return false;
|
|
253
259
|
}
|
|
254
260
|
|
|
261
|
+
/**
|
|
262
|
+
* The sandbox-trust channel a view item will MOUNT on. For a resolved mount
|
|
263
|
+
* that is the mount's own channel; for the `"locator"` arm it is what the
|
|
264
|
+
* reader will assign at resolution — derived from the requested uri
|
|
265
|
+
* (`uiResourceChannel`), the same rule the reader itself applies. Since the
|
|
266
|
+
* ggui vendor arm retired (guuey#209) every live ggui card is a locator at
|
|
267
|
+
* plan time, so the channel-aware label must look through the arm.
|
|
268
|
+
*/
|
|
269
|
+
function trustChannel(item: Pick<ViewMountItem, "mount" | "channel">): "inline" | "ggui" | null {
|
|
270
|
+
if (item.mount === null) return item.channel === "ggui" || item.channel === "inline" ? item.channel : null;
|
|
271
|
+
return item.mount.channel === "locator" ? uiResourceChannel(item.mount.resourceUri) : item.mount.channel;
|
|
272
|
+
}
|
|
273
|
+
|
|
255
274
|
function viewLabel(
|
|
256
|
-
item: Pick<ViewMountItem, "phase" | "channel">,
|
|
275
|
+
item: Pick<ViewMountItem, "phase" | "channel" | "mount">,
|
|
257
276
|
policy: TranscriptPolicy,
|
|
258
277
|
): string | null {
|
|
259
278
|
const s = policy.strings;
|
|
@@ -267,7 +286,9 @@ function viewLabel(
|
|
|
267
286
|
case "no-handshake":
|
|
268
287
|
// Channel-aware (R6): a ggui shell that never handshakes is a boot
|
|
269
288
|
// failure; inline tenant HTML may legitimately be a non-App document.
|
|
270
|
-
|
|
289
|
+
// Keyed on the TRUST channel (locators resolve to it by uri), not the
|
|
290
|
+
// mount arm — a live ggui card is a locator until its read lands.
|
|
291
|
+
return trustChannel(item) === "ggui" ? s.viewBootFailure : s.viewInlineFallback;
|
|
271
292
|
}
|
|
272
293
|
}
|
|
273
294
|
|
|
@@ -411,7 +432,7 @@ function planAssistantSource(
|
|
|
411
432
|
// `result` is narrowed by `mount !== undefined` above; the scope
|
|
412
433
|
// is the PERSISTED locator (`uiData.resourceUri`), never the
|
|
413
434
|
// mount payload's own uri (synthetic for a ggui shell).
|
|
414
|
-
actionScope: result ? (
|
|
435
|
+
actionScope: result ? (toolResultLocator(result) ?? null) : null,
|
|
415
436
|
};
|
|
416
437
|
view.label = viewLabel(view, policy);
|
|
417
438
|
items.push(view);
|
package/src/react/guuey-chat.tsx
CHANGED
|
@@ -55,7 +55,11 @@ import {
|
|
|
55
55
|
type CSSProperties,
|
|
56
56
|
type ReactNode,
|
|
57
57
|
} from "react";
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
createUiResourceReader,
|
|
60
|
+
createWebAdapters,
|
|
61
|
+
type AgentInvokeAdapters,
|
|
62
|
+
} from "@guuey/agent-client";
|
|
59
63
|
import type { AgHitlAnswer, AgPausedAsk } from "@silverprotocol/core";
|
|
60
64
|
import { useAgentInvoke } from "@guuey/agent-client/react";
|
|
61
65
|
import type { UiResourceReader } from "@guuey/mcp-apps-host";
|
|
@@ -93,6 +97,15 @@ export interface GuueyChatHandle {
|
|
|
93
97
|
prefill(text: string, opts?: { focus?: boolean; append?: boolean }): void;
|
|
94
98
|
/** Focus the composer input. */
|
|
95
99
|
focusComposer(): void;
|
|
100
|
+
/**
|
|
101
|
+
* The CURRENT persisted thread id, or `null` before the first turn is
|
|
102
|
+
* admitted (it hydrates from storage on mount and from the pod's
|
|
103
|
+
* `session` frame on the first send). Read on demand — a live value, not
|
|
104
|
+
* a mount-time snapshot — so a host can key its own per-thread state
|
|
105
|
+
* without wrapping `adapters.storage`. For a push-style notification use
|
|
106
|
+
* {@link GuueyChatProps.onThread}.
|
|
107
|
+
*/
|
|
108
|
+
readonly threadId: string | null;
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
export interface GuueyChatProps {
|
|
@@ -100,10 +113,31 @@ export interface GuueyChatProps {
|
|
|
100
113
|
endpointUrl: string | null;
|
|
101
114
|
/** Owning app id — namespaces the persisted threadId. */
|
|
102
115
|
appId?: string;
|
|
116
|
+
/**
|
|
117
|
+
* The guuey public API base (`…/v1`). Enables the batteries-included
|
|
118
|
+
* read paths without hand-wiring: when set and no `adapters` are given,
|
|
119
|
+
* the default `createWebAdapters` gains transcript history; when set and
|
|
120
|
+
* no `reader` is given, a `UiResourceReader` is built over the same
|
|
121
|
+
* identity so generative-UI locators resolve (guuey#221 — a guest kit
|
|
122
|
+
* user has no bearer to construct one with). Explicit `adapters` /
|
|
123
|
+
* `reader` always win. Absent → today's behavior (no history, no
|
|
124
|
+
* default reader; locators render as expired, labeled).
|
|
125
|
+
*/
|
|
126
|
+
apiBaseUrl?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Identity for the default adapters + default reader — the same two
|
|
129
|
+
* resolvers `createWebAdapters` takes (bearer wins; guest secret next;
|
|
130
|
+
* neither → cookie identity). Ignored when explicit `adapters` and
|
|
131
|
+
* `reader` are both supplied.
|
|
132
|
+
*/
|
|
133
|
+
getAccessToken?: (opts?: { forceRefresh?: boolean }) => Promise<string | null>;
|
|
134
|
+
getGuestSecret?: () => string | null;
|
|
103
135
|
/**
|
|
104
136
|
* Host couplings (storage / id / transport / history). Default:
|
|
105
|
-
* `createWebAdapters(
|
|
106
|
-
* transport (cookie/guest
|
|
137
|
+
* `createWebAdapters({ apiBaseUrl, getAccessToken, getGuestSecret })` —
|
|
138
|
+
* localStorage thread persistence + the web SSE transport (cookie/guest
|
|
139
|
+
* identity, saturation + cold-start retries), plus history when
|
|
140
|
+
* `apiBaseUrl` is set.
|
|
107
141
|
*/
|
|
108
142
|
adapters?: AgentInvokeAdapters;
|
|
109
143
|
/** Policy preset (spec §5). Default `"calm"`. */
|
|
@@ -118,7 +152,11 @@ export interface GuueyChatProps {
|
|
|
118
152
|
mode?: ThemeMode;
|
|
119
153
|
/** DOM windowing (§3.2). `false` renders everything. */
|
|
120
154
|
window?: TranscriptWindowing | false;
|
|
121
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* R6 locator resolution (history cards + meta-less live renders). See
|
|
157
|
+
* `useTranscript`. Defaults from `apiBaseUrl` (+ `endpointUrl` for the
|
|
158
|
+
* pod door) when omitted; an explicit reader always wins.
|
|
159
|
+
*/
|
|
122
160
|
reader?: UiResourceReader;
|
|
123
161
|
/** The debug sink (spec §5) — fires only under the debug policy. */
|
|
124
162
|
onDebugEvent?: (event: ChatDebugEvent) => void;
|
|
@@ -147,6 +185,14 @@ export interface GuueyChatProps {
|
|
|
147
185
|
* the same stable handle the ref receives.
|
|
148
186
|
*/
|
|
149
187
|
onReady?: (handle: GuueyChatHandle) => void;
|
|
188
|
+
/**
|
|
189
|
+
* Fires with the thread id when it first hydrates and again whenever it
|
|
190
|
+
* changes to a DIFFERENT id (a new chat, a reset that starts a fresh
|
|
191
|
+
* thread). Never fires for `null`, and never re-fires for the same id
|
|
192
|
+
* across re-renders or StrictMode's remount cycle. Pair with
|
|
193
|
+
* {@link GuueyChatHandle.threadId} for the pull-style read.
|
|
194
|
+
*/
|
|
195
|
+
onThread?: (threadId: string) => void;
|
|
150
196
|
className?: string;
|
|
151
197
|
style?: CSSProperties;
|
|
152
198
|
}
|
|
@@ -164,6 +210,9 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
164
210
|
const {
|
|
165
211
|
endpointUrl,
|
|
166
212
|
appId,
|
|
213
|
+
apiBaseUrl,
|
|
214
|
+
getAccessToken,
|
|
215
|
+
getGuestSecret,
|
|
167
216
|
adapters: adaptersProp,
|
|
168
217
|
preset = "calm",
|
|
169
218
|
policy: policyOverrides,
|
|
@@ -179,13 +228,55 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
179
228
|
onHitlAnswer,
|
|
180
229
|
onErrorAction,
|
|
181
230
|
onReady,
|
|
231
|
+
onThread,
|
|
182
232
|
className,
|
|
183
233
|
style,
|
|
184
234
|
} = props;
|
|
185
235
|
|
|
186
|
-
const adapters = useMemo(
|
|
236
|
+
const adapters = useMemo(
|
|
237
|
+
() =>
|
|
238
|
+
adaptersProp ??
|
|
239
|
+
createWebAdapters({
|
|
240
|
+
...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}),
|
|
241
|
+
...(getAccessToken !== undefined ? { getAccessToken } : {}),
|
|
242
|
+
...(getGuestSecret !== undefined ? { getGuestSecret } : {}),
|
|
243
|
+
}),
|
|
244
|
+
[adaptersProp, apiBaseUrl, getAccessToken, getGuestSecret],
|
|
245
|
+
);
|
|
187
246
|
const invoke = useAgentInvoke({ endpointUrl, ...(appId !== undefined ? { appId } : {}), adapters, preserveBlocks: true });
|
|
188
247
|
|
|
248
|
+
// Default reader (guuey#221): built over the SAME identity as the
|
|
249
|
+
// transport/history, targeting the pod door (live turns) then the
|
|
250
|
+
// platform door (persisted). The threadId hydrates after mount and can
|
|
251
|
+
// change on reset, so the reader is a stable function that reads the
|
|
252
|
+
// CURRENT thread through a ref — `useTranscript` sees one reader for the
|
|
253
|
+
// component's life and never re-resolves every mount on a thread flip.
|
|
254
|
+
const threadIdRef = useRef<string | null>(invoke.threadId);
|
|
255
|
+
threadIdRef.current = invoke.threadId;
|
|
256
|
+
const defaultReader = useMemo<UiResourceReader | undefined>(() => {
|
|
257
|
+
if (apiBaseUrl === undefined) return undefined;
|
|
258
|
+
return async (resourceUri: string) => {
|
|
259
|
+
const threadId = threadIdRef.current;
|
|
260
|
+
// No thread yet ⇒ nothing persisted to scope a read to; a live-turn
|
|
261
|
+
// locator always arrives with the thread already admitted (the
|
|
262
|
+
// `session` frame precedes tool results).
|
|
263
|
+
if (threadId === null) return undefined;
|
|
264
|
+
// Assembled per read: `createUiResourceReader` is a cheap closure, and
|
|
265
|
+
// the guest secret is re-resolved each call so a rotation takes
|
|
266
|
+
// effect immediately — the same per-request property
|
|
267
|
+
// `createWebAdapters` documents for its own resolvers.
|
|
268
|
+
const read = createUiResourceReader({
|
|
269
|
+
apiBaseUrl,
|
|
270
|
+
threadId,
|
|
271
|
+
endpointUrl,
|
|
272
|
+
...(getAccessToken !== undefined ? { getAccessToken } : {}),
|
|
273
|
+
guestSecret: getGuestSecret ? getGuestSecret() : null,
|
|
274
|
+
});
|
|
275
|
+
return read(resourceUri);
|
|
276
|
+
};
|
|
277
|
+
}, [apiBaseUrl, endpointUrl, getAccessToken, getGuestSecret]);
|
|
278
|
+
const effectiveReader = reader ?? defaultReader;
|
|
279
|
+
|
|
189
280
|
const policy = useMemo(() => {
|
|
190
281
|
const factory = preset === "debug" ? debugPolicy : calmPolicy;
|
|
191
282
|
const strings: ChatStrings = {
|
|
@@ -200,7 +291,7 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
200
291
|
const { plan, toggle, resolvedMounts, onViewPhase } = useTranscript({
|
|
201
292
|
inputs,
|
|
202
293
|
policy,
|
|
203
|
-
...(
|
|
294
|
+
...(effectiveReader !== undefined ? { reader: effectiveReader } : {}),
|
|
204
295
|
...(onDebugEvent !== undefined ? { onDebugEvent } : {}),
|
|
205
296
|
});
|
|
206
297
|
|
|
@@ -243,12 +334,33 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
243
334
|
focusComposer: (): void => {
|
|
244
335
|
inputRef.current?.focus();
|
|
245
336
|
},
|
|
337
|
+
// A getter, not a captured value: the handle is created once, but the
|
|
338
|
+
// thread hydrates after mount and can change — reads go through the
|
|
339
|
+
// same ref the default reader uses, so it is always the live id.
|
|
340
|
+
get threadId(): string | null {
|
|
341
|
+
return threadIdRef.current;
|
|
342
|
+
},
|
|
246
343
|
}),
|
|
247
344
|
[],
|
|
248
345
|
);
|
|
249
346
|
|
|
250
347
|
useImperativeHandle(ref, () => handle, [handle]);
|
|
251
348
|
|
|
349
|
+
// `onThread` fires on the first hydrated id and on each DISTINCT change —
|
|
350
|
+
// never for null, never twice for the same id (a StrictMode remount
|
|
351
|
+
// re-runs the effect with the same value; the last-notified ref absorbs
|
|
352
|
+
// it). Read the callback through the live ref so an inline arrow prop
|
|
353
|
+
// doesn't churn the effect.
|
|
354
|
+
const lastNotifiedThreadRef = useRef<string | null>(null);
|
|
355
|
+
const onThreadRef = useRef(onThread);
|
|
356
|
+
onThreadRef.current = onThread;
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
const id = invoke.threadId;
|
|
359
|
+
if (id === null || id === lastNotifiedThreadRef.current) return;
|
|
360
|
+
lastNotifiedThreadRef.current = id;
|
|
361
|
+
onThreadRef.current?.(id);
|
|
362
|
+
}, [invoke.threadId]);
|
|
363
|
+
|
|
252
364
|
// `onReady` fires once per instance, on mount, with the stable handle
|
|
253
365
|
// (guarded ref: StrictMode's remount cycle must not double-fire it).
|
|
254
366
|
const readyFiredRef = useRef(false);
|