@guuey/chat 0.5.0 → 0.6.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.
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 +211 -13
  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 +62 -4
  26. package/dist/react/guuey-chat.d.ts.map +1 -1
  27. package/dist/react/guuey-chat.js +73 -8
  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 +13 -1
  44. package/src/corpus/__snapshots__/corpus.test.ts.snap +304 -0
  45. package/src/corpus/drive.ts +3 -3
  46. package/src/corpus/fixtures.ts +220 -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 +233 -20
  53. package/src/policy.ts +4 -0
  54. package/src/react/components.tsx +171 -8
  55. package/src/react/guuey-chat.tsx +143 -9
  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,20 +31,70 @@
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 { useCallback, useMemo, useState, type CSSProperties, type ReactNode } from "react";
47
+ import {
48
+ forwardRef,
49
+ useCallback,
50
+ useEffect,
51
+ useImperativeHandle,
52
+ useMemo,
53
+ useRef,
54
+ useState,
55
+ type CSSProperties,
56
+ type ReactNode,
57
+ } from "react";
36
58
  import { createWebAdapters, type AgentInvokeAdapters } from "@guuey/agent-client";
59
+ import type { AgHitlAnswer, AgPausedAsk } from "@silverprotocol/core";
37
60
  import { useAgentInvoke } from "@guuey/agent-client/react";
38
61
  import type { UiResourceReader } from "@guuey/mcp-apps-host";
39
62
  import { calmPolicy, debugPolicy, type TranscriptPolicy } from "../policy.js";
40
63
  import { defaultChatStrings, type ChatStrings } from "../strings.js";
41
64
  import { DEFAULT_CHAT_THEME, type GuueyChatTheme } from "../theme.js";
42
- import type { ErrorItem, PromptItem, UserMessageItem } from "../types.js";
65
+ import type { ChatDebugEvent, ErrorItem, PromptItem, UserMessageItem } from "../types.js";
43
66
  import type { ThemeMode } from "./theme-css.js";
44
67
  import { Transcript, type TranscriptWindowing } from "./transcript.js";
45
68
  import type { TranscriptComponents, TranscriptItemContext } from "./components.js";
46
69
  import { useTranscript, useTranscriptInputs } from "./use-transcript.js";
47
70
 
