@guuey/chat 0.7.1 → 0.8.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.
@@ -18,6 +18,7 @@ import {
18
18
  resolveViewMount,
19
19
  type ResolvedViewMount,
20
20
  type UiResourceReader,
21
+ type ViewCspDiagnosis,
21
22
  type ViewHostPhase,
22
23
  } from "@guuey/mcp-apps-host";
23
24
  import { buildHitlAnswer, hitlPromptsFromFold, type HitlAnswerRecord, type HitlPromptAction } from "../hitl.js";
@@ -62,6 +63,12 @@ export interface UseTranscriptResult {
62
63
  overrides: TranscriptOverrides;
63
64
  /** Wire to `<GuueyView onPhaseChange>` (the default kit already does). */
64
65
  onViewPhase: (key: ItemKey, phase: ViewHostPhase) => void;
66
+ /**
67
+ * Wire to `<GuueyView onCspDiagnosis>` (the default kit already does):
68
+ * the host's CSP tripwire caught the embedding page blocking this view
69
+ * (guuey#235). Feeds `viewDiagnoses` so the R6 label names the cause.
70
+ */
71
+ onViewDiagnosis: (key: ItemKey, diagnosis: ViewCspDiagnosis) => void;
65
72
  /** Locator resolutions: mount material, or `"expired"` for a miss. */
66
73
  resolvedMounts: ReadonlyMap<ItemKey, ResolvedViewMount | "expired">;
67
74
  }
@@ -74,6 +81,7 @@ export function useTranscript({
74
81
  }: UseTranscriptArgs): UseTranscriptResult {
75
82
  const [overrides, setOverrides] = useState<TranscriptOverrides>({});
76
83
  const [phases, setPhases] = useState<Readonly<Record<string, ViewHostPhase>>>({});
84
+ const [diagnoses, setDiagnoses] = useState<Readonly<Record<string, ViewCspDiagnosis>>>({});
77
85
  const [resolvedMounts, setResolvedMounts] = useState<
78
86
  ReadonlyMap<ItemKey, ResolvedViewMount | "expired">
79
87
  >(new Map());
@@ -83,8 +91,12 @@ export function useTranscript({
83
91
  debugSink.current = policy.debugDetail && onDebugEvent !== undefined ? onDebugEvent : null;
84
92
 
85
93
  const merged = useMemo<TranscriptInputs>(
86
- () => ({ ...inputs, viewPhases: { ...inputs.viewPhases, ...phases } }),
87
- [inputs, phases],
94
+ () => ({
95
+ ...inputs,
96
+ viewPhases: { ...inputs.viewPhases, ...phases },
97
+ viewDiagnoses: { ...inputs.viewDiagnoses, ...diagnoses },
98
+ }),
99
+ [inputs, phases, diagnoses],
88
100
  );
89
101
  const plan = useMemo(() => planTranscript(merged, policy, overrides), [merged, policy, overrides]);
90
102
 
@@ -114,13 +126,32 @@ export function useTranscript({
114
126
  // Mirror of `phases` for the change check OUTSIDE the state updater — a
115
127
  // sink call inside an updater would double-fire under StrictMode.
116
128
  const phasesRef = useRef<Readonly<Record<string, ViewHostPhase>>>({});
129
+ const diagnosesRef = useRef<Readonly<Record<string, ViewCspDiagnosis>>>({});
117
130
  const onViewPhase = useCallback((key: ItemKey, phase: ViewHostPhase) => {
118
131
  if (phasesRef.current[key] !== phase) {
119
132
  phasesRef.current = { ...phasesRef.current, [key]: phase };
120
- debugSink.current?.({ type: "view-phase", key, phase });
133
+ const diagnosis = diagnosesRef.current[key];
134
+ debugSink.current?.({
135
+ type: "view-phase",
136
+ key,
137
+ phase,
138
+ ...(diagnosis !== undefined ? { diagnosis } : {}),
139
+ });
121
140
  }
122
141
  setPhases((prev) => (prev[key] === phase ? prev : { ...prev, [key]: phase }));
123
142
  }, []);
143
+ // The tripwire fires BEFORE the negotiation window lapses (a blocked
144
+ // runtime never negotiates), so the diagnosis is usually known by the
145
+ // time `no-handshake` arrives — the phase event above carries it. If it
146
+ // lands after, re-emit the current phase with the verdict so a debug
147
+ // sink still sees the pairing.
148
+ const onViewDiagnosis = useCallback((key: ItemKey, diagnosis: ViewCspDiagnosis) => {
149
+ if (diagnosesRef.current[key] === diagnosis) return;
150
+ diagnosesRef.current = { ...diagnosesRef.current, [key]: diagnosis };
151
+ const phase = phasesRef.current[key];
152
+ if (phase !== undefined) debugSink.current?.({ type: "view-phase", key, phase, diagnosis });
153
+ setDiagnoses((prev) => (prev[key] === diagnosis ? prev : { ...prev, [key]: diagnosis }));
154
+ }, []);
124
155
 
125
156
  // Plan-derived debug events, emitted once per sighting (post-render — the
126
157
  // plan itself stays pure data).
@@ -177,7 +208,7 @@ export function useTranscript({
177
208
  }
178
209
  }, [plan, resolvedMounts]);
179
210
 
180
- return { plan, toggle, overrides, onViewPhase, resolvedMounts };
211
+ return { plan, toggle, overrides, onViewPhase, onViewDiagnosis, resolvedMounts };
181
212
  }
182
213
 
183
214
  // ─── useTranscriptInputs (the live assembler) ──────────────────────────────
@@ -196,9 +227,9 @@ export interface UseTranscriptInputsResult {
196
227
  * VALIDATES it against the ask's persisted record (`validateHitlAnswer`
197
228
  * — required-iff-declared, echo-must-be-declared, requestState byte-echo)
198
229
  * 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).
230
+ * for the HOST to deliver — the kit renders and validates; the transport
231
+ * is the host's (`@guuey/agent-client`'s `createHitlAnswerRelay` posts it
232
+ * to `<pod>/agent/hitl-answer`, guuey#207).
202
233
  */
203
234
  answerHitlPrompt: (ask: AgPausedAsk, action: HitlPromptAction) => AgHitlAnswer;
204
235
  }
@@ -219,36 +250,12 @@ export function useTranscriptInputs(invoke: UseAgentInvokeReturn): UseTranscript
219
250
  return () => clearInterval(timer);
220
251
  }, [invoke.status]);
