@guuey/chat 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/hitl.d.ts +58 -0
  2. package/dist/hitl.d.ts.map +1 -0
  3. package/dist/hitl.js +81 -0
  4. package/dist/index.d.ts +3 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -1
  7. package/dist/native/components.d.ts +18 -2
  8. package/dist/native/components.d.ts.map +1 -1
  9. package/dist/native/components.js +49 -0
  10. package/dist/native/transcript.d.ts +1 -1
  11. package/dist/native/transcript.d.ts.map +1 -1
  12. package/dist/native/transcript.js +2 -2
  13. package/dist/native.d.ts +1 -1
  14. package/dist/native.d.ts.map +1 -1
  15. package/dist/native.js +1 -1
  16. package/dist/plan.d.ts +15 -0
  17. package/dist/plan.d.ts.map +1 -1
  18. package/dist/plan.js +213 -15
  19. package/dist/policy.d.ts +4 -0
  20. package/dist/policy.d.ts.map +1 -1
  21. package/dist/policy.js +2 -0
  22. package/dist/react/components.d.ts +42 -5
  23. package/dist/react/components.d.ts.map +1 -1
  24. package/dist/react/components.js +51 -1
  25. package/dist/react/guuey-chat.d.ts +92 -7
  26. package/dist/react/guuey-chat.d.ts.map +1 -1
  27. package/dist/react/guuey-chat.js +114 -11
  28. package/dist/react/transcript.d.ts +1 -1
  29. package/dist/react/transcript.d.ts.map +1 -1
  30. package/dist/react/transcript.js +2 -2
  31. package/dist/react/use-transcript.d.ts +21 -2
  32. package/dist/react/use-transcript.d.ts.map +1 -1
  33. package/dist/react/use-transcript.js +57 -3
  34. package/dist/react.d.ts +2 -2
  35. package/dist/react.d.ts.map +1 -1
  36. package/dist/react.js +1 -1
  37. package/dist/strings.d.ts +13 -0
  38. package/dist/strings.d.ts.map +1 -1
  39. package/dist/strings.js +8 -0
  40. package/dist/types.d.ts +146 -7
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +5 -5
  43. package/src/corpus/README.md +17 -1
  44. package/src/corpus/__snapshots__/corpus.test.ts.snap +348 -0
  45. package/src/corpus/drive.ts +3 -3
  46. package/src/corpus/fixtures.ts +249 -1
  47. package/src/hitl.ts +103 -0
  48. package/src/index.ts +15 -0
  49. package/src/native/components.tsx +136 -1
  50. package/src/native/transcript.tsx +4 -3
  51. package/src/native.tsx +1 -0
  52. package/src/plan.ts +235 -22
  53. package/src/policy.ts +4 -0
  54. package/src/react/components.tsx +171 -8
  55. package/src/react/guuey-chat.tsx +222 -15
  56. package/src/react/transcript.tsx +4 -3
  57. package/src/react/use-transcript.ts +83 -5
  58. package/src/react.tsx +3 -1
  59. package/src/strings.ts +25 -0
  60. package/src/types.ts +152 -6
  61. package/styles.css +21 -0
@@ -31,26 +31,91 @@
31
31
  * adapter (e.g. `createWebAdapters({ apiBaseUrl, … })`) and a persisted
32
32
  * threadId rehydrates on mount — text transcript + persisted cards, which
33
33
  * mount through the same R6 path as live views. Nothing here to configure.
34
+ *
35
+ * ## The imperative seam (guuey#210)
36
+ *
37
+ * Suggested-prompt chips and other host-driven sends stay on the
38
+ * batteries-included path via {@link GuueyChatHandle} — a ref handle
39
+ * (`forwardRef`) and/or the `onReady` callback, ONE stable object for the
40
+ * component's whole life. `send` runs through exactly the Send button's
41
+ * gate (never bypasses it; the typed draft is left untouched — a chip send
42
+ * must not eat a half-typed message); `prefill` mirrors the widget's
43
+ * staged-composer semantics (append joins with a space, never clobbers).
44
+ * Web-only for now: the native tier ships `<NativeTranscript>` without a
45
+ * native GuueyChat, so there is no native surface to put a handle on yet.
34
46
  */
35
- import { type CSSProperties, type ReactNode } from "react";
47
+ import { type CSSProperties } from "react";
36
48
  import { type AgentInvokeAdapters } from "@guuey/agent-client";
49
+ import type { AgHitlAnswer, AgPausedAsk } from "@silverprotocol/core";
37
50
  import type { UiResourceReader } from "@guuey/mcp-apps-host";
38
51
  import { type TranscriptPolicy } from "../policy.js";
39
52
  import { type ChatStrings } from "../strings.js";
40
53
  import { type GuueyChatTheme } from "../theme.js";
41
- import type { ErrorItem, PromptItem } from "../types.js";
54
+ import type { ChatDebugEvent, ErrorItem, PromptItem } from "../types.js";
42
55
  import type { ThemeMode } from "./theme-css.js";
43
56
  import { type TranscriptWindowing } from "./transcript.js";
44
57
  import type { TranscriptComponents, TranscriptItemContext } from "./components.js";
