@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.
- package/README.md +13 -0
- package/dist/hitl.d.ts +5 -0
- package/dist/hitl.d.ts.map +1 -1
- package/dist/hitl.js +5 -0
- package/dist/native/components.d.ts.map +1 -1
- package/dist/native/components.js +1 -3
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +9 -1
- package/dist/react/components.d.ts +8 -2
- package/dist/react/components.d.ts.map +1 -1
- package/dist/react/components.js +2 -4
- package/dist/react/guuey-chat.d.ts.map +1 -1
- package/dist/react/guuey-chat.js +2 -2
- package/dist/react/transcript.d.ts +1 -1
- package/dist/react/transcript.d.ts.map +1 -1
- package/dist/react/transcript.js +13 -2
- package/dist/react/use-transcript.d.ts +10 -4
- package/dist/react/use-transcript.d.ts.map +1 -1
- package/dist/react/use-transcript.js +33 -30
- package/dist/strings.d.ts +11 -0
- package/dist/strings.d.ts.map +1 -1
- package/dist/strings.js +1 -0
- package/dist/types.d.ts +31 -12
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/corpus/__snapshots__/corpus.test.ts.snap +52 -7
- package/src/corpus/drive.ts +43 -12
- package/src/corpus/fixtures.ts +18 -4
- package/src/hitl.ts +5 -0
- package/src/native/components.tsx +1 -3
- package/src/plan.ts +9 -2
- package/src/react/components.tsx +10 -6
- package/src/react/guuey-chat.tsx +2 -1
- package/src/react/transcript.tsx +23 -3
- package/src/react/use-transcript.ts +42 -36
- package/src/strings.ts +13 -0
- package/src/types.ts +41 -13
|
@@ -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
|
-
() => ({
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
200
|
-
*
|
|
201
|
-
*
|
|
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
|
|
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 {
|
|
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
|
|
46
|
-
* `profile-
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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: "
|
|
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
|
|
82
|
-
*
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
| {
|
|
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
|
|