@guuey/chat 0.6.1 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AAkCA,OAAO,EAA6D,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,KAAK,EASV,gBAAgB,EAChB,mBAAmB,EACnB,cAAc,EAIf,MAAM,YAAY,CAAC;AAslBpB,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"}
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, toolResultLocator, toolResultViewMount } from "@guuey/mcp-apps-host";
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
- return item.channel === "ggui" ? s.viewBootFailure : s.viewInlineFallback;
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) {
@@ -83,6 +83,15 @@ 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. */
@@ -162,6 +171,14 @@ export interface GuueyChatProps {
162
171
  * the same stable handle the ref receives.
163
172
  */
164
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;
165
182
  className?: string;
166
183
  style?: CSSProperties;
167
184
  }
@@ -1 +1 @@
1
- {"version":3,"file":"guuey-chat.d.ts","sourceRoot":"","sources":["../../src/react/guuey-chat.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"guuey-chat.d.ts","sourceRoot":"","sources":["../../src/react/guuey-chat.tsx"],"names":[],"mappings":"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"}
@@ -59,7 +59,7 @@ const PROMPT_STATE = {
59
59
  dismiss: "dismissed",
60
60
  };
61
61
  export const GuueyChat = forwardRef(function GuueyChat(props, ref) {
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, className, style, } = props;
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
63
  const adapters = useMemo(() => adaptersProp ??
64
64
  createWebAdapters({
65
65
  ...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}),
@@ -153,8 +153,29 @@ export const GuueyChat = forwardRef(function GuueyChat(props, ref) {
153
153
  focusComposer: () => {
154
154
  inputRef.current?.focus();
155
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
+ },
156
162
  }), []);
157
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]);
158
179
  // `onReady` fires once per instance, on mount, with the stable handle
159
180
  // (guarded ref: StrictMode's remount cycle must not double-fire it).
160
181
  const readyFiredRef = useRef(false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guuey/chat",
3
- "version": "0.6.1",
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.6.1",
43
- "@guuey/mcp-apps-host": "0.6.1"
42
+ "@guuey/agent-client": "0.7.0",
43
+ "@guuey/mcp-apps-host": "0.7.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "react": ">=18",
@@ -48,7 +48,15 @@ witness.
48
48
  23 (`meta-less-locator` — guuey#209 route-A: a `_meta`-withholding
49
49
  producer's `ui://` locator rides `structuredContent` (AgJSON §2.1 model-
50
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).
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).
52
60
  - `capture.ts` / `captures/` — family 18: production-capture replay
53
61
  through the real `invokeTurn` (provenance in the module header).
54
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": "ggui",
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": "ggui",
1125
- "resource": {
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": "ggui",
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": "ggui",
1651
- "resource": {
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",
@@ -1936,3 +1918,56 @@ exports[`corpus > 23. meta-less-locator — a \`_meta\`-withholding producer's r
1936
1918
  "status": null,
1937
1919
  }
1938
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
+ `;
@@ -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",
@@ -584,3 +584,70 @@ export function metaLessLocator(): TranscriptInputs {
584
584
  ];
585
585
  return driveTurn(events, { userText: "render a slot picker" });
586
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 { snapshotViewMount, toolResultLocator, toolResultViewMount, type ViewMount } from "@guuey/mcp-apps-host";
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
- return item.channel === "ggui" ? s.viewBootFailure : s.viewInlineFallback;
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
 
@@ -97,6 +97,15 @@ export interface GuueyChatHandle {
97
97
  prefill(text: string, opts?: { focus?: boolean; append?: boolean }): void;
98
98
  /** Focus the composer input. */
99
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;
100
109
  }
101
110
 
102
111
  export interface GuueyChatProps {
@@ -176,6 +185,14 @@ export interface GuueyChatProps {
176
185
  * the same stable handle the ref receives.
177
186
  */
178
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;
179
196
  className?: string;
180
197
  style?: CSSProperties;
181
198
  }
@@ -211,6 +228,7 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
211
228
  onHitlAnswer,
212
229
  onErrorAction,
213
230
  onReady,
231
+ onThread,
214
232
  className,
215
233
  style,
216
234
  } = props;
@@ -316,12 +334,33 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
316
334
  focusComposer: (): void => {
317
335
  inputRef.current?.focus();
318
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
+ },
319
343
  }),
320
344
  [],
321
345
  );
322
346
 
323
347
  useImperativeHandle(ref, () => handle, [handle]);
324
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
+
325
364
  // `onReady` fires once per instance, on mount, with the stable handle
326
365
  // (guarded ref: StrictMode's remount cycle must not double-fire it).
327
366
  const readyFiredRef = useRef(false);