58
+ /**
59
+ * The imperative seam (guuey#210): programmatic send/prefill/focus for
60
+ * hosts that stay on the batteries-included path (suggested-prompt chips
61
+ * being the filing use case). Reach it via `ref` or `onReady` — both
62
+ * deliver the SAME stable object, valid for the component's whole life.
63
+ */
64
+ export interface GuueyChatHandle {
65
+ /**
66
+ * Send `text` through exactly the Send button's gate: returns `false`
67
+ * and does nothing when chat is unavailable (`endpointUrl === null`), a
68
+ * turn is in flight, or the text is blank — it NEVER bypasses the
69
+ * composer's rules. The typed draft is left untouched (a programmatic
70
+ * send must not eat a half-typed message). Failures surface the same
71
+ * way a composer send's do (the hook's `error` / R0 failed-send).
72
+ */
73
+ send(text: string): boolean;
74
+ /**
75
+ * Put `text` into the composer draft. `append: true` joins onto a
76
+ * non-empty draft with a single space (the widget's staged-composer
77
+ * semantic — never clobbers); default replaces. Focuses the input
78
+ * unless `focus: false`.
79
+ */
80
+ prefill(text: string, opts?: {
81
+ focus?: boolean;
82
+ append?: boolean;
83
+ }): void;
84
+ /** Focus the composer input. */
85
+ focusComposer(): void;
86
+ }
45
87
  export interface GuueyChatProps {
46
88
  /** Pod base URL (with or without `/agent/invoke`). `null` disables chat. */
47
89
  endpointUrl: string | null;
48
90
  /** Owning app id — namespaces the persisted threadId. */
49
91
  appId?: string;
92
+ /**
93
+ * The guuey public API base (`…/v1`). Enables the batteries-included
94
+ * read paths without hand-wiring: when set and no `adapters` are given,
95
+ * the default `createWebAdapters` gains transcript history; when set and
96
+ * no `reader` is given, a `UiResourceReader` is built over the same
97
+ * identity so generative-UI locators resolve (guuey#221 — a guest kit
98
+ * user has no bearer to construct one with). Explicit `adapters` /
99
+ * `reader` always win. Absent → today's behavior (no history, no
100
+ * default reader; locators render as expired, labeled).
101
+ */
102
+ apiBaseUrl?: string;
103
+ /**
104
+ * Identity for the default adapters + default reader — the same two
105
+ * resolvers `createWebAdapters` takes (bearer wins; guest secret next;
106
+ * neither → cookie identity). Ignored when explicit `adapters` and
107
+ * `reader` are both supplied.
108
+ */
109
+ getAccessToken?: (opts?: {
110
+ forceRefresh?: boolean;
111
+ }) => Promise<string | null>;
112
+ getGuestSecret?: () => string | null;
50
113
  /**
51
114
  * Host couplings (storage / id / transport / history). Default:
52
- * `createWebAdapters()` localStorage thread persistence + the web SSE
53
- * transport (cookie/guest identity, saturation + cold-start retries).
115
+ * `createWebAdapters({ apiBaseUrl, getAccessToken, getGuestSecret })`
116
+ * localStorage thread persistence + the web SSE transport (cookie/guest
117
+ * identity, saturation + cold-start retries), plus history when
118
+ * `apiBaseUrl` is set.
54
119
  */
55
120
  adapters?: AgentInvokeAdapters;
56
121
  /** Policy preset (spec §5). Default `"calm"`. */
@@ -65,8 +130,14 @@ export interface GuueyChatProps {
65
130
  mode?: ThemeMode;
66
131
  /** DOM windowing (§3.2). `false` renders everything. */
67
132
  window?: TranscriptWindowing | false;
68
- /** R6 locator resolution (history cards). See `useTranscript`. */
133
+ /**
134
+ * R6 locator resolution (history cards + meta-less live renders). See
135
+ * `useTranscript`. Defaults from `apiBaseUrl` (+ `endpointUrl` for the
136
+ * pod door) when omitted; an explicit reader always wins.
137
+ */
69
138
  reader?: UiResourceReader;
139
+ /** The debug sink (spec §5) — fires only under the debug policy. */
140
+ onDebugEvent?: (event: ChatDebugEvent) => void;
70
141
  /** R6 pass-through (relay hook, sandbox page/flags, host context…). */
71
142
  viewProps?: TranscriptItemContext["viewProps"];
72
143
  /**
@@ -74,11 +145,25 @@ export interface GuueyChatProps {
74
145
  * The transcript record moves regardless; without a handler the prompt
75
146
  * card is record-only.
76
147
  */
77
- onPromptAction?: (item: PromptItem, action: "accept" | "decline" | "dismiss") => void;
148
+ onPromptAction?: (item: PromptItem, action: "accept" | "decline" | "dismiss" | {
149
+ grantModeId: string;
150
+ }) => void;
151
+ /**
152
+ * Receives the VALIDATED wire answer for an AgJSON HITL ask (spec
153
+ * draft.2) — the host owns delivering it (the kit has no answer
154
+ * transport). Fired after the transcript record moves.
155
+ */
156
+ onHitlAnswer?: (answer: AgHitlAnswer, ask: AgPausedAsk) => void;
78
157
  /** R11 action slot (sign-in / retry affordances). */
79
158
  onErrorAction?: (item: ErrorItem) => void;
159
+ /**
160
+ * Callback route to the {@link GuueyChatHandle} for hosts that prefer
161
+ * wiring over refs. Fires ONCE per component instance, on mount, with
162
+ * the same stable handle the ref receives.
163
+ */
164
+ onReady?: (handle: GuueyChatHandle) => void;
80
165
  className?: string;
81
166
  style?: CSSProperties;
82
167
  }
83
- export declare function GuueyChat(props: GuueyChatProps): ReactNode;
168
+ export declare const GuueyChat: import("react").ForwardRefExoticComponent<GuueyChatProps & import("react").RefAttributes<GuueyChatHandle>>;
84
169
  //# sourceMappingURL=guuey-chat.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"guuey-chat.d.ts","sourceRoot":"","sources":["../../src/react/guuey-chat.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAAkC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC3F,OAAO,EAAqB,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAElF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAmB,MAAM,aAAa,CAAC;AAC1E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAc,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAGnF,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnC,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC3C,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,wDAAwD;IACxD,MAAM,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAAC;IACrC,kEAAkE;IAClE,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,uEAAuE;IACvE,SAAS,CAAC,EAAE,qBAAqB,CAAC,WAAW,CAAC,CAAC;IAC/C;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,KAAK,IAAI,CAAC;IACtF,qDAAqD;IACrD,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAQD,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CAsI1D"}
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,EAGL,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAmB,MAAM,aAAa,CAAC;AAC1F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAc,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAGnF;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1E,gCAAgC;IAChC,aAAa,IAAI,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E,cAAc,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnC,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC3C,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,wDAAwD;IACxD,MAAM,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAAC;IACrC;;;;OAIG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,oEAAoE;IACpE,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC/C,uEAAuE;IACvE,SAAS,CAAC,EAAE,qBAAqB,CAAC,WAAW,CAAC,CAAC;IAC/C;;;;OAIG;IACH,cAAc,CAAC,EAAE,CACf,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,KAC/D,IAAI,CAAC;IACV;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,WAAW,KAAK,IAAI,CAAC;IAChE,qDAAqD;IACrD,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IAC1C;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAQD,eAAO,MAAM,SAAS,4GAqPpB,CAAC"}
@@ -32,9 +32,21 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
32
32
  * adapter (e.g. `createWebAdapters({ apiBaseUrl, … })`) and a persisted
33
33
  * threadId rehydrates on mount — text transcript + persisted cards, which
34
34
  * mount through the same R6 path as live views. Nothing here to configure.
35
+ *
36
+ * ## The imperative seam (guuey#210)
37
+ *
38
+ * Suggested-prompt chips and other host-driven sends stay on the
39
+ * batteries-included path via {@link GuueyChatHandle} — a ref handle
40
+ * (`forwardRef`) and/or the `onReady` callback, ONE stable object for the
41
+ * component's whole life. `send` runs through exactly the Send button's
42
+ * gate (never bypasses it; the typed draft is left untouched — a chip send
43
+ * must not eat a half-typed message); `prefill` mirrors the widget's
44
+ * staged-composer semantics (append joins with a space, never clobbers).
45
+ * Web-only for now: the native tier ships `<NativeTranscript>` without a
46
+ * native GuueyChat, so there is no native surface to put a handle on yet.
35
47
  */
36
- import { useCallback, useMemo, useState } from "react";
37
- import { createWebAdapters } from "@guuey/agent-client";
48
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
49
+ import { createUiResourceReader, createWebAdapters, } from "@guuey/agent-client";
38
50
  import { useAgentInvoke } from "@guuey/agent-client/react";
39
51
  import { calmPolicy, debugPolicy } from "../policy.js";
40
52
  import { defaultChatStrings } from "../strings.js";
@@ -46,10 +58,48 @@ const PROMPT_STATE = {
46
58
  decline: "declined",
47
59
  dismiss: "dismissed",
48
60
  };
49
- export function GuueyChat(props) {
50
- const { endpointUrl, appId, adapters: adaptersProp, preset = "calm", policy: policyOverrides, components, strings: stringOverrides, theme = DEFAULT_CHAT_THEME, mode = "light", window: windowing, reader, viewProps, onPromptAction, onErrorAction, className, style, } = props;
51
- const adapters = useMemo(() => adaptersProp ?? createWebAdapters(), [adaptersProp]);
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;
63
+ const adapters = useMemo(() => adaptersProp ??
64
+ createWebAdapters({
65
+ ...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}),
66
+ ...(getAccessToken !== undefined ? { getAccessToken } : {}),
67
+ ...(getGuestSecret !== undefined ? { getGuestSecret } : {}),
68
+ }), [adaptersProp, apiBaseUrl, getAccessToken, getGuestSecret]);
52
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;
53
103
  const policy = useMemo(() => {
54
104
  const factory = preset === "debug" ? debugPolicy : calmPolicy;
55
105
  const strings = {
@@ -59,17 +109,61 @@ export function GuueyChat(props) {
59
109
  };
60
110
  return factory({ ...policyOverrides, strings });
61
111
  }, [preset, policyOverrides, stringOverrides]);
62
- const { inputs, resolvePrompt } = useTranscriptInputs(invoke);
112
+ const { inputs, resolvePrompt, answerHitlPrompt } = useTranscriptInputs(invoke);
63
113
  const { plan, toggle, resolvedMounts, onViewPhase } = useTranscript({
64
114
  inputs,
65
115
  policy,
66
- ...(reader !== undefined ? { reader } : {}),
116
+ ...(effectiveReader !== undefined ? { reader: effectiveReader } : {}),
117
+ ...(onDebugEvent !== undefined ? { onDebugEvent } : {}),
67
118
  });
68
119
  // ── Composer ─────────────────────────────────────────────────────────
69
120
  const [input, setInput] = useState("");
121
+ const inputRef = useRef(null);
70
122
  const busy = invoke.status !== "ready";
71
123
  const available = endpointUrl !== null;
72
124
  const canSend = available && !busy && input.trim() !== "";
125
+ // ── The imperative seam (guuey#210) ──────────────────────────────────
126
+ // ONE stable handle for the component's whole life (hosts capture it in
127
+ // `onReady` and keep it), reading live truth through a ref so a call
128
+ // always sees the CURRENT gate — never a stale closure's.
129
+ const liveRef = useRef({ available, busy, invoke, onReady });
130
+ useEffect(() => {
131
+ liveRef.current = { available, busy, invoke, onReady };
132
+ });
133
+ const handle = useMemo(() => ({
134
+ send: (text) => {
135
+ const live = liveRef.current;
136
+ const trimmed = text.trim();
137
+ // Exactly the Send button's gate — a handle send never bypasses it.
138
+ if (!live.available || live.busy || trimmed === "")
139
+ return false;
140
+ void live.invoke.send(trimmed).catch(() => {
141
+ // Same contract as submit: the hook owns failure surfacing.
142
+ });
143
+ return true;
144
+ },
145
+ prefill: (text, opts) => {
146
+ setInput((prev) =>
147
+ // The widget's staged-composer semantic: append joins with a
148
+ // space onto a non-empty draft, never clobbers it.
149
+ opts?.append === true && prev.trim() !== "" ? `${prev.trimEnd()} ${text}` : text);
150
+ if (opts?.focus !== false)
151
+ inputRef.current?.focus();
152
+ },
153
+ focusComposer: () => {
154
+ inputRef.current?.focus();
155
+ },
156
+ }), []);
157
+ useImperativeHandle(ref, () => handle, [handle]);
158
+ // `onReady` fires once per instance, on mount, with the stable handle
159
+ // (guarded ref: StrictMode's remount cycle must not double-fire it).
160
+ const readyFiredRef = useRef(false);
161
+ useEffect(() => {
162
+ if (readyFiredRef.current)
163
+ return;
164
+ readyFiredRef.current = true;
165
+ liveRef.current.onReady?.(handle);
166
+ }, [handle]);
73
167
  const submit = useCallback(() => {
74
168
  const text = input.trim();
75
169
  if (text === "" || !available || busy)
@@ -85,14 +179,23 @@ export function GuueyChat(props) {
85
179
  });
86
180
  }, [invoke]);