221
252
 
222
- // R10 ledger: the hook exposes only the LATEST pending ask; the
223
- // transcript keeps the record of every ask and its resolution.
253
+ // R10 ledger (the LINK invite): the hook exposes only the LATEST pending
254
+ // invite; the transcript keeps the record of every ask and its resolution.
255
+ // Consent is NOT ledgered here — it is an AgJSON paused turn in the fold
256
+ // (`hitlPromptsFromFold` below, guuey#207).
224
257
  const [prompts, setPrompts] = useState<ProfilePromptInput[]>([]);
225
258
  const promptSeq = useRef(0);
226
- useEffect(() => {
227
- const request = invoke.profileConsentRequest;
228
- if (request === null) {
229
- setPrompts((prev) =>
230
- prev.some((p) => p.kind === "consent" && p.state === "pending")
231
- ? prev.map((p) =>
232
- p.kind === "consent" && p.state === "pending" ? { ...p, state: "dismissed" } : p,
233
- )
234
- : prev,
235
- );
236
- return;
237
- }
238
- setPrompts((prev) => {
239
- if (prev.some((p) => p.kind === "consent" && p.state === "pending")) return prev;
240
- return [
241
- ...prev,
242
- {
243
- id: `consent.${promptSeq.current++}`,
244
- kind: "consent",
245
- appId: request.appId,
246
- requested: request.requested,
247
- state: "pending",
248
- },
249
- ];
250
- });
251
- }, [invoke.profileConsentRequest]);
252
259
  useEffect(() => {
253
260
  const request = invoke.profileLinkRequest;
254
261
  if (request === null) {
@@ -282,7 +289,6 @@ export function useTranscriptInputs(invoke: UseAgentInvokeReturn): UseTranscript
282
289
  // Clearing the hook's pending request AFTER the ledger moved keeps the
283
290
  // dismissal effect above from double-transitioning it.
284
291
  const pending = prompts.find((p) => p.id === id);
285
- if (pending?.kind === "consent") invoke.clearProfileConsentRequest();
286
292
  if (pending?.kind === "link") invoke.clearProfileLinkRequest();
287
293
  },
288
294
  [invoke, prompts],
package/src/strings.ts CHANGED
@@ -55,6 +55,17 @@ export interface ChatStrings {
55
55
  viewInlineFallback: string;
56
56
  viewExpired: string;
57
57
  viewSandboxUnavailable: string;
58
+ /**
59
+ * R6 no-handshake with a CSP diagnosis (guuey#235): the embedding page's
60
+ * own policy blocked the view. Receives the structured verdict so a
61
+ * locale can shape the sentence; the en default names the blocked URI
62
+ * and the exact allowance to add.
63
+ */
64
+ viewCspBlocked: (diagnosis: {
65
+ blockedUri: string;
66
+ violatedDirective: string;
67
+ suggestedEntry: string;
68
+ }) => string;
58
69
  /** guuey#204: the chip text for a mount promoted to a host stage/canvas. */
59
70
  viewPromoted: (title: string) => string;
60
71
  /** Chip title when the mount has no producing-call title (history cards). */
@@ -136,6 +147,8 @@ export const defaultChatStrings: ChatStrings = {
136
147
  viewInlineFallback: "Showing plain content",
137
148
  viewExpired: "This view expired",
138
149
  viewSandboxUnavailable: "Interactive view unavailable",
150
+ viewCspBlocked: (d) =>
151
+ `This page's Content-Security-Policy blocks ${d.blockedUri} — add "${d.violatedDirective} ${d.suggestedEntry}" to the policy so the view can start`,
139
152
  viewPromoted: (title) => `${title} — on canvas`,
140
153
  viewRefFallbackTitle: "Card",
141
154
 
package/src/types.ts CHANGED
@@ -20,7 +20,12 @@ import type {
20
20
  JsonValue,
21
21
  } from "@silverprotocol/core";
22
22
  import type { AgentInvokeStatus, HistoryCard } from "@guuey/agent-client";
23
- import type { ViewHostPhase, ViewMount, ViewMountChannel } from "@guuey/mcp-apps-host";
23
+ import type {
24
+ ViewCspDiagnosis,
25
+ ViewHostPhase,
26
+ ViewMount,
27
+ ViewMountChannel,
28
+ } from "@guuey/mcp-apps-host";
24
29
 
25
30
  /**
26
31
  * One settled entry of the flat conversation transcript — structurally
@@ -42,14 +47,17 @@ export interface TranscriptMessage {
42
47
  }
43
48
 
44
49
  /**
45
- * A pending/resolved consent or link ask (R10) — the turn-level
46
- * `profile-consent` / `profile-link` events lifted into renderable state.
47
- * The assembler (3b) accumulates these from `invokeTurn` events; `id` is
48
- * assembler-chosen and stable for the ask's lifetime.
50
+ * A pending/resolved account-LINK invite (R10) — the turn-level
51
+ * `profile-link` event lifted into renderable state. The assembler (3b)
52
+ * accumulates these from `invokeTurn` events; `id` is assembler-chosen and
53
+ * stable for the ask's lifetime. (Consent is NOT a profile arm any more:
54
+ * since guuey#207 the pod asks over AgJSON `hitl.ask` + `turn.done
55
+ * outcome:"paused"` with declared `grantModes`, and it renders through
56
+ * {@link HitlPromptInput}.)
49
57
  */
50
58
  export interface ProfilePromptInput {
51
59
  id: string;
52
- kind: "consent" | "link";
60
+ kind: "link";
53
61
  appId: string;
54
62
  requested: "read" | "read-write";
55
63
  state: "pending" | "answered" | "declined" | "dismissed";
@@ -78,9 +86,9 @@ export interface HitlPromptInput {
78
86
 
79
87
  /**
80
88
  * R10 prompt inputs — a discriminated union (narrow on `kind`). The
81
- * profile arm is the original guuey-wire shape unchanged; `hitl` arrived
82
- * with spec draft.2. (Direct un-narrowed reads of profile-only fields are
83
- * the one shape this union retired — narrow first.)
89
+ * profile arm is the guuey-wire LINK invite; `hitl` (spec draft.2) carries
90
+ * every AgJSON ask, consent included. (Direct un-narrowed reads of
91
+ * profile-only fields are the one shape this union retired — narrow first.)
84
92
  */
85
93
  export type PromptItemInput = ProfilePromptInput | HitlPromptInput;
86
94
 
@@ -112,7 +120,7 @@ export interface TranscriptInputs {
112
120
  statusElapsedMs: number;
113
121
  activeTool: string | null;
114
122
  error: { message: string; code: string | null } | null;
115
- /** Pending/answered consent + link asks (R10). */
123
+ /** Pending/answered asks (R10): the link invite + AgJSON hitl asks (consent). */
116
124
  prompts: PromptItemInput[];
117
125
  /**
118
126
  * The settled conversation, both roles, in order. Assistant entries are
@@ -128,6 +136,14 @@ export interface TranscriptInputs {
128
136
  sendStates?: Readonly<Record<string, "sending" | "failed">>;
129
137
  /** mountKey → live phase (R6 states). Keys match `ViewMountItem.key`. */
130
138
  viewPhases?: Readonly<Record<string, ViewHostPhase>>;
139
+ /**
140
+ * mountKey → the host's CSP tripwire verdict (guuey#235): the embedding
141
+ * page's own policy blocked the view. Renderer-supplied like
142
+ * `viewPhases` (`<GuueyView onCspDiagnosis>` → `useTranscript`); a
143
+ * `no-handshake` mount with a diagnosis labels the actionable cause
144
+ * instead of the channel heuristic.
145
+ */
146
+ viewDiagnoses?: Readonly<Record<string, ViewCspDiagnosis>>;
131
147
  /**
132
148
  * The mount key a host-owned stage/canvas surface currently shows
133
149
  * (guuey#204 "promote and reference") — renderer-supplied state, like
@@ -247,6 +263,12 @@ export interface ViewMountItem extends BaseItem {
247
263
  phase: ViewHostPhase | "expired";
248
264
  /** The channel-aware state label the renderer shows for non-connected phases. */
249
265
  label: string | null;
266
+ /**
267
+ * The CSP tripwire's verdict when the embedding page blocked this view
268
+ * (guuey#235) — WHY a `no-handshake`; `label` already carries its
269
+ * message, this is the structured form for renderers/debug sinks.
270
+ */
271
+ diagnosis: ViewCspDiagnosis | null;
250
272
  /** Calm chrome: "via {tool}" when this mount broke an R4 group. */
251
273
  attribution: string | null;
252
274
  /** The producing call's humanized title (live mounts); null for history cards. */
@@ -301,12 +323,12 @@ export interface CitationsItem extends BaseItem {
301
323
  style: "chips" | "list";
302
324
  }
303
325
 
304
- /** R10 — the guuey-wire consent/link prompt card (the original arm). */
326
+ /** R10 — the guuey-wire account-LINK prompt card (the original arm). */
305
327
  export interface ProfilePromptItem extends BaseItem {
306
328
  kind: "prompt";
307
329
  /** The `PromptItemInput.id` this row records — the host's resolution key. */
308
330
  promptId: string;
309
- promptKind: "consent" | "link";
331
+ promptKind: "link";
310
332
  appId: string;
311
333
  requested: "read" | "read-write";
312
334
  state: "pending" | "answered" | "declined" | "dismissed";
@@ -448,7 +470,13 @@ export interface StatusLineItem {
448
470
  * ignores it by design (spec §5's per-preset row).
449
471
  */
450
472
  export type ChatDebugEvent =
451
- | { type: "view-phase"; key: ItemKey; phase: ViewHostPhase | "expired" }
473
+ | {
474
+ type: "view-phase";
475
+ key: ItemKey;
476
+ phase: ViewHostPhase | "expired";
477
+ /** Present when the host's CSP tripwire diagnosed the failure (guuey#235). */
478
+ diagnosis?: ViewCspDiagnosis;
479
+ }
452
480
  | { type: "unknown-block"; key: ItemKey; typeName: string; byteSize: number }
453
481
  | { type: "turn-recovered"; marker: string };
454
482