@guuey/chat 0.7.2 → 0.8.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.
- package/dist/hitl.d.ts +5 -0
- package/dist/hitl.d.ts.map +1 -1
- package/dist/hitl.js +5 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/native/components.d.ts.map +1 -1
- package/dist/native/components.js +5 -5
- package/dist/oauth.d.ts +87 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +121 -0
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +4 -1
- package/dist/react/components.d.ts.map +1 -1
- package/dist/react/components.js +7 -5
- package/dist/react/guuey-chat.d.ts +14 -0
- package/dist/react/guuey-chat.d.ts.map +1 -1
- package/dist/react/guuey-chat.js +21 -3
- package/dist/react/oauth-return.d.ts +61 -0
- package/dist/react/oauth-return.d.ts.map +1 -0
- package/dist/react/oauth-return.js +103 -0
- package/dist/react/use-transcript.d.ts +3 -3
- package/dist/react/use-transcript.d.ts.map +1 -1
- package/dist/react/use-transcript.js +4 -27
- package/dist/react.d.ts +1 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +1 -0
- package/dist/strings.d.ts +10 -0
- package/dist/strings.d.ts.map +1 -1
- package/dist/strings.js +4 -0
- package/dist/types.d.ts +25 -11
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/corpus/README.md +7 -1
- package/src/corpus/__snapshots__/corpus.test.ts.snap +126 -7
- package/src/corpus/drive.ts +43 -12
- package/src/corpus/fixtures.ts +95 -4
- package/src/hitl.ts +5 -0
- package/src/index.ts +11 -0
- package/src/native/components.tsx +15 -7
- package/src/oauth.ts +157 -0
- package/src/plan.ts +4 -1
- package/src/react/components.tsx +17 -9
- package/src/react/guuey-chat.tsx +49 -1
- package/src/react/oauth-return.ts +129 -0
- package/src/react/use-transcript.ts +7 -32
- package/src/react.tsx +9 -0
- package/src/strings.ts +14 -0
- package/src/types.ts +22 -11
- package/styles.css +26 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web half of the OAuth "authorize this server" arm (guuey#178 Slice 4)
|
|
3
|
+
* — the DOM-touching pieces the pure `../oauth.js` helpers deliberately
|
|
4
|
+
* leave out. Every web surface (the `<GuueyChat>` composite, the widget, the
|
|
5
|
+
* studio agent page) shares these three so the redirect + return behave the
|
|
6
|
+
* same everywhere:
|
|
7
|
+
*
|
|
8
|
+
* - {@link oauthReturnToHere} — the surface's own location, with a stale
|
|
9
|
+
* `?connected=`/`?error=` from a PREVIOUS dance stripped, ready to be
|
|
10
|
+
* the next `returnTo`.
|
|
11
|
+
* - {@link openOAuthAuthorize} — how the link is opened: a top-level page
|
|
12
|
+
* navigates in place (the broker 302s straight back to `returnTo`); a
|
|
13
|
+
* FRAMED page (the widget inside a customer origin) opens a new tab,
|
|
14
|
+
* because identity providers refuse to render inside a third-party
|
|
15
|
+
* frame (`X-Frame-Options` / `frame-ancestors`).
|
|
16
|
+
* - {@link useOAuthReturn} — on mount, read the broker's return params off
|
|
17
|
+
* the address bar, REPLACE the URL without them (a reload never re-shows
|
|
18
|
+
* the notice), and hand the surface a one-shot notice to render.
|
|
19
|
+
*/
|
|
20
|
+
import { useCallback, useEffect, useState } from "react";
|
|
21
|
+
import type { AgHitlAnswer, AgPausedAsk } from "@silverprotocol/core";
|
|
22
|
+
import type { HitlPromptAction } from "../hitl.js";
|
|
23
|
+
import { oauthAuthorizeHref, parseOAuthReturn, stripOAuthReturn, type OAuthReturn } from "../oauth.js";
|
|
24
|
+
import type { HitlPromptItem } from "../types.js";
|
|
25
|
+
|
|
26
|
+
/** The surface's own location as the next `returnTo` (stale return params stripped). */
|
|
27
|
+
export function oauthReturnToHere(): string {
|
|
28
|
+
return stripOAuthReturn(window.location.href);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The slice of `window` the opener touches (injectable — jsdom's `location.assign` cannot be spied). */
|
|
32
|
+
export interface OAuthWindow {
|
|
33
|
+
self: object;
|
|
34
|
+
top: object | null;
|
|
35
|
+
open: (url: string, target: string, features: string) => unknown;
|
|
36
|
+
location: { assign: (url: string) => void };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether this document is rendered inside another origin's frame. */
|
|
40
|
+
function isFramed(win: OAuthWindow): boolean {
|
|
41
|
+
try {
|
|
42
|
+
return win.top !== win.self;
|
|
43
|
+
} catch {
|
|
44
|
+
// A cross-origin `window.top` read throws in some browsers — that IS framed.
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Open the authorize link. In place when top-level (the dance ends back on
|
|
51
|
+
* this page); a new tab when framed (the IdP will not render in a frame —
|
|
52
|
+
* `returnTo` then lands the SAME page top-level in that tab, where
|
|
53
|
+
* {@link useOAuthReturn} shows the notice; the embedded chat picks the
|
|
54
|
+
* connection up on its next turn).
|
|
55
|
+
*/
|
|
56
|
+
export function openOAuthAuthorize(href: string, win: OAuthWindow = window): void {
|
|
57
|
+
if (isFramed(win)) {
|
|
58
|
+
win.open(href, "_blank", "noopener,noreferrer");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
win.location.assign(href);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface UseOAuthReturnResult {
|
|
65
|
+
/** The broker's answer found on the address bar at mount, until dismissed. */
|
|
66
|
+
notice: OAuthReturn | null;
|
|
67
|
+
dismiss: () => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Read `?connected=<serverName>` / `?error=<reason>` off the current
|
|
72
|
+
* location ONCE (on mount), strip them from the address bar via
|
|
73
|
+
* `history.replaceState`, and expose the result as a dismissible notice.
|
|
74
|
+
* SSR-safe: nothing runs until the effect.
|
|
75
|
+
*/
|
|
76
|
+
export function useOAuthReturn(): UseOAuthReturnResult {
|
|
77
|
+
const [notice, setNotice] = useState<OAuthReturn | null>(null);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (typeof window === "undefined") return;
|
|
80
|
+
const href = window.location.href;
|
|
81
|
+
const found = parseOAuthReturn(href);
|
|
82
|
+
if (found === null) return;
|
|
83
|
+
const clean = stripOAuthReturn(href);
|
|
84
|
+
if (clean !== href) window.history.replaceState(window.history.state, "", clean);
|
|
85
|
+
setNotice(found);
|
|
86
|
+
}, []);
|
|
87
|
+
const dismiss = useCallback(() => setNotice(null), []);
|
|
88
|
+
return { notice, dismiss };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface OAuthPromptActionArgs {
|
|
92
|
+
item: HitlPromptItem;
|
|
93
|
+
action: HitlPromptAction;
|
|
94
|
+
/** The kit's ledger (`useTranscriptInputs().answerHitlPrompt`) — records the pick locally; nothing is delivered. */
|
|
95
|
+
answerHitlPrompt: (ask: AgPausedAsk, action: HitlPromptAction) => AgHitlAnswer;
|
|
96
|
+
/** Where the broker should send the user back; defaults to {@link oauthReturnToHere}. */
|
|
97
|
+
returnTo?: string;
|
|
98
|
+
/** How to open the link; defaults to {@link openOAuthAuthorize}. */
|
|
99
|
+
open?: (href: string, ask: AgPausedAsk) => void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The OAuth arm of a prompt action, shared by every web surface. Returns
|
|
104
|
+
* `false` (nothing done) when the item is not an OAuth ask, so a host's
|
|
105
|
+
* #207 hitl branch (build the answer, POST it to the pod door) runs as
|
|
106
|
+
* before. For an OAuth ask:
|
|
107
|
+
*
|
|
108
|
+
* - a mode pick (or a plain accept on a mode-less ask) records the pick in
|
|
109
|
+
* the ledger — the card shows "Connecting — <mode>" — and OPENS
|
|
110
|
+
* `authorizationUrl&mode=<id>&returnTo=<here>`; NOTHING is posted to the
|
|
111
|
+
* pod (there is no answer door — the answer is the redirect);
|
|
112
|
+
* - "Not now" / decline / dismiss all record `cancelled` (still pending,
|
|
113
|
+
* re-askable): nothing is written anywhere and the pod asks again next
|
|
114
|
+
* turn.
|
|
115
|
+
*/
|
|
116
|
+
export function oauthPromptAction(args: OAuthPromptActionArgs): boolean {
|
|
117
|
+
const { item, action, answerHitlPrompt } = args;
|
|
118
|
+
if (item.oauth === null) return false;
|
|
119
|
+
if (typeof action === "object" || action === "accept") {
|
|
120
|
+
const grantModeId = typeof action === "object" ? action.grantModeId : null;
|
|
121
|
+
answerHitlPrompt(item.ask, action);
|
|
122
|
+
const href = oauthAuthorizeHref(item.ask, grantModeId, args.returnTo ?? oauthReturnToHere());
|
|
123
|
+
if (args.open !== undefined) args.open(href, item.ask);
|
|
124
|
+
else openOAuthAuthorize(href);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
answerHitlPrompt(item.ask, "dismiss");
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
@@ -227,9 +227,9 @@ export interface UseTranscriptInputsResult {
|
|
|
227
227
|
* VALIDATES it against the ask's persisted record (`validateHitlAnswer`
|
|
228
228
|
* — required-iff-declared, echo-must-be-declared, requestState byte-echo)
|
|
229
229
|
* BEFORE anything dispatches, records it in the ledger, and returns it
|
|
230
|
-
* for the HOST to deliver — the kit renders and validates; the
|
|
231
|
-
*
|
|
232
|
-
*
|
|
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).
|
|
233
233
|
*/
|
|
234
234
|
answerHitlPrompt: (ask: AgPausedAsk, action: HitlPromptAction) => AgHitlAnswer;
|
|
235
235
|
}
|
|
@@ -250,36 +250,12 @@ export function useTranscriptInputs(invoke: UseAgentInvokeReturn): UseTranscript
|
|
|
250
250
|
return () => clearInterval(timer);
|
|
251
251
|
}, [invoke.status]);
|
|
252
252
|
|
|
253
|
-
// R10 ledger: the hook exposes only the LATEST pending
|
|
254
|
-
// 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).
|
|
255
257
|
const [prompts, setPrompts] = useState<ProfilePromptInput[]>([]);
|
|
256
258
|
const promptSeq = useRef(0);
|
|
257
|
-
useEffect(() => {
|
|
258
|
-
const request = invoke.profileConsentRequest;
|
|
259
|
-
if (request === null) {
|
|
260
|
-
setPrompts((prev) =>
|
|
261
|
-
prev.some((p) => p.kind === "consent" && p.state === "pending")
|
|
262
|
-
? prev.map((p) =>
|
|
263
|
-
p.kind === "consent" && p.state === "pending" ? { ...p, state: "dismissed" } : p,
|
|
264
|
-
)
|
|
265
|
-
: prev,
|
|
266
|
-
);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
setPrompts((prev) => {
|
|
270
|
-
if (prev.some((p) => p.kind === "consent" && p.state === "pending")) return prev;
|
|
271
|
-
return [
|
|
272
|
-
...prev,
|
|
273
|
-
{
|
|
274
|
-
id: `consent.${promptSeq.current++}`,
|
|
275
|
-
kind: "consent",
|
|
276
|
-
appId: request.appId,
|
|
277
|
-
requested: request.requested,
|
|
278
|
-
state: "pending",
|
|
279
|
-
},
|
|
280
|
-
];
|
|
281
|
-
});
|
|
282
|
-
}, [invoke.profileConsentRequest]);
|
|
283
259
|
useEffect(() => {
|
|
284
260
|
const request = invoke.profileLinkRequest;
|
|
285
261
|
if (request === null) {
|
|
@@ -313,7 +289,6 @@ export function useTranscriptInputs(invoke: UseAgentInvokeReturn): UseTranscript
|
|
|
313
289
|
// Clearing the hook's pending request AFTER the ledger moved keeps the
|
|
314
290
|
// dismissal effect above from double-transitioning it.
|
|
315
291
|
const pending = prompts.find((p) => p.id === id);
|
|
316
|
-
if (pending?.kind === "consent") invoke.clearProfileConsentRequest();
|
|
317
292
|
if (pending?.kind === "link") invoke.clearProfileLinkRequest();
|
|
318
293
|
},
|
|
319
294
|
[invoke, prompts],
|
package/src/react.tsx
CHANGED
|
@@ -49,5 +49,14 @@ export {
|
|
|
49
49
|
type UseTranscriptInputsResult,
|
|
50
50
|
} from "./react/use-transcript.js";
|
|
51
51
|
export { GuueyChat, type GuueyChatProps, type GuueyChatHandle } from "./react/guuey-chat.js";
|
|
52
|
+
export {
|
|
53
|
+
oauthPromptAction,
|
|
54
|
+
oauthReturnToHere,
|
|
55
|
+
openOAuthAuthorize,
|
|
56
|
+
useOAuthReturn,
|
|
57
|
+
type OAuthPromptActionArgs,
|
|
58
|
+
type OAuthWindow,
|
|
59
|
+
type UseOAuthReturnResult,
|
|
60
|
+
} from "./react/oauth-return.js";
|
|
52
61
|
export { Markdown } from "./react/markdown.js";
|
|
53
62
|
export { themeCssVars, type ThemeMode } from "./react/theme-css.js";
|
package/src/strings.ts
CHANGED
|
@@ -96,6 +96,16 @@ export interface ChatStrings {
|
|
|
96
96
|
/** The answered record line, e.g. `Allowed — Always`. */
|
|
97
97
|
promptAnsweredWith: (modeLabel: string) => string;
|
|
98
98
|
promptDeclinedRecord: string;
|
|
99
|
+
/**
|
|
100
|
+
* The OAuth arm (guuey#178): the dismiss action ("Not now" — nothing is
|
|
101
|
+
* written, the ask re-emits next turn), the answered record (the user was
|
|
102
|
+
* sent to the provider), and the return notices the surface shows after
|
|
103
|
+
* the broker 302s back with `?connected=<serverName>` / `?error=<reason>`.
|
|
104
|
+
*/
|
|
105
|
+
promptNotNow: string;
|
|
106
|
+
promptOAuthSent: (modeLabel: string) => string;
|
|
107
|
+
oauthConnected: (serverName: string) => string;
|
|
108
|
+
oauthFailed: (reason: string) => string;
|
|
99
109
|
|
|
100
110
|
/** R16 — the notice row's label (provenance shows only under debug). */
|
|
101
111
|
noticeLabel: string;
|
|
@@ -171,6 +181,10 @@ export const defaultChatStrings: ChatStrings = {
|
|
|
171
181
|
promptDismissed: "Dismissed",
|
|
172
182
|
promptAnsweredWith: (modeLabel) => `Allowed — ${modeLabel}`,
|
|
173
183
|
promptDeclinedRecord: "Not allowed",
|
|
184
|
+
promptNotNow: "Not now",
|
|
185
|
+
promptOAuthSent: (modeLabel) => `Connecting — ${modeLabel}`,
|
|
186
|
+
oauthConnected: (serverName) => `Connected ${serverName}. The agent can use it from your next message.`,
|
|
187
|
+
oauthFailed: (reason) => `Couldn't connect: ${reason}`,
|
|
174
188
|
|
|
175
189
|
noticeLabel: "Note",
|
|
176
190
|
|
package/src/types.ts
CHANGED
|
@@ -47,14 +47,17 @@ export interface TranscriptMessage {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* A pending/resolved
|
|
51
|
-
* `profile-
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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}.)
|
|
54
57
|
*/
|
|
55
58
|
export interface ProfilePromptInput {
|
|
56
59
|
id: string;
|
|
57
|
-
kind: "
|
|
60
|
+
kind: "link";
|
|
58
61
|
appId: string;
|
|
59
62
|
requested: "read" | "read-write";
|
|
60
63
|
state: "pending" | "answered" | "declined" | "dismissed";
|
|
@@ -83,9 +86,9 @@ export interface HitlPromptInput {
|
|
|
83
86
|
|
|
84
87
|
/**
|
|
85
88
|
* R10 prompt inputs — a discriminated union (narrow on `kind`). The
|
|
86
|
-
* profile arm is the
|
|
87
|
-
*
|
|
88
|
-
* 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.)
|
|
89
92
|
*/
|
|
90
93
|
export type PromptItemInput = ProfilePromptInput | HitlPromptInput;
|
|
91
94
|
|
|
@@ -117,7 +120,7 @@ export interface TranscriptInputs {
|
|
|
117
120
|
statusElapsedMs: number;
|
|
118
121
|
activeTool: string | null;
|
|
119
122
|
error: { message: string; code: string | null } | null;
|
|
120
|
-
/** Pending/answered
|
|
123
|
+
/** Pending/answered asks (R10): the link invite + AgJSON hitl asks (consent). */
|
|
121
124
|
prompts: PromptItemInput[];
|
|
122
125
|
/**
|
|
123
126
|
* The settled conversation, both roles, in order. Assistant entries are
|
|
@@ -320,12 +323,12 @@ export interface CitationsItem extends BaseItem {
|
|
|
320
323
|
style: "chips" | "list";
|
|
321
324
|
}
|
|
322
325
|
|
|
323
|
-
/** R10 — the guuey-wire
|
|
326
|
+
/** R10 — the guuey-wire account-LINK prompt card (the original arm). */
|
|
324
327
|
export interface ProfilePromptItem extends BaseItem {
|
|
325
328
|
kind: "prompt";
|
|
326
329
|
/** The `PromptItemInput.id` this row records — the host's resolution key. */
|
|
327
330
|
promptId: string;
|
|
328
|
-
promptKind: "
|
|
331
|
+
promptKind: "link";
|
|
329
332
|
appId: string;
|
|
330
333
|
requested: "read" | "read-write";
|
|
331
334
|
state: "pending" | "answered" | "declined" | "dismissed";
|
|
@@ -352,6 +355,14 @@ export interface HitlPromptItem extends BaseItem {
|
|
|
352
355
|
askKind: AgPausedAsk["kind"];
|
|
353
356
|
/** Declared accept variants; empty = a plain accept/decline ask. */
|
|
354
357
|
grantModes: readonly AgGrantMode[];
|
|
358
|
+
/**
|
|
359
|
+
* The OAuth arm (guuey#178): set when the ask is `kind:"auth"` with
|
|
360
|
+
* `authConfig.scheme:"oauth2"` + an `authorizationUrl`. There is no
|
|
361
|
+
* answer door for this ask — a mode pick OPENS `authorizationUrl` with
|
|
362
|
+
* `&mode=` + `&returnTo=` appended (`oauthAuthorizeHref`); "Not now" is a
|
|
363
|
+
* plain dismissal. `null` for every other ask.
|
|
364
|
+
*/
|
|
365
|
+
oauth: { authorizationUrl: string; scopes: readonly string[] } | null;
|
|
355
366
|
state: "pending" | "resolved" | "declined" | "cancelled";
|
|
356
367
|
/** Echo of the chosen mode id (identity, never displayed as meaning). */
|
|
357
368
|
chosenModeId: string | null;
|
package/styles.css
CHANGED
|
@@ -349,6 +349,32 @@
|
|
|
349
349
|
margin: 0;
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
/* The OAuth "authorize this server" return notice (guuey#178) — shown by
|
|
353
|
+
<GuueyChat> after the broker sends the user back with ?connected= / ?error=. */
|
|
354
|
+
.guuey-chat-oauth-notice {
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
justify-content: space-between;
|
|
358
|
+
gap: 8px;
|
|
359
|
+
margin: 0;
|
|
360
|
+
padding: 8px 12px;
|
|
361
|
+
font-size: 0.85em;
|
|
362
|
+
color: var(--guuey-chat-ink);
|
|
363
|
+
background: color-mix(in srgb, var(--guuey-chat-accent) 10%, var(--guuey-chat-surface));
|
|
364
|
+
border-top: 1px solid var(--guuey-chat-canvas-muted);
|
|
365
|
+
}
|
|
366
|
+
.guuey-chat-oauth-error {
|
|
367
|
+
background: color-mix(in srgb, var(--guuey-chat-error) 7%, var(--guuey-chat-surface));
|
|
368
|
+
}
|
|
369
|
+
.guuey-chat-oauth-dismiss {
|
|
370
|
+
font: inherit;
|
|
371
|
+
cursor: pointer;
|
|
372
|
+
border: 0;
|
|
373
|
+
background: transparent;
|
|
374
|
+
color: var(--guuey-chat-ink-muted);
|
|
375
|
+
text-decoration: underline;
|
|
376
|
+
}
|
|
377
|
+
|
|
352
378
|
/* ── R11 errors ── */
|
|
353
379
|
.guuey-chat-error {
|
|
354
380
|
border: 1px solid color-mix(in srgb, var(--guuey-chat-error) 45%, transparent);
|