87
181
  const handlePromptAction = useCallback((item, action) => {
88
- resolvePrompt(item.promptId, PROMPT_STATE[action]);
182
+ if (item.promptKind === "hitl") {
183
+ const answer = answerHitlPrompt(item.ask, typeof action === "object" ? action : action === "accept" ? "accept" : action);
184
+ onHitlAnswer?.(answer, item.ask);
185
+ onPromptAction?.(item, action);
186
+ return;
187
+ }
188
+ // Profile prompts only ever receive the string actions (the default
189
+ // card renders no mode buttons for them).
190
+ if (typeof action !== "object")
191
+ resolvePrompt(item.promptId, PROMPT_STATE[action]);
89
192
  onPromptAction?.(item, action);
90
- }, [resolvePrompt, onPromptAction]);
193
+ }, [resolvePrompt, answerHitlPrompt, onHitlAnswer, onPromptAction]);
91
194
  const strings = policy.strings;
92
195
  return (_jsxs("div", { className: `guuey-chat-surface${className !== undefined ? ` ${className}` : ""}`, style: style, children: [_jsx(Transcript, { plan: plan, strings: strings, theme: theme, mode: mode, ...(windowing !== undefined ? { window: windowing } : {}), ...(components !== undefined ? { components } : {}), onToggle: toggle, onRetry: handleRetry, onPromptAction: handlePromptAction, ...(onErrorAction !== undefined ? { onErrorAction } : {}), resolvedMounts: resolvedMounts, onViewPhase: onViewPhase, ...(viewProps !== undefined ? { viewProps } : {}) }), _jsxs("form", { className: "guuey-chat-composer", onSubmit: (e) => {
93
196
  e.preventDefault();
94
197
  submit();
95
- }, children: [_jsx("textarea", { className: "guuey-chat-composer-input", rows: 1, value: input, onChange: (e) => setInput(e.target.value), onKeyDown: (e) => {
198
+ }, children: [_jsx("textarea", { ref: inputRef, className: "guuey-chat-composer-input", rows: 1, value: input, onChange: (e) => setInput(e.target.value), onKeyDown: (e) => {
96
199
  // `isComposing` guards IME input: an Enter that commits a
97
200
  // candidate must not also send the message.
98
201
  if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
@@ -100,4 +203,4 @@ export function GuueyChat(props) {
100
203
  submit();
101
204
  }
102
205
  }, disabled: !available, "aria-label": strings.composerLabel, placeholder: available ? strings.composerPlaceholder : strings.composerUnavailable }), busy ? (_jsx("button", { type: "button", className: "guuey-chat-composer-stop", onClick: () => invoke.abort(), children: strings.stop })) : (_jsx("button", { type: "submit", className: "guuey-chat-composer-send", disabled: !canSend, children: strings.send }))] })] }));
103
- }
206
+ });
@@ -25,7 +25,7 @@ export interface TranscriptWindowing {
25
25
  /** Trailing items rendered; earlier ones sit behind the expander. */
26
26
  tail: number;
27
27
  }