71
+ /**
72
+ * The imperative seam (guuey#210): programmatic send/prefill/focus for
73
+ * hosts that stay on the batteries-included path (suggested-prompt chips
74
+ * being the filing use case). Reach it via `ref` or `onReady` — both
75
+ * deliver the SAME stable object, valid for the component's whole life.
76
+ */
77
+ export interface GuueyChatHandle {
78
+ /**
79
+ * Send `text` through exactly the Send button's gate: returns `false`
80
+ * and does nothing when chat is unavailable (`endpointUrl === null`), a
81
+ * turn is in flight, or the text is blank — it NEVER bypasses the
82
+ * composer's rules. The typed draft is left untouched (a programmatic
83
+ * send must not eat a half-typed message). Failures surface the same
84
+ * way a composer send's do (the hook's `error` / R0 failed-send).
85
+ */
86
+ send(text: string): boolean;
87
+ /**
88
+ * Put `text` into the composer draft. `append: true` joins onto a
89
+ * non-empty draft with a single space (the widget's staged-composer
90
+ * semantic — never clobbers); default replaces. Focuses the input
91
+ * unless `focus: false`.
92
+ */
93
+ prefill(text: string, opts?: { focus?: boolean; append?: boolean }): void;
94
+ /** Focus the composer input. */
95
+ focusComposer(): void;
96
+ }
97
+
48
98
  export interface GuueyChatProps {
49
99
  /** Pod base URL (with or without `/agent/invoke`). `null` disables chat. */
50
100
  endpointUrl: string | null;
@@ -70,6 +120,8 @@ export interface GuueyChatProps {
70
120
  window?: TranscriptWindowing | false;
71
121
  /** R6 locator resolution (history cards). See `useTranscript`. */
72
122
  reader?: UiResourceReader;
123
+ /** The debug sink (spec §5) — fires only under the debug policy. */
124
+ onDebugEvent?: (event: ChatDebugEvent) => void;
73
125
  /** R6 pass-through (relay hook, sandbox page/flags, host context…). */
74
126
  viewProps?: TranscriptItemContext["viewProps"];
75
127
  /**
@@ -77,9 +129,24 @@ export interface GuueyChatProps {
77
129
  * The transcript record moves regardless; without a handler the prompt
78
130
  * card is record-only.
79
131
  */
80
- onPromptAction?: (item: PromptItem, action: "accept" | "decline" | "dismiss") => void;
132
+ onPromptAction?: (
133
+ item: PromptItem,
134
+ action: "accept" | "decline" | "dismiss" | { grantModeId: string },
135
+ ) => void;
136
+ /**
137
+ * Receives the VALIDATED wire answer for an AgJSON HITL ask (spec
138
+ * draft.2) — the host owns delivering it (the kit has no answer
139
+ * transport). Fired after the transcript record moves.
140
+ */
141
+ onHitlAnswer?: (answer: AgHitlAnswer, ask: AgPausedAsk) => void;
81
142
  /** R11 action slot (sign-in / retry affordances). */
82
143
  onErrorAction?: (item: ErrorItem) => void;
144
+ /**
145
+ * Callback route to the {@link GuueyChatHandle} for hosts that prefer
146
+ * wiring over refs. Fires ONCE per component instance, on mount, with
147
+ * the same stable handle the ref receives.
148
+ */
149
+ onReady?: (handle: GuueyChatHandle) => void;
83
150
  className?: string;
84
151
  style?: CSSProperties;
85
152
  }
@@ -90,7 +157,10 @@ const PROMPT_STATE = {
90
157
  dismiss: "dismissed",
91
158
  } as const;
92
159
 
93
- export function GuueyChat(props: GuueyChatProps): ReactNode {
160
+ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function GuueyChat(
161
+ props: GuueyChatProps,
162
+ ref,
163
+ ): ReactNode {
94
164
  const {
95
165
  endpointUrl,
96
166
  appId,
@@ -103,9 +173,12 @@ export function GuueyChat(props: GuueyChatProps): ReactNode {
103
173
  mode = "light",
104
174
  window: windowing,
105
175
  reader,
176
+ onDebugEvent,
106
177
  viewProps,
107
178
  onPromptAction,
179
+ onHitlAnswer,
108
180
  onErrorAction,
181
+ onReady,
109
182
  className,
110
183
  style,
111
184
  } = props;
@@ -123,19 +196,68 @@ export function GuueyChat(props: GuueyChatProps): ReactNode {
123
196
  return factory({ ...policyOverrides, strings });
124
197
  }, [preset, policyOverrides, stringOverrides]);
125
198
 
126
- const { inputs, resolvePrompt } = useTranscriptInputs(invoke);
199
+ const { inputs, resolvePrompt, answerHitlPrompt } = useTranscriptInputs(invoke);
127
200
  const { plan, toggle, resolvedMounts, onViewPhase } = useTranscript({
128
201
  inputs,
129
202
  policy,
130
203
  ...(reader !== undefined ? { reader } : {}),
204
+ ...(onDebugEvent !== undefined ? { onDebugEvent } : {}),
131
205
  });
132
206
 
133
207
  // ── Composer ─────────────────────────────────────────────────────────
134
208
  const [input, setInput] = useState("");
209
+ const inputRef = useRef<HTMLTextAreaElement | null>(null);
135
210
  const busy = invoke.status !== "ready";
136
211
  const available = endpointUrl !== null;
137
212
  const canSend = available && !busy && input.trim() !== "";
138
213
 
214
+ // ── The imperative seam (guuey#210) ──────────────────────────────────
215
+ // ONE stable handle for the component's whole life (hosts capture it in
216
+ // `onReady` and keep it), reading live truth through a ref so a call
217
+ // always sees the CURRENT gate — never a stale closure's.
218
+ const liveRef = useRef({ available, busy, invoke, onReady });
219
+ useEffect(() => {
220
+ liveRef.current = { available, busy, invoke, onReady };
221
+ });
222
+
223
+ const handle = useMemo<GuueyChatHandle>(
224
+ () => ({
225
+ send: (text: string): boolean => {
226
+ const live = liveRef.current;
227
+ const trimmed = text.trim();
228
+ // Exactly the Send button's gate — a handle send never bypasses it.
229
+ if (!live.available || live.busy || trimmed === "") return false;
230
+ void live.invoke.send(trimmed).catch(() => {
231
+ // Same contract as submit: the hook owns failure surfacing.
232
+ });
233
+ return true;
234
+ },
235
+ prefill: (text: string, opts?: { focus?: boolean; append?: boolean }): void => {
236
+ setInput((prev) =>
237
+ // The widget's staged-composer semantic: append joins with a
238
+ // space onto a non-empty draft, never clobbers it.
239
+ opts?.append === true && prev.trim() !== "" ? `${prev.trimEnd()} ${text}` : text,
240
+ );
241
+ if (opts?.focus !== false) inputRef.current?.focus();
242
+ },
243
+ focusComposer: (): void => {
244
+ inputRef.current?.focus();
245
+ },
246
+ }),
247
+ [],
248
+ );
249
+
250
+ useImperativeHandle(ref, () => handle, [handle]);
251
+
252
+ // `onReady` fires once per instance, on mount, with the stable handle
253
+ // (guarded ref: StrictMode's remount cycle must not double-fire it).
254
+ const readyFiredRef = useRef(false);
255
+ useEffect(() => {
256
+ if (readyFiredRef.current) return;
257
+ readyFiredRef.current = true;
258
+ liveRef.current.onReady?.(handle);
259
+ }, [handle]);
260
+
139
261
  const submit = useCallback((): void => {
140
262
  const text = input.trim();
141
263
  if (text === "" || !available || busy) return;
@@ -155,11 +277,22 @@ export function GuueyChat(props: GuueyChatProps): ReactNode {
155
277
  );
156
278
 
157
279
  const handlePromptAction = useCallback(
158
- (item: PromptItem, action: "accept" | "decline" | "dismiss") => {
159
- resolvePrompt(item.promptId, PROMPT_STATE[action]);
280
+ (item: PromptItem, action: "accept" | "decline" | "dismiss" | { grantModeId: string }) => {
281
+ if (item.promptKind === "hitl") {
282
+ const answer = answerHitlPrompt(
283
+ item.ask,
284
+ typeof action === "object" ? action : action === "accept" ? "accept" : action,
285
+ );
286
+ onHitlAnswer?.(answer, item.ask);
287
+ onPromptAction?.(item, action);
288
+ return;
289
+ }
290
+ // Profile prompts only ever receive the string actions (the default
291
+ // card renders no mode buttons for them).
292
+ if (typeof action !== "object") resolvePrompt(item.promptId, PROMPT_STATE[action]);
160
293
  onPromptAction?.(item, action);
161
294
  },
162
- [resolvePrompt, onPromptAction],
295
+ [resolvePrompt, answerHitlPrompt, onHitlAnswer, onPromptAction],
163
296
  );
164
297
 
165
298
  const strings = policy.strings;
@@ -192,6 +325,7 @@ export function GuueyChat(props: GuueyChatProps): ReactNode {
192
325
  }}
193
326
  >
194
327
  <textarea
328
+ ref={inputRef}
195
329
  className="guuey-chat-composer-input"
196
330
  rows={1}
197
331
  value={input}
@@ -224,4 +358,4 @@ export function GuueyChat(props: GuueyChatProps): ReactNode {
224
358
  </form>
225
359
  </div>
226
360
  );
227
- }
361
+ });
@@ -46,7 +46,7 @@ export interface TranscriptWindowing {
46
46
  export interface TranscriptProps
47
47
  extends Pick<
48
48
  TranscriptItemContext,
49
- "onToggle" | "onRetry" | "onPromptAction" | "onErrorAction" | "resolvedMounts" | "onViewPhase" | "viewProps"
49
+ "onToggle" | "onRetry" | "onPromptAction" | "onErrorAction" | "resolvedMounts" | "onViewPhase" | "onViewRef" | "viewProps"
50
50
  > {
51
51
  plan: TranscriptPlan;
52
52
  /** Per-slot component overrides (spec §3's override column). */
@@ -77,6 +77,7 @@ export function Transcript(props: TranscriptProps): ReactNode {
77
77
  onErrorAction,
78
78
  resolvedMounts,
79
79
  onViewPhase,
80
+ onViewRef,
80
81
  viewProps,
81
82
  } = props;
82
83
 
@@ -85,8 +86,8 @@ export function Transcript(props: TranscriptProps): ReactNode {
85
86
  [components],
86
87
  );
87
88
  const ctx: TranscriptItemContext = useMemo(
88
- () => ({ strings, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase, viewProps }),
89
- [strings, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase, viewProps],
89
+ () => ({ strings, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase, onViewRef, viewProps }),
90
+ [strings, onToggle, onRetry, onPromptAction, onErrorAction, resolvedMounts, onViewPhase, onViewRef, viewProps],
90
91
  );
91
92
 
92
93
  // ── Windowing ────────────────────────────────────────────────────────
@@ -13,17 +13,20 @@
13
13
  */
14
14
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
15
15
  import type { UseAgentInvokeReturn } from "@guuey/agent-client";
16
+ import type { AgHitlAnswer, AgPausedAsk } from "@silverprotocol/core";
16
17
  import {
17
18
  resolveViewMount,
18
19
  type ResolvedViewMount,
19
20
  type UiResourceReader,
20
21
  type ViewHostPhase,
21
22
  } from "@guuey/mcp-apps-host";
23
+ import { buildHitlAnswer, hitlPromptsFromFold, type HitlAnswerRecord, type HitlPromptAction } from "../hitl.js";
22
24
  import { planTranscript } from "../plan.js";
23
25
  import type { TranscriptPolicy } from "../policy.js";
24
26
  import type {
27
+ ChatDebugEvent,
25
28
  ItemKey,
26
- PromptItemInput,
29
+ ProfilePromptInput,
27
30
  TranscriptInputs,
28
31
  TranscriptMessage,
29
32
  TranscriptOverrides,
@@ -42,6 +45,13 @@ export interface UseTranscriptArgs {
42
45
  * resolution — labeled, never blank.
43
46
  */
44
47
  reader?: UiResourceReader;
48
+ /**
49
+ * The debug sink (spec §5, real API since the #135 refinement wave):
50
+ * receives {@link ChatDebugEvent}s — view-phase transitions, R15
51
+ * unknown-block sightings, the #192 recovered-turn marker. Fires ONLY
52
+ * under the debug policy (`debugDetail`); `calm` ignores it by design.
53
+ */
54
+ onDebugEvent?: (event: ChatDebugEvent) => void;
45
55
  }
46
56
 
47
57
  export interface UseTranscriptResult {
@@ -56,13 +66,22 @@ export interface UseTranscriptResult {
56
66
  resolvedMounts: ReadonlyMap<ItemKey, ResolvedViewMount | "expired">;
57
67
  }
58
68
 
59
- export function useTranscript({ inputs, policy, reader }: UseTranscriptArgs): UseTranscriptResult {
69
+ export function useTranscript({
70
+ inputs,
71
+ policy,
72
+ reader,
73
+ onDebugEvent,
74
+ }: UseTranscriptArgs): UseTranscriptResult {
60
75
  const [overrides, setOverrides] = useState<TranscriptOverrides>({});
61
76
  const [phases, setPhases] = useState<Readonly<Record<string, ViewHostPhase>>>({});
62
77
  const [resolvedMounts, setResolvedMounts] = useState<
63
78
  ReadonlyMap<ItemKey, ResolvedViewMount | "expired">
64
79
  >(new Map());
65
80
 
81
+ // The debug sink (gated on the debug policy — calm ignores it, spec §5).
82
+ const debugSink = useRef<((event: ChatDebugEvent) => void) | null>(null);
83
+ debugSink.current = policy.debugDetail && onDebugEvent !== undefined ? onDebugEvent : null;
84
+
66
85
  const merged = useMemo<TranscriptInputs>(
67
86
  () => ({ ...inputs, viewPhases: { ...inputs.viewPhases, ...phases } }),
68
87
  [inputs, phases],
@@ -92,10 +111,37 @@ export function useTranscript({ inputs, policy, reader }: UseTranscriptArgs): Us
92
111
  setOverrides((prev) => ({ ...prev, [key]: { expanded: !current } }));
93
112
  }, []);
94
113
 
114
+ // Mirror of `phases` for the change check OUTSIDE the state updater — a
115
+ // sink call inside an updater would double-fire under StrictMode.
116
+ const phasesRef = useRef<Readonly<Record<string, ViewHostPhase>>>({});
95
117
  const onViewPhase = useCallback((key: ItemKey, phase: ViewHostPhase) => {
118
+ if (phasesRef.current[key] !== phase) {
119
+ phasesRef.current = { ...phasesRef.current, [key]: phase };
120
+ debugSink.current?.({ type: "view-phase", key, phase });
121
+ }
96
122
  setPhases((prev) => (prev[key] === phase ? prev : { ...prev, [key]: phase }));
97
123
  }, []);
98
124
 
125
+ // Plan-derived debug events, emitted once per sighting (post-render — the
126
+ // plan itself stays pure data).
127
+ const emittedUnknowns = useRef(new Set<ItemKey>());
128
+ const recoveryEmitted = useRef(false);
129
+ useEffect(() => {
130
+ const sink = debugSink.current;
131
+ if (sink === null) return;
132
+ for (const item of plan.items) {
133
+ if (item.kind !== "unknown" || emittedUnknowns.current.has(item.key)) continue;
134
+ emittedUnknowns.current.add(item.key);
135
+ sink({ type: "unknown-block", key: item.key, typeName: item.typeName, byteSize: item.byteSize });
136
+ }
137
+ if (plan.recovery !== null && !recoveryEmitted.current) {
138
+ recoveryEmitted.current = true;
139
+ sink({ type: "turn-recovered", marker: plan.recovery });
140
+ } else if (plan.recovery === null) {
141
+ recoveryEmitted.current = false;
142
+ }
143
+ }, [plan]);
144
+
99
145
  // Locator resolution — one read per locator key, misses become "expired".
100
146
  const readerRef = useRef(reader);
101
147
  readerRef.current = reader;
@@ -110,6 +156,10 @@ export function useTranscript({ inputs, policy, reader }: UseTranscriptArgs): Us
110
156
  inFlight.current.add(item.key);
111
157
  const settle = (value: ResolvedViewMount | "expired"): void => {
112
158
  inFlight.current.delete(item.key);
159
+ // The R13 expired verdict is a phase transition too (debug sink).
160
+ if (value === "expired") {
161
+ debugSink.current?.({ type: "view-phase", key: item.key, phase: "expired" });
162
+ }
113
163
  setResolvedMounts((prev) => {
114
164
  const next = new Map(prev);
115
165
  next.set(item.key, value);
@@ -141,6 +191,16 @@ export interface UseTranscriptInputsResult {
141
191
  * without a recorded action reads as `dismissed`.
142
192
  */
143
193
  resolvePrompt: (id: string, state: "answered" | "declined" | "dismissed") => void;
194
+ /**
195
+ * Answer an AgJSON HITL ask (spec draft.2): constructs the wire answer,
196
+ * VALIDATES it against the ask's persisted record (`validateHitlAnswer`
197
+ * — required-iff-declared, echo-must-be-declared, requestState byte-echo)
198
+ * BEFORE anything dispatches, records it in the ledger, and returns it
199
+ * for the HOST to deliver — the kit renders and validates; the answer
200
+ * transport is the host's (no client→pod hitl-answer channel exists on
201
+ * the guuey wire today; see the #16 producer flag).
202
+ */
203
+ answerHitlPrompt: (ask: AgPausedAsk, action: HitlPromptAction) => AgHitlAnswer;
144
204
  }
145
205
 
146
206
  /** How often the escalation clock ticks while a status needs one. */
@@ -161,7 +221,7 @@ export function useTranscriptInputs(invoke: UseAgentInvokeReturn): UseTranscript
161
221
 
162
222
  // R10 ledger: the hook exposes only the LATEST pending ask; the
163
223
  // transcript keeps the record of every ask and its resolution.
164
- const [prompts, setPrompts] = useState<PromptItemInput[]>([]);
224
+ const [prompts, setPrompts] = useState<ProfilePromptInput[]>([]);
165
225
  const promptSeq = useRef(0);
166
226
  useEffect(() => {
167
227
  const request = invoke.profileConsentRequest;
@@ -228,6 +288,23 @@ export function useTranscriptInputs(invoke: UseAgentInvokeReturn): UseTranscript
228
288
  [invoke, prompts],
229
289
  );
230
290
 
291
+ // HITL ledger (spec draft.2): asks come from the FOLD's persisted
292
+ // records; this ledger holds only the host-side answers, keyed by askId.
293
+ // A `cancelled` record keeps the card re-askable (guuey's #16 ruling) —
294
+ // a later action on the same ask simply overwrites it.
295
+ const [hitlAnswers, setHitlAnswers] = useState<Readonly<Record<string, HitlAnswerRecord>>>({});
296
+ const answerHitlPrompt = useCallback((ask: AgPausedAsk, action: HitlPromptAction): AgHitlAnswer => {
297
+ const answer = buildHitlAnswer(ask, action);
298
+ setHitlAnswers((prev) => ({
299
+ ...prev,
300
+ [ask.askId]: {
301
+ status: answer.status,
302
+ ...(answer.grantModeId !== undefined ? { grantModeId: answer.grantModeId } : {}),
303
+ },
304
+ }));
305
+ return answer;
306
+ }, []);
307
+
231
308
  const inputs = useMemo<TranscriptInputs>(() => {
232
309
  // Source-ownership split (plan.ts's rules): the trailing assistant
233
310
  // entry is the IN-FLIGHT fold (or the abort-kept partial) — it moves to
@@ -248,7 +325,7 @@ export function useTranscriptInputs(invoke: UseAgentInvokeReturn): UseTranscript
248
325
  statusElapsedMs: elapsedMs,
249
326
  activeTool: invoke.activeTool,
250
327
  error: invoke.error !== null ? { message: invoke.error, code: invoke.errorCode } : null,
251
- prompts,
328
+ prompts: [...prompts, ...hitlPromptsFromFold(invoke.reduceResult, hitlAnswers)],
252
329
  messages,
253
330
  ...(invoke.historyCards.length > 0 ? { historyCards: invoke.historyCards } : {}),
254
331
  sendStates: invoke.sendStates,
@@ -268,7 +345,8 @@ export function useTranscriptInputs(invoke: UseAgentInvokeReturn): UseTranscript
268
345
  invoke.adopted,
269
346
  elapsedMs,
270
347
  prompts,
348
+ hitlAnswers,
271
349
  ]);
272
350
 
273
- return { inputs, resolvePrompt };
351
+ return { inputs, resolvePrompt, answerHitlPrompt };
274
352
  }
package/src/react.tsx CHANGED
@@ -27,6 +27,7 @@ export {
27
27
  DefaultToolGroup,
28
28
  DefaultDataResult,
29
29
  DefaultView,
30
+ DefaultViewRef,
30
31
  DefaultMedia,
31
32
  DefaultCode,
32
33
  DefaultCitations,
@@ -38,6 +39,7 @@ export {
38
39
  DefaultStatus,
39
40
  type TranscriptComponents,
40
41
  type TranscriptItemContext,
42
+ type ViewSlotProps,
41
43
  } from "./react/components.js";
42
44
  export {
43
45
  useTranscript,
@@ -46,6 +48,6 @@ export {
46
48
  type UseTranscriptResult,
47
49
  type UseTranscriptInputsResult,
48
50
  } from "./react/use-transcript.js";
49
- export { GuueyChat, type GuueyChatProps } from "./react/guuey-chat.js";
51
+ export { GuueyChat, type GuueyChatProps, type GuueyChatHandle } from "./react/guuey-chat.js";
50
52
  export { Markdown } from "./react/markdown.js";
51
53
  export { themeCssVars, type ThemeMode } from "./react/theme-css.js";
package/src/strings.ts CHANGED
@@ -55,6 +55,10 @@ export interface ChatStrings {
55
55
  viewInlineFallback: string;
56
56
  viewExpired: string;
57
57
  viewSandboxUnavailable: string;
58
+ /** guuey#204: the chip text for a mount promoted to a host stage/canvas. */
59
+ viewPromoted: (title: string) => string;
60
+ /** Chip title when the mount has no producing-call title (history cards). */
61
+ viewRefFallbackTitle: string;
58
62
 
59
63
  /** #192 debug-preset marker (calm never shows it — spec §3, F10). */
60
64
  recoveredFromHistory: string;
@@ -74,6 +78,17 @@ export interface ChatStrings {
74
78
  copy: string;
75
79
  copied: string;
76
80
 
81
+ /** R10 hitl actions (spec draft.2) — mode buttons use the ASKER's labels. */
82
+ promptAccept: string;
83
+ promptDecline: string;
84
+ promptDismissed: string;
85
+ /** The answered record line, e.g. `Allowed — Always`. */
86
+ promptAnsweredWith: (modeLabel: string) => string;
87
+ promptDeclinedRecord: string;
88
+
89
+ /** R16 — the notice row's label (provenance shows only under debug). */
90
+ noticeLabel: string;
91
+
77
92
  /** The 3c composer (`<GuueyChat>`). */
78
93
  composerPlaceholder: string;
79
94
  composerUnavailable: string;
@@ -121,6 +136,8 @@ export const defaultChatStrings: ChatStrings = {
121
136
  viewInlineFallback: "Showing plain content",
122
137
  viewExpired: "This view expired",
123
138
  viewSandboxUnavailable: "Interactive view unavailable",
139
+ viewPromoted: (title) => `${title} — on canvas`,
140
+ viewRefFallbackTitle: "Card",
124
141
 
125
142
  recoveredFromHistory: "recovered from history",
126
143
 
@@ -136,6 +153,14 @@ export const defaultChatStrings: ChatStrings = {
136
153
  copy: "Copy",
137
154
  copied: "Copied",
138
155
 
156
+ promptAccept: "Allow",
157
+ promptDecline: "Don't allow",
158
+ promptDismissed: "Dismissed",
159
+ promptAnsweredWith: (modeLabel) => `Allowed — ${modeLabel}`,
160
+ promptDeclinedRecord: "Not allowed",
161
+
162
+ noticeLabel: "Note",
163
+
139
164
  composerPlaceholder: "Message the agent…",
140
165
  composerUnavailable: "Chat is unavailable.",
141
166
  composerLabel: "Message",