28
- export interface TranscriptProps extends Pick<TranscriptItemContext, "onToggle" | "onRetry" | "onPromptAction" | "onErrorAction" | "resolvedMounts" | "onViewPhase" | "viewProps"> {
28
+ export interface TranscriptProps extends Pick<TranscriptItemContext, "onToggle" | "onRetry" | "onPromptAction" | "onErrorAction" | "resolvedMounts" | "onViewPhase" | "onViewRef" | "viewProps"> {
29
29
  plan: TranscriptPlan;
30
30
  /** Per-slot component overrides (spec §3's override column). */
31
31
  components?: Partial<TranscriptComponents>;
@@ -1 +1 @@
1
- {"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../src/react/transcript.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAK9D,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eACf,SAAQ,IAAI,CACV,qBAAqB,EACrB,UAAU,GAAG,SAAS,GAAG,gBAAgB,GAAG,eAAe,GAAG,gBAAgB,GAAG,aAAa,GAAG,WAAW,CAC7G;IACD,IAAI,EAAE,cAAc,CAAC;IACrB,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC3C,gEAAgE;IAChE,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,yEAAyE;IACzE,MAAM,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,SAAS,CA2H5D"}
1
+ {"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../src/react/transcript.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAK9D,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eACf,SAAQ,IAAI,CACV,qBAAqB,EACrB,UAAU,GAAG,SAAS,GAAG,gBAAgB,GAAG,eAAe,GAAG,gBAAgB,GAAG,aAAa,GAAG,WAAW,GAAG,WAAW,CAC3H;IACD,IAAI,EAAE,cAAc,CAAC;IACrB,gEAAgE;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC3C,gEAAgE;IAChE,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,yEAAyE;IACzE,MAAM,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,SAAS,CA4H5D"}
@@ -24,9 +24,9 @@ import { themeCssVars } from "./theme-css.js";
24
24
  /** How close to the bottom (px) still counts as pinned. */
25
25
  const PIN_THRESHOLD_PX = 48;
26
26
  export function Transcript(props) {
27
- const { plan, components, strings = defaultChatStrings, theme = DEFAULT_CHAT_THEME, mode = "light", window: windowing = { tail: 80 }, className, style, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase, viewProps, } = props;
27
+ const { plan, components, strings = defaultChatStrings, theme = DEFAULT_CHAT_THEME, mode = "light", window: windowing = { tail: 80 }, className, style, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase, onViewRef, viewProps, } = props;
28
28
  const resolvedComponents = useMemo(() => ({ ...defaultTranscriptComponents, ...components }), [components]);
29
- const ctx = useMemo(() => ({ strings, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase, viewProps }), [strings, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase, viewProps]);
29
+ const ctx = useMemo(() => ({ strings, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase, onViewRef, viewProps }), [strings, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase, onViewRef, viewProps]);
30
30
  // ── Windowing ────────────────────────────────────────────────────────
31
31
  const [extraShown, setExtraShown] = useState(0);
32
32
  const tail = windowing === false ? Number.POSITIVE_INFINITY : windowing.tail;
@@ -1,7 +1,9 @@
1
1
  import type { UseAgentInvokeReturn } from "@guuey/agent-client";
2
+ import type { AgHitlAnswer, AgPausedAsk } from "@silverprotocol/core";
2
3
  import { type ResolvedViewMount, type UiResourceReader, type ViewHostPhase } from "@guuey/mcp-apps-host";
4
+ import { type HitlPromptAction } from "../hitl.js";
3
5
  import type { TranscriptPolicy } from "../policy.js";
4
- import type { ItemKey, TranscriptInputs, TranscriptOverrides, TranscriptPlan } from "../types.js";
6
+ import type { ChatDebugEvent, ItemKey, TranscriptInputs, TranscriptOverrides, TranscriptPlan } from "../types.js";
5
7
  export interface UseTranscriptArgs {
6
8
  inputs: TranscriptInputs;
7
9
  policy: TranscriptPolicy;
@@ -12,6 +14,13 @@ export interface UseTranscriptArgs {
12
14
  * resolution — labeled, never blank.
13
15
  */
14
16
  reader?: UiResourceReader;
17
+ /**
18
+ * The debug sink (spec §5, real API since the #135 refinement wave):
19
+ * receives {@link ChatDebugEvent}s — view-phase transitions, R15
20
+ * unknown-block sightings, the #192 recovered-turn marker. Fires ONLY
21
+ * under the debug policy (`debugDetail`); `calm` ignores it by design.
22
+ */
23
+ onDebugEvent?: (event: ChatDebugEvent) => void;
15
24
  }
16
25
  export interface UseTranscriptResult {
17
26
  plan: TranscriptPlan;
@@ -24,7 +33,7 @@ export interface UseTranscriptResult {
24
33
  /** Locator resolutions: mount material, or `"expired"` for a miss. */
25
34
  resolvedMounts: ReadonlyMap<ItemKey, ResolvedViewMount | "expired">;
26
35
  }
27
- export declare function useTranscript({ inputs, policy, reader }: UseTranscriptArgs): UseTranscriptResult;
36
+ export declare function useTranscript({ inputs, policy, reader, onDebugEvent, }: UseTranscriptArgs): UseTranscriptResult;
28
37
  export interface UseTranscriptInputsResult {
29
38
  inputs: TranscriptInputs;
30
39
  /**
@@ -34,6 +43,16 @@ export interface UseTranscriptInputsResult {
34
43
  * without a recorded action reads as `dismissed`.
35
44
  */
36
45
  resolvePrompt: (id: string, state: "answered" | "declined" | "dismissed") => void;
46
+ /**
47
+ * Answer an AgJSON HITL ask (spec draft.2): constructs the wire answer,
48
+ * VALIDATES it against the ask's persisted record (`validateHitlAnswer`
49
+ * — required-iff-declared, echo-must-be-declared, requestState byte-echo)
50
+ * BEFORE anything dispatches, records it in the ledger, and returns it
51
+ * for the HOST to deliver — the kit renders and validates; the answer
52
+ * transport is the host's (no client→pod hitl-answer channel exists on
53
+ * the guuey wire today; see the #16 producer flag).
54
+ */
55
+ answerHitlPrompt: (ask: AgPausedAsk, action: HitlPromptAction) => AgHitlAnswer;
37
56
  }
38
57
  export declare function useTranscriptInputs(invoke: UseAgentInvokeReturn): UseTranscriptInputsResult;
39
58
  //# sourceMappingURL=use-transcript.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-transcript.d.ts","sourceRoot":"","sources":["../../src/react/use-transcript.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EACV,OAAO,EAEP,gBAAgB,EAEhB,mBAAmB,EACnB,cAAc,EACf,MAAM,aAAa,CAAC;AAIrB,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,gBAAgB,CAAC;IACzB,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,cAAc,CAAC;IACrB,wEAAwE;IACxE,MAAM,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/B,mEAAmE;IACnE,SAAS,EAAE,mBAAmB,CAAC;IAC/B,0EAA0E;IAC1E,WAAW,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1D,sEAAsE;IACtE,cAAc,EAAE,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,CAAC,CAAC;CACrE;AAED,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,iBAAiB,GAAG,mBAAmB,CAwEhG;AAID,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;;;OAKG;IACH,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,UAAU,GAAG,WAAW,KAAK,IAAI,CAAC;CACnF;AAKD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,GAAG,yBAAyB,CA6H3F"}
1
+ {"version":3,"file":"use-transcript.d.ts","sourceRoot":"","sources":["../../src/react/use-transcript.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAA+D,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEhH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EACV,cAAc,EACd,OAAO,EAEP,gBAAgB,EAEhB,mBAAmB,EACnB,cAAc,EACf,MAAM,aAAa,CAAC;AAIrB,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,gBAAgB,CAAC;IACzB,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAChD;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,cAAc,CAAC;IACrB,wEAAwE;IACxE,MAAM,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/B,mEAAmE;IACnE,SAAS,EAAE,mBAAmB,CAAC;IAC/B,0EAA0E;IAC1E,WAAW,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1D,sEAAsE;IACtE,cAAc,EAAE,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,CAAC,CAAC;CACrE;AAED,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,MAAM,EACN,MAAM,EACN,YAAY,GACb,EAAE,iBAAiB,GAAG,mBAAmB,CA2GzC;AAID,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;;;OAKG;IACH,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,UAAU,GAAG,WAAW,KAAK,IAAI,CAAC;IAClF;;;;;;;;OAQG;IACH,gBAAgB,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,gBAAgB,KAAK,YAAY,CAAC;CAChF;AAKD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,GAAG,yBAAyB,CA+I3F"}
@@ -13,11 +13,15 @@
13
13
  */
14
14
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
15
15
  import { resolveViewMount, } from "@guuey/mcp-apps-host";
16
+ import { buildHitlAnswer, hitlPromptsFromFold } from "../hitl.js";
16
17
  import { planTranscript } from "../plan.js";
17
- export function useTranscript({ inputs, policy, reader }) {
18
+ export function useTranscript({ inputs, policy, reader, onDebugEvent, }) {
18
19
  const [overrides, setOverrides] = useState({});
19
20
  const [phases, setPhases] = useState({});
20
21
  const [resolvedMounts, setResolvedMounts] = useState(new Map());
22
+ // The debug sink (gated on the debug policy — calm ignores it, spec §5).
23
+ const debugSink = useRef(null);
24
+ debugSink.current = policy.debugDetail && onDebugEvent !== undefined ? onDebugEvent : null;
21
25
  const merged = useMemo(() => ({ ...inputs, viewPhases: { ...inputs.viewPhases, ...phases } }), [inputs, phases]);
22
26
  const plan = useMemo(() => planTranscript(merged, policy, overrides), [merged, policy, overrides]);
23
27
  // Toggle flips the item's CURRENT resolved state (policy default or a
@@ -46,9 +50,38 @@ export function useTranscript({ inputs, policy, reader }) {
46
50
  const current = findExpanded();
47
51
  setOverrides((prev) => ({ ...prev, [key]: { expanded: !current } }));
48
52
  }, []);
53
+ // Mirror of `phases` for the change check OUTSIDE the state updater — a
54
+ // sink call inside an updater would double-fire under StrictMode.
55
+ const phasesRef = useRef({});
49
56
  const onViewPhase = useCallback((key, phase) => {
57
+ if (phasesRef.current[key] !== phase) {
58
+ phasesRef.current = { ...phasesRef.current, [key]: phase };
59
+ debugSink.current?.({ type: "view-phase", key, phase });
60
+ }
50
61
  setPhases((prev) => (prev[key] === phase ? prev : { ...prev, [key]: phase }));
51
62
  }, []);
63
+ // Plan-derived debug events, emitted once per sighting (post-render — the
64
+ // plan itself stays pure data).
65
+ const emittedUnknowns = useRef(new Set());
66
+ const recoveryEmitted = useRef(false);
67
+ useEffect(() => {
68
+ const sink = debugSink.current;
69
+ if (sink === null)
70
+ return;
71
+ for (const item of plan.items) {
72
+ if (item.kind !== "unknown" || emittedUnknowns.current.has(item.key))
73
+ continue;
74
+ emittedUnknowns.current.add(item.key);
75
+ sink({ type: "unknown-block", key: item.key, typeName: item.typeName, byteSize: item.byteSize });
76
+ }
77
+ if (plan.recovery !== null && !recoveryEmitted.current) {
78
+ recoveryEmitted.current = true;
79
+ sink({ type: "turn-recovered", marker: plan.recovery });
80
+ }
81
+ else if (plan.recovery === null) {
82
+ recoveryEmitted.current = false;
83
+ }
84
+ }, [plan]);
52
85
  // Locator resolution — one read per locator key, misses become "expired".
53
86
  const readerRef = useRef(reader);
54
87
  readerRef.current = reader;
@@ -66,6 +99,10 @@ export function useTranscript({ inputs, policy, reader }) {
66
99
  inFlight.current.add(item.key);
67
100
  const settle = (value) => {
68
101
  inFlight.current.delete(item.key);
102
+ // The R13 expired verdict is a phase transition too (debug sink).
103
+ if (value === "expired") {
104
+ debugSink.current?.({ type: "view-phase", key: item.key, phase: "expired" });
105
+ }
69
106
  setResolvedMounts((prev) => {
70
107
  const next = new Map(prev);
71
108
  next.set(item.key, value);
@@ -156,6 +193,22 @@ export function useTranscriptInputs(invoke) {
156
193
  if (pending?.kind === "link")
157
194
  invoke.clearProfileLinkRequest();
158
195
  }, [invoke, prompts]);
196
+ // HITL ledger (spec draft.2): asks come from the FOLD's persisted
197
+ // records; this ledger holds only the host-side answers, keyed by askId.
198
+ // A `cancelled` record keeps the card re-askable (guuey's #16 ruling) —
199
+ // a later action on the same ask simply overwrites it.
200
+ const [hitlAnswers, setHitlAnswers] = useState({});
201
+ const answerHitlPrompt = useCallback((ask, action) => {
202
+ const answer = buildHitlAnswer(ask, action);
203
+ setHitlAnswers((prev) => ({
204
+ ...prev,
205
+ [ask.askId]: {
206
+ status: answer.status,
207
+ ...(answer.grantModeId !== undefined ? { grantModeId: answer.grantModeId } : {}),
208
+ },
209
+ }));
210
+ return answer;
211
+ }, []);
159
212
  const inputs = useMemo(() => {
160
213
  // Source-ownership split (plan.ts's rules): the trailing assistant
161
214
  // entry is the IN-FLIGHT fold (or the abort-kept partial) — it moves to
@@ -176,7 +229,7 @@ export function useTranscriptInputs(invoke) {
176
229
  statusElapsedMs: elapsedMs,
177
230
  activeTool: invoke.activeTool,
178
231
  error: invoke.error !== null ? { message: invoke.error, code: invoke.errorCode } : null,
179
- prompts,
232
+ prompts: [...prompts, ...hitlPromptsFromFold(invoke.reduceResult, hitlAnswers)],
180
233
  messages,
181
234
  ...(invoke.historyCards.length > 0 ? { historyCards: invoke.historyCards } : {}),
182
235
  sendStates: invoke.sendStates,
@@ -196,6 +249,7 @@ export function useTranscriptInputs(invoke) {
196
249
  invoke.adopted,
197
250
  elapsedMs,
198
251
  prompts,
252
+ hitlAnswers,
199
253
  ]);
200
- return { inputs, resolvePrompt };
254
+ return { inputs, resolvePrompt, answerHitlPrompt };
201
255
  }
package/dist/react.d.ts CHANGED
@@ -13,9 +13,9 @@
13
13
  * → drop to `planTranscript` + `attachViewHost` and render it all yourself.
14
14
  */
15
15
  export { Transcript, type TranscriptProps, type TranscriptWindowing, } from "./react/transcript.js";
16
- export { defaultTranscriptComponents, renderItem, DefaultUserMessage, DefaultText, DefaultReasoning, DefaultTool, DefaultToolGroup, DefaultDataResult, DefaultView, DefaultMedia, DefaultCode, DefaultCitations, DefaultPrompt, DefaultError, DefaultHistoryBoundary, DefaultCompaction, DefaultUnknown, DefaultStatus, type TranscriptComponents, type TranscriptItemContext, } from "./react/components.js";
16
+ export { defaultTranscriptComponents, renderItem, DefaultUserMessage, DefaultText, DefaultReasoning, DefaultTool, DefaultToolGroup, DefaultDataResult, DefaultView, DefaultViewRef, DefaultMedia, DefaultCode, DefaultCitations, DefaultPrompt, DefaultError, DefaultHistoryBoundary, DefaultCompaction, DefaultUnknown, DefaultStatus, type TranscriptComponents, type TranscriptItemContext, type ViewSlotProps, } from "./react/components.js";
17
17
  export { useTranscript, useTranscriptInputs, type UseTranscriptArgs, type UseTranscriptResult, type UseTranscriptInputsResult, } from "./react/use-transcript.js";
18
- export { GuueyChat, type GuueyChatProps } from "./react/guuey-chat.js";
18
+ export { GuueyChat, type GuueyChatProps, type GuueyChatHandle } from "./react/guuey-chat.js";
19
19
  export { Markdown } from "./react/markdown.js";
20
20
  export { themeCssVars, type ThemeMode } from "./react/theme-css.js";
21
21
  //# sourceMappingURL=react.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EACL,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,mBAAmB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,2BAA2B,EAC3B,UAAU,EACV,kBAAkB,EAClB,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,GAC/B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EACL,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,mBAAmB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,2BAA2B,EAC3B,UAAU,EACV,kBAAkB,EAClB,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,GACnB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,GAC/B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7F,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/react.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * → drop to `planTranscript` + `attachViewHost` and render it all yourself.
14
14
  */
15
15
  export { Transcript, } from "./react/transcript.js";
16
- export { defaultTranscriptComponents, renderItem, DefaultUserMessage, DefaultText, DefaultReasoning, DefaultTool, DefaultToolGroup, DefaultDataResult, DefaultView, DefaultMedia, DefaultCode, DefaultCitations, DefaultPrompt, DefaultError, DefaultHistoryBoundary, DefaultCompaction, DefaultUnknown, DefaultStatus, } from "./react/components.js";
16
+ export { defaultTranscriptComponents, renderItem, DefaultUserMessage, DefaultText, DefaultReasoning, DefaultTool, DefaultToolGroup, DefaultDataResult, DefaultView, DefaultViewRef, DefaultMedia, DefaultCode, DefaultCitations, DefaultPrompt, DefaultError, DefaultHistoryBoundary, DefaultCompaction, DefaultUnknown, DefaultStatus, } from "./react/components.js";
17
17
  export { useTranscript, useTranscriptInputs, } from "./react/use-transcript.js";
18
18
  export { GuueyChat } from "./react/guuey-chat.js";
19
19
  export { Markdown } from "./react/markdown.js";
package/dist/strings.d.ts CHANGED
@@ -47,6 +47,10 @@ export interface ChatStrings {
47
47
  viewInlineFallback: string;
48
48
  viewExpired: string;
49
49
  viewSandboxUnavailable: string;
50
+ /** guuey#204: the chip text for a mount promoted to a host stage/canvas. */
51
+ viewPromoted: (title: string) => string;
52
+ /** Chip title when the mount has no producing-call title (history cards). */
53
+ viewRefFallbackTitle: string;
50
54
  /** #192 debug-preset marker (calm never shows it — spec §3, F10). */
51
55
  recoveredFromHistory: string;
52
56
  /** R5 empty result. */
@@ -61,6 +65,15 @@ export interface ChatStrings {
61
65
  showEarlier: (count: number) => string;
62
66
  copy: string;
63
67
  copied: string;
68
+ /** R10 hitl actions (spec draft.2) — mode buttons use the ASKER's labels. */
69
+ promptAccept: string;
70
+ promptDecline: string;
71
+ promptDismissed: string;
72
+ /** The answered record line, e.g. `Allowed — Always`. */
73
+ promptAnsweredWith: (modeLabel: string) => string;
74
+ promptDeclinedRecord: string;
75
+ /** R16 — the notice row's label (provenance shows only under debug). */
76
+ noticeLabel: string;
64
77
  /** The 3c composer (`<GuueyChat>`). */
65
78
  composerPlaceholder: string;
66
79
  composerUnavailable: string;
@@ -1 +1 @@
1
- {"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../src/strings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACzC,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAEhB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IAErB,aAAa;IACb,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,eAAe,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAEvC,UAAU;IACV,cAAc,EAAE,MAAM,CAAC;IAEvB,UAAU;IACV,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAErC,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAC;IAErB,WAAW;IACX,UAAU,EAAE,MAAM,CAAC;IAEnB,UAAU;IACV,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAElB,iBAAiB;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,sBAAsB,EAAE,MAAM,CAAC;IAE/B,qEAAqE;IACrE,oBAAoB,EAAE,MAAM,CAAC;IAE7B,uBAAuB;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAErC,kBAAkB;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IAEnB,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IAEf,uCAAuC;IACvC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,eAAO,MAAM,kBAAkB,EAAE,WAsDhC,CAAC"}
1
+ {"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../src/strings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACzC,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAEhB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IAErB,aAAa;IACb,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,eAAe,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAEvC,UAAU;IACV,cAAc,EAAE,MAAM,CAAC;IAEvB,UAAU;IACV,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAErC,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAC;IAErB,WAAW;IACX,UAAU,EAAE,MAAM,CAAC;IAEnB,UAAU;IACV,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAElB,iBAAiB;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,4EAA4E;IAC5E,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,6EAA6E;IAC7E,oBAAoB,EAAE,MAAM,CAAC;IAE7B,qEAAqE;IACrE,oBAAoB,EAAE,MAAM,CAAC;IAE7B,uBAAuB;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAErC,kBAAkB;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IAEnB,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IAEf,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,kBAAkB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,oBAAoB,EAAE,MAAM,CAAC;IAE7B,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IAEpB,uCAAuC;IACvC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,eAAO,MAAM,kBAAkB,EAAE,WAgEhC,CAAC"}
package/dist/strings.js CHANGED
@@ -28,6 +28,8 @@ export const defaultChatStrings = {
28
28
  viewInlineFallback: "Showing plain content",
29
29
  viewExpired: "This view expired",
30
30
  viewSandboxUnavailable: "Interactive view unavailable",
31
+ viewPromoted: (title) => `${title} — on canvas`,
32
+ viewRefFallbackTitle: "Card",
31
33
  recoveredFromHistory: "recovered from history",
32
34
  noOutput: "no output",
33
35
  bytes: (byteCount) => byteCount >= 1024 ? `${Math.round(byteCount / 1024)} KB` : `${byteCount} B`,
@@ -37,6 +39,12 @@ export const defaultChatStrings = {
37
39
  showEarlier: (count) => `Show ${count} earlier`,
38
40
  copy: "Copy",
39
41
  copied: "Copied",
42
+ promptAccept: "Allow",
43
+ promptDecline: "Don't allow",
44
+ promptDismissed: "Dismissed",
45
+ promptAnsweredWith: (modeLabel) => `Allowed — ${modeLabel}`,
46
+ promptDeclinedRecord: "Not allowed",
47
+ noticeLabel: "Note",
40
48
  composerPlaceholder: "Message the agent…",
41
49
  composerUnavailable: "Chat is unavailable.",
42
50
  composerLabel: "Message",