@guuey/chat 0.4.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/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/history-inputs.d.ts +24 -0
- package/dist/history-inputs.d.ts.map +1 -0
- package/dist/history-inputs.js +34 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/plan.d.ts +5 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +629 -0
- package/dist/policy.d.ts +110 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +75 -0
- package/dist/react/components.d.ts +87 -0
- package/dist/react/components.d.ts.map +1 -0
- package/dist/react/components.js +270 -0
- package/dist/react/guuey-chat.d.ts +84 -0
- package/dist/react/guuey-chat.d.ts.map +1 -0
- package/dist/react/guuey-chat.js +103 -0
- package/dist/react/markdown.d.ts +32 -0
- package/dist/react/markdown.d.ts.map +1 -0
- package/dist/react/markdown.js +40 -0
- package/dist/react/theme-css.d.ts +16 -0
- package/dist/react/theme-css.d.ts.map +1 -0
- package/dist/react/theme-css.js +37 -0
- package/dist/react/transcript.d.ts +42 -0
- package/dist/react/transcript.d.ts.map +1 -0
- package/dist/react/transcript.js +88 -0
- package/dist/react/use-transcript.d.ts +39 -0
- package/dist/react/use-transcript.d.ts.map +1 -0
- package/dist/react/use-transcript.js +201 -0
- package/dist/react.d.ts +21 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +20 -0
- package/dist/strings.d.ts +74 -0
- package/dist/strings.d.ts.map +1 -0
- package/dist/strings.js +45 -0
- package/dist/theme.d.ts +99 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +182 -0
- package/dist/types.d.ts +283 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +87 -0
- package/src/corpus/README.md +40 -0
- package/src/corpus/__snapshots__/corpus.test.ts.snap +1590 -0
- package/src/corpus/capture.ts +67 -0
- package/src/corpus/captures/issue2627-render-capture.coalesced.sse.txt +173 -0
- package/src/corpus/drive.ts +184 -0
- package/src/corpus/fixtures.ts +338 -0
- package/src/history-inputs.ts +48 -0
- package/src/index.ts +58 -0
- package/src/plan.ts +740 -0
- package/src/policy.ts +146 -0
- package/src/react/components.tsx +655 -0
- package/src/react/guuey-chat.tsx +227 -0
- package/src/react/markdown.tsx +114 -0
- package/src/react/theme-css.ts +50 -0
- package/src/react/transcript.tsx +187 -0
- package/src/react/use-transcript.ts +274 -0
- package/src/react.tsx +51 -0
- package/src/strings.ts +144 -0
- package/src/theme.ts +195 -0
- package/src/types.ts +320 -0
- package/styles.css +514 -0
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* `<GuueyChat>` — the batteries-included surface (guuey#135 wave 3c): the
|
|
4
|
+
* whole tier stack wired end to end, PLUS the composer (which arrives here
|
|
5
|
+
* per the founder ruling — earlier tiers are transcript-only).
|
|
6
|
+
*
|
|
7
|
+
* useAgentInvoke → useTranscriptInputs → useTranscript → <Transcript>
|
|
8
|
+
* + the composer
|
|
9
|
+
*
|
|
10
|
+
* It is a THIN composition of the exported tiers — every wire below is a
|
|
11
|
+
* public API, so a builder ejects one level down (own composer around
|
|
12
|
+
* `<Transcript>`, own renderer over `planTranscript`, own everything over
|
|
13
|
+
* `invokeTurn`) without a cliff. Every prop beyond the connection
|
|
14
|
+
* essentials is optional.
|
|
15
|
+
*
|
|
16
|
+
* ## Composer state matrix
|
|
17
|
+
*
|
|
18
|
+
* - `endpointUrl === null` → input disabled, `composerUnavailable`
|
|
19
|
+
* placeholder (chat cannot exist).
|
|
20
|
+
* - idle (`status === "ready"`) → input enabled; **Send** enabled iff the
|
|
21
|
+
* input has non-whitespace text.
|
|
22
|
+
* - in flight (any other status, cold-start waits included — R12's status
|
|
23
|
+
* line owns the WHY) → input stays enabled (type the next message while
|
|
24
|
+
* the agent works), Send is replaced by **Stop**, which aborts the turn
|
|
25
|
+
* (partial text kept, "Stopped." marked — the hook's contract).
|
|
26
|
+
* - Enter sends; Shift+Enter inserts a newline; an IME-composing Enter
|
|
27
|
+
* never sends (the candidate commit is not a submit).
|
|
28
|
+
*
|
|
29
|
+
* ## History
|
|
30
|
+
*
|
|
31
|
+
* Thread rehydration is the HOOK's mechanics: give `adapters` a `history`
|
|
32
|
+
* adapter (e.g. `createWebAdapters({ apiBaseUrl, … })`) and a persisted
|
|
33
|
+
* threadId rehydrates on mount — text transcript + persisted cards, which
|
|
34
|
+
* mount through the same R6 path as live views. Nothing here to configure.
|
|
35
|
+
*/
|
|
36
|
+
import { useCallback, useMemo, useState } from "react";
|
|
37
|
+
import { createWebAdapters } from "@guuey/agent-client";
|
|
38
|
+
import { useAgentInvoke } from "@guuey/agent-client/react";
|
|
39
|
+
import { calmPolicy, debugPolicy } from "../policy.js";
|
|
40
|
+
import { defaultChatStrings } from "../strings.js";
|
|
41
|
+
import { DEFAULT_CHAT_THEME } from "../theme.js";
|
|
42
|
+
import { Transcript } from "./transcript.js";
|
|
43
|
+
import { useTranscript, useTranscriptInputs } from "./use-transcript.js";
|
|
44
|
+
const PROMPT_STATE = {
|
|
45
|
+
accept: "answered",
|
|
46
|
+
decline: "declined",
|
|
47
|
+
dismiss: "dismissed",
|
|
48
|
+
};
|
|
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]);
|
|
52
|
+
const invoke = useAgentInvoke({ endpointUrl, ...(appId !== undefined ? { appId } : {}), adapters, preserveBlocks: true });
|
|
53
|
+
const policy = useMemo(() => {
|
|
54
|
+
const factory = preset === "debug" ? debugPolicy : calmPolicy;
|
|
55
|
+
const strings = {
|
|
56
|
+
...defaultChatStrings,
|
|
57
|
+
...policyOverrides?.strings,
|
|
58
|
+
...stringOverrides,
|
|
59
|
+
};
|
|
60
|
+
return factory({ ...policyOverrides, strings });
|
|
61
|
+
}, [preset, policyOverrides, stringOverrides]);
|
|
62
|
+
const { inputs, resolvePrompt } = useTranscriptInputs(invoke);
|
|
63
|
+
const { plan, toggle, resolvedMounts, onViewPhase } = useTranscript({
|
|
64
|
+
inputs,
|
|
65
|
+
policy,
|
|
66
|
+
...(reader !== undefined ? { reader } : {}),
|
|
67
|
+
});
|
|
68
|
+
// ── Composer ─────────────────────────────────────────────────────────
|
|
69
|
+
const [input, setInput] = useState("");
|
|
70
|
+
const busy = invoke.status !== "ready";
|
|
71
|
+
const available = endpointUrl !== null;
|
|
72
|
+
const canSend = available && !busy && input.trim() !== "";
|
|
73
|
+
const submit = useCallback(() => {
|
|
74
|
+
const text = input.trim();
|
|
75
|
+
if (text === "" || !available || busy)
|
|
76
|
+
return;
|
|
77
|
+
setInput("");
|
|
78
|
+
void invoke.send(text).catch(() => {
|
|
79
|
+
// The hook surfaces the failure (`error` / R0 failed-send).
|
|
80
|
+
});
|
|
81
|
+
}, [input, available, busy, invoke]);
|
|
82
|
+
const handleRetry = useCallback((item) => {
|
|
83
|
+
void invoke.send(item.text).catch(() => {
|
|
84
|
+
// Same contract as submit: the hook owns failure surfacing.
|
|
85
|
+
});
|
|
86
|
+
}, [invoke]);
|
|
87
|
+
const handlePromptAction = useCallback((item, action) => {
|
|
88
|
+
resolvePrompt(item.promptId, PROMPT_STATE[action]);
|
|
89
|
+
onPromptAction?.(item, action);
|
|
90
|
+
}, [resolvePrompt, onPromptAction]);
|
|
91
|
+
const strings = policy.strings;
|
|
92
|
+
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
|
+
e.preventDefault();
|
|
94
|
+
submit();
|
|
95
|
+
}, children: [_jsx("textarea", { className: "guuey-chat-composer-input", rows: 1, value: input, onChange: (e) => setInput(e.target.value), onKeyDown: (e) => {
|
|
96
|
+
// `isComposing` guards IME input: an Enter that commits a
|
|
97
|
+
// candidate must not also send the message.
|
|
98
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
submit();
|
|
101
|
+
}
|
|
102
|
+
}, 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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The R1 markdown surface — the ONE place `@guuey/chat` renders
|
|
3
|
+
* agent-influenced content in the embedder's origin, and therefore the
|
|
4
|
+
* package's sanitizer boundary (spec §3.1).
|
|
5
|
+
*
|
|
6
|
+
* The pipeline is `@silverprotocol/richtext`: markdown → a TYPED AST in
|
|
7
|
+
* which raw HTML is structurally unrepresentable (no HTML node type
|
|
8
|
+
* exists; angle brackets can only ever be literal text) → React elements.
|
|
9
|
+
* That is deliberately STRONGER than the spec's letter (markdown → HTML →
|
|
10
|
+
* sanitize): there is no HTML string at any point, so there is nothing to
|
|
11
|
+
* sanitize incorrectly — no `dangerouslySetInnerHTML` anywhere in this
|
|
12
|
+
* package.
|
|
13
|
+
*
|
|
14
|
+
* Safety properties, each pinned by a test:
|
|
15
|
+
* - raw HTML (`<img onerror=…>`, `<script>`) renders as literal text;
|
|
16
|
+
* - link `href` is populated upstream ONLY for http/https/mailto —
|
|
17
|
+
* `javascript:`, `data:`, `vbscript:`, and relative targets parse as
|
|
18
|
+
* styled text with NO anchor `href` (richtext's `SAFE_HREF` allowlist);
|
|
19
|
+
* - navigable links carry `rel="noopener noreferrer" target="_blank"`;
|
|
20
|
+
* - there is NO image syntax in the node vocabulary at all — the spec's
|
|
21
|
+
* F5 ruling (markdown images off; R7 media blocks are the sanctioned
|
|
22
|
+
* image path) holds structurally, not by configuration.
|
|
23
|
+
*
|
|
24
|
+
* Streaming-tolerant by upstream design: unclosed emphasis mid-delta
|
|
25
|
+
* renders as formatted-so-far.
|
|
26
|
+
*/
|
|
27
|
+
import type { ReactNode } from "react";
|
|
28
|
+
/** Sanitized markdown → React elements (see the module docblock). */
|
|
29
|
+
export declare function Markdown({ text }: {
|
|
30
|
+
text: string;
|
|
31
|
+
}): ReactNode;
|
|
32
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/react/markdown.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AA8EvC,qEAAqE;AACrE,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAQ9D"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { parseRichText, } from "@silverprotocol/richtext";
|
|
3
|
+
function Inline({ nodes }) {
|
|
4
|
+
return nodes.map((node, i) => {
|
|
5
|
+
switch (node.type) {
|
|
6
|
+
case "text":
|
|
7
|
+
return _jsx("span", { children: node.text }, i);
|
|
8
|
+
case "break":
|
|
9
|
+
return _jsx("br", {}, i);
|
|
10
|
+
case "strong":
|
|
11
|
+
return (_jsx("strong", { children: _jsx(Inline, { nodes: node.children }) }, i));
|
|
12
|
+
case "em":
|
|
13
|
+
return (_jsx("em", { children: _jsx(Inline, { nodes: node.children }) }, i));
|
|
14
|
+
case "code":
|
|
15
|
+
return (_jsx("code", { className: "guuey-chat-inline-code", children: node.code }, i));
|
|
16
|
+
case "link":
|
|
17
|
+
return node.href ? (_jsx("a", { href: node.href, target: "_blank", rel: "noopener noreferrer", children: _jsx(Inline, { nodes: node.children }) }, i)) : (
|
|
18
|
+
// Unsafe or unresolvable target: the styled text, no anchor.
|
|
19
|
+
_jsx("span", { className: "guuey-chat-dead-link", children: _jsx(Inline, { nodes: node.children }) }, i));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function Block({ block }) {
|
|
24
|
+
switch (block.type) {
|
|
25
|
+
case "paragraph":
|
|
26
|
+
return (_jsx("p", { children: _jsx(Inline, { nodes: block.children }) }));
|
|
27
|
+
case "heading":
|
|
28
|
+
return (_jsx("p", { className: `guuey-chat-heading${block.level <= 3 ? " guuey-chat-heading-major" : ""}`, children: _jsx(Inline, { nodes: block.children }) }));
|
|
29
|
+
case "code-fence":
|
|
30
|
+
return _jsx("pre", { className: "guuey-chat-code-fence", children: block.code });
|
|
31
|
+
case "list": {
|
|
32
|
+
const Tag = block.ordered ? "ol" : "ul";
|
|
33
|
+
return (_jsx(Tag, { ...(block.ordered && block.start !== undefined ? { start: block.start } : {}), children: block.items.map((item, j) => (_jsx("li", { children: _jsx(Inline, { nodes: item.children }) }, j))) }));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Sanitized markdown → React elements (see the module docblock). */
|
|
38
|
+
export function Markdown({ text }) {
|
|
39
|
+
return (_jsx(_Fragment, { children: parseRichText(text).map((block, i) => (_jsx(Block, { block: block }, i))) }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CSS projection of `GuueyChatTheme` (spec §6): the schema is the
|
|
3
|
+
* cross-platform contract; this maps one mode's tokens onto the
|
|
4
|
+
* `--guuey-chat-*` custom properties the shipped stylesheet consumes. RN
|
|
5
|
+
* (3c) maps the same object to style values instead — no CSS here is
|
|
6
|
+
* load-bearing for the view-model.
|
|
7
|
+
*/
|
|
8
|
+
import type { GuueyChatTheme } from "../theme.js";
|
|
9
|
+
export type ThemeMode = "light" | "dark";
|
|
10
|
+
/**
|
|
11
|
+
* One mode's tokens as inline custom properties for the transcript root.
|
|
12
|
+
* Returned as a plain record so callers can spread it into `style` or emit
|
|
13
|
+
* a stylesheet from it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function themeCssVars(theme: GuueyChatTheme, mode: ThemeMode): Record<string, string>;
|
|
16
|
+
//# sourceMappingURL=theme-css.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-css.d.ts","sourceRoot":"","sources":["../../src/react/theme-css.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAazC;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAsB3F"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const RADIUS_PX = {
|
|
2
|
+
none: "0px",
|
|
3
|
+
soft: "10px",
|
|
4
|
+
round: "18px",
|
|
5
|
+
};
|
|
6
|
+
const DENSITY_GAP = {
|
|
7
|
+
compact: "6px",
|
|
8
|
+
comfortable: "10px",
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* One mode's tokens as inline custom properties for the transcript root.
|
|
12
|
+
* Returned as a plain record so callers can spread it into `style` or emit
|
|
13
|
+
* a stylesheet from it.
|
|
14
|
+
*/
|
|
15
|
+
export function themeCssVars(theme, mode) {
|
|
16
|
+
const palette = theme.colors[mode];
|
|
17
|
+
const vars = {
|
|
18
|
+
"--guuey-chat-accent": palette.accent,
|
|
19
|
+
"--guuey-chat-on-accent": palette.onAccent,
|
|
20
|
+
"--guuey-chat-ink": palette.ink,
|
|
21
|
+
"--guuey-chat-ink-muted": palette.inkMuted,
|
|
22
|
+
"--guuey-chat-surface": palette.surface,
|
|
23
|
+
"--guuey-chat-canvas": palette.canvas,
|
|
24
|
+
"--guuey-chat-canvas-muted": palette.canvasMuted,
|
|
25
|
+
"--guuey-chat-error": palette.error,
|
|
26
|
+
"--guuey-chat-radius": RADIUS_PX[theme.shape.radius],
|
|
27
|
+
"--guuey-chat-gap": DENSITY_GAP[theme.shape.density],
|
|
28
|
+
"--guuey-chat-scale": String(theme.typography.scale ?? 1),
|
|
29
|
+
};
|
|
30
|
+
if (theme.typography.fontFamily !== undefined) {
|
|
31
|
+
vars["--guuey-chat-font"] = theme.typography.fontFamily;
|
|
32
|
+
}
|
|
33
|
+
if (theme.typography.monoFontFamily !== undefined) {
|
|
34
|
+
vars["--guuey-chat-mono-font"] = theme.typography.monoFontFamily;
|
|
35
|
+
}
|
|
36
|
+
return vars;
|
|
37
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<Transcript>` — the plan walker + the §3.2 renderer obligations:
|
|
3
|
+
*
|
|
4
|
+
* - **Scroll contract:** stick-to-bottom while content grows UNLESS the
|
|
5
|
+
* user scrolled up, in which case a "jump to latest" affordance appears
|
|
6
|
+
* instead; anchoring holds through content resize (a ResizeObserver on
|
|
7
|
+
* the item column re-pins — R6 cards growing on `connected` are the
|
|
8
|
+
* canonical breaker). `prefers-reduced-motion` downgrades smooth
|
|
9
|
+
* scrolling to instant.
|
|
10
|
+
* - **Windowing:** long transcripts render the trailing `window.tail`
|
|
11
|
+
* items with a "show earlier" expander — the DOM is capped even though
|
|
12
|
+
* the plan (already O(groups)) carries everything; the plan's stable
|
|
13
|
+
* keys keep expansion state and DOM identity across the window edge.
|
|
14
|
+
* - Theme: the resolved `GuueyChatTheme` is projected as `--guuey-chat-*`
|
|
15
|
+
* custom properties on the root (import `@guuey/chat/styles.css` once
|
|
16
|
+
* for the default look, or restyle the class names entirely).
|
|
17
|
+
*/
|
|
18
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
19
|
+
import { type GuueyChatTheme } from "../theme.js";
|
|
20
|
+
import { type ChatStrings } from "../strings.js";
|
|
21
|
+
import type { TranscriptPlan } from "../types.js";
|
|
22
|
+
import { type TranscriptComponents, type TranscriptItemContext } from "./components.js";
|
|
23
|
+
import { type ThemeMode } from "./theme-css.js";
|
|
24
|
+
export interface TranscriptWindowing {
|
|
25
|
+
/** Trailing items rendered; earlier ones sit behind the expander. */
|
|
26
|
+
tail: number;
|
|
27
|
+
}
|
|
28
|
+
export interface TranscriptProps extends Pick<TranscriptItemContext, "onToggle" | "onRetry" | "onPromptAction" | "onErrorAction" | "resolvedMounts" | "onViewPhase" | "viewProps"> {
|
|
29
|
+
plan: TranscriptPlan;
|
|
30
|
+
/** Per-slot component overrides (spec §3's override column). */
|
|
31
|
+
components?: Partial<TranscriptComponents>;
|
|
32
|
+
/** The i18n seam — pass the same strings the policy carries. */
|
|
33
|
+
strings?: ChatStrings;
|
|
34
|
+
theme?: GuueyChatTheme;
|
|
35
|
+
mode?: ThemeMode;
|
|
36
|
+
/** DOM windowing (§3.2). `false` renders everything. Default tail 80. */
|
|
37
|
+
window?: TranscriptWindowing | false;
|
|
38
|
+
className?: string;
|
|
39
|
+
style?: CSSProperties;
|
|
40
|
+
}
|
|
41
|
+
export declare function Transcript(props: TranscriptProps): ReactNode;
|
|
42
|
+
//# sourceMappingURL=transcript.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* `<Transcript>` — the plan walker + the §3.2 renderer obligations:
|
|
4
|
+
*
|
|
5
|
+
* - **Scroll contract:** stick-to-bottom while content grows UNLESS the
|
|
6
|
+
* user scrolled up, in which case a "jump to latest" affordance appears
|
|
7
|
+
* instead; anchoring holds through content resize (a ResizeObserver on
|
|
8
|
+
* the item column re-pins — R6 cards growing on `connected` are the
|
|
9
|
+
* canonical breaker). `prefers-reduced-motion` downgrades smooth
|
|
10
|
+
* scrolling to instant.
|
|
11
|
+
* - **Windowing:** long transcripts render the trailing `window.tail`
|
|
12
|
+
* items with a "show earlier" expander — the DOM is capped even though
|
|
13
|
+
* the plan (already O(groups)) carries everything; the plan's stable
|
|
14
|
+
* keys keep expansion state and DOM identity across the window edge.
|
|
15
|
+
* - Theme: the resolved `GuueyChatTheme` is projected as `--guuey-chat-*`
|
|
16
|
+
* custom properties on the root (import `@guuey/chat/styles.css` once
|
|
17
|
+
* for the default look, or restyle the class names entirely).
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
20
|
+
import { DEFAULT_CHAT_THEME } from "../theme.js";
|
|
21
|
+
import { defaultChatStrings } from "../strings.js";
|
|
22
|
+
import { defaultTranscriptComponents, renderItem, } from "./components.js";
|
|
23
|
+
import { themeCssVars } from "./theme-css.js";
|
|
24
|
+
/** How close to the bottom (px) still counts as pinned. */
|
|
25
|
+
const PIN_THRESHOLD_PX = 48;
|
|
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;
|
|
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]);
|
|
30
|
+
// ── Windowing ────────────────────────────────────────────────────────
|
|
31
|
+
const [extraShown, setExtraShown] = useState(0);
|
|
32
|
+
const tail = windowing === false ? Number.POSITIVE_INFINITY : windowing.tail;
|
|
33
|
+
const visibleFrom = Math.max(0, plan.items.length - tail - extraShown);
|
|
34
|
+
const hidden = visibleFrom;
|
|
35
|
+
const visible = plan.items.slice(visibleFrom);
|
|
36
|
+
// ── Scroll contract ──────────────────────────────────────────────────
|
|
37
|
+
const scroller = useRef(null);
|
|
38
|
+
const column = useRef(null);
|
|
39
|
+
const pinned = useRef(true);
|
|
40
|
+
const [showJump, setShowJump] = useState(false);
|
|
41
|
+
const reducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
42
|
+
const scrollToBottom = (smooth) => {
|
|
43
|
+
const el = scroller.current;
|
|
44
|
+
if (el === null)
|
|
45
|
+
return;
|
|
46
|
+
if (typeof el.scrollTo === "function") {
|
|
47
|
+
el.scrollTo({ top: el.scrollHeight, behavior: smooth && !reducedMotion() ? "smooth" : "auto" });
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Environments without Element.scrollTo (jsdom; ancient WebViews).
|
|
51
|
+
el.scrollTop = el.scrollHeight;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const onScroll = () => {
|
|
55
|
+
const el = scroller.current;
|
|
56
|
+
if (el === null)
|
|
57
|
+
return;
|
|
58
|
+
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < PIN_THRESHOLD_PX;
|
|
59
|
+
pinned.current = nearBottom;
|
|
60
|
+
setShowJump(!nearBottom);
|
|
61
|
+
};
|
|
62
|
+
// New content while pinned keeps the bottom in view (before paint, so
|
|
63
|
+
// per-frame streaming updates never visibly jump).
|
|
64
|
+
useLayoutEffect(() => {
|
|
65
|
+
if (pinned.current)
|
|
66
|
+
scrollToBottom(false);
|
|
67
|
+
}, [plan]);
|
|
68
|
+
// Content RESIZE (an R6 card connecting and growing, an image loading)
|
|
69
|
+
// re-pins too — scrollHeight changes with no plan change.
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const el = column.current;
|
|
72
|
+
if (el === null || typeof ResizeObserver === "undefined")
|
|
73
|
+
return;
|
|
74
|
+
const observer = new ResizeObserver(() => {
|
|
75
|
+
if (pinned.current)
|
|
76
|
+
scrollToBottom(false);
|
|
77
|
+
});
|
|
78
|
+
observer.observe(el);
|
|
79
|
+
return () => observer.disconnect();
|
|
80
|
+
}, []);
|
|
81
|
+
const rootStyle = { ...themeCssVars(theme, mode), ...style };
|
|
82
|
+
const StatusComponent = resolvedComponents.status;
|
|
83
|
+
return (_jsxs("div", { className: `guuey-chat${className !== undefined ? ` ${className}` : ""}`, style: rootStyle, "data-guuey-chat-mode": mode, children: [_jsx("div", { ref: scroller, className: "guuey-chat-scroller", onScroll: onScroll, children: _jsxs("div", { ref: column, className: "guuey-chat-column", children: [hidden > 0 ? (_jsx("button", { type: "button", className: "guuey-chat-show-earlier", onClick: () => setExtraShown((n) => n + tail), children: strings.showEarlier(hidden) })) : null, visible.map((item) => renderItem(item, resolvedComponents, ctx)), plan.status !== null ? (_jsx(StatusComponent, { item: plan.status, ctx: ctx })) : null] }) }), showJump ? (_jsx("button", { type: "button", className: "guuey-chat-jump", onClick: () => {
|
|
84
|
+
pinned.current = true;
|
|
85
|
+
setShowJump(false);
|
|
86
|
+
scrollToBottom(true);
|
|
87
|
+
}, children: strings.jumpToLatest })) : null] }));
|
|
88
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { UseAgentInvokeReturn } from "@guuey/agent-client";
|
|
2
|
+
import { type ResolvedViewMount, type UiResourceReader, type ViewHostPhase } from "@guuey/mcp-apps-host";
|
|
3
|
+
import type { TranscriptPolicy } from "../policy.js";
|
|
4
|
+
import type { ItemKey, TranscriptInputs, TranscriptOverrides, TranscriptPlan } from "../types.js";
|
|
5
|
+
export interface UseTranscriptArgs {
|
|
6
|
+
inputs: TranscriptInputs;
|
|
7
|
+
policy: TranscriptPolicy;
|
|
8
|
+
/**
|
|
9
|
+
* Resolves R6 `locator` mounts (history cards persisted as `ui://`
|
|
10
|
+
* identities) with a fresh authenticated `resources/read`. Without one,
|
|
11
|
+
* locators render the R13 "view expired" state after a failed local
|
|
12
|
+
* resolution — labeled, never blank.
|
|
13
|
+
*/
|
|
14
|
+
reader?: UiResourceReader;
|
|
15
|
+
}
|
|
16
|
+
export interface UseTranscriptResult {
|
|
17
|
+
plan: TranscriptPlan;
|
|
18
|
+
/** Flip one item's collapse state (wired to every toggle's onClick). */
|
|
19
|
+
toggle: (key: ItemKey) => void;
|
|
20
|
+
/** The live collapse-override map (renderer-owned, plan input). */
|
|
21
|
+
overrides: TranscriptOverrides;
|
|
22
|
+
/** Wire to `<GuueyView onPhaseChange>` (the default kit already does). */
|
|
23
|
+
onViewPhase: (key: ItemKey, phase: ViewHostPhase) => void;
|
|
24
|
+
/** Locator resolutions: mount material, or `"expired"` for a miss. */
|
|
25
|
+
resolvedMounts: ReadonlyMap<ItemKey, ResolvedViewMount | "expired">;
|
|
26
|
+
}
|
|
27
|
+
export declare function useTranscript({ inputs, policy, reader }: UseTranscriptArgs): UseTranscriptResult;
|
|
28
|
+
export interface UseTranscriptInputsResult {
|
|
29
|
+
inputs: TranscriptInputs;
|
|
30
|
+
/**
|
|
31
|
+
* Record the user's action on an R10 prompt (the host performs the
|
|
32
|
+
* actual grant/decline through its own channel; this moves the
|
|
33
|
+
* transcript record). A pending prompt whose hook-side request vanishes
|
|
34
|
+
* without a recorded action reads as `dismissed`.
|
|
35
|
+
*/
|
|
36
|
+
resolvePrompt: (id: string, state: "answered" | "declined" | "dismissed") => void;
|
|
37
|
+
}
|
|
38
|
+
export declare function useTranscriptInputs(invoke: UseAgentInvokeReturn): UseTranscriptInputsResult;
|
|
39
|
+
//# sourceMappingURL=use-transcript.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderer-state owner + the LIVE input assembler (spec §7).
|
|
3
|
+
*
|
|
4
|
+
* `planTranscript` is pure — everything stateful about rendering lives
|
|
5
|
+
* here, in renderer-owned React state:
|
|
6
|
+
*
|
|
7
|
+
* - `useTranscript` owns the user's collapse overrides, the live
|
|
8
|
+
* `ViewHostPhase` reports, and locator-mount resolution, merges them
|
|
9
|
+
* into the inputs, and returns the (memoized) plan;
|
|
10
|
+
* - `useTranscriptInputs` assembles `TranscriptInputs` from
|
|
11
|
+
* `useAgentInvoke`'s return — the live twin of the root export's
|
|
12
|
+
* `transcriptInputsFromHistory`.
|
|
13
|
+
*/
|
|
14
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
15
|
+
import { resolveViewMount, } from "@guuey/mcp-apps-host";
|
|
16
|
+
import { planTranscript } from "../plan.js";
|
|
17
|
+
export function useTranscript({ inputs, policy, reader }) {
|
|
18
|
+
const [overrides, setOverrides] = useState({});
|
|
19
|
+
const [phases, setPhases] = useState({});
|
|
20
|
+
const [resolvedMounts, setResolvedMounts] = useState(new Map());
|
|
21
|
+
const merged = useMemo(() => ({ ...inputs, viewPhases: { ...inputs.viewPhases, ...phases } }), [inputs, phases]);
|
|
22
|
+
const plan = useMemo(() => planTranscript(merged, policy, overrides), [merged, policy, overrides]);
|
|
23
|
+
// Toggle flips the item's CURRENT resolved state (policy default or a
|
|
24
|
+
// previous override) — read from the plan so the first toggle of a
|
|
25
|
+
// default-expanded item collapses it.
|
|
26
|
+
const planRef = useRef(plan);
|
|
27
|
+
planRef.current = plan;
|
|
28
|
+
const toggle = useCallback((key) => {
|
|
29
|
+
const findExpanded = () => {
|
|
30
|
+
for (const item of planRef.current.items) {
|
|
31
|
+
if (item.key === key)
|
|
32
|
+
return item.expanded;
|
|
33
|
+
if (item.kind === "tool-group") {
|
|
34
|
+
for (const tool of item.tools) {
|
|
35
|
+
if (tool.key === key)
|
|
36
|
+
return tool.expanded;
|
|
37
|
+
if (tool.result?.key === key)
|
|
38
|
+
return tool.result.expanded;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (item.kind === "tool" && item.result?.key === key)
|
|
42
|
+
return item.result.expanded;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
};
|
|
46
|
+
const current = findExpanded();
|
|
47
|
+
setOverrides((prev) => ({ ...prev, [key]: { expanded: !current } }));
|
|
48
|
+
}, []);
|
|
49
|
+
const onViewPhase = useCallback((key, phase) => {
|
|
50
|
+
setPhases((prev) => (prev[key] === phase ? prev : { ...prev, [key]: phase }));
|
|
51
|
+
}, []);
|
|
52
|
+
// Locator resolution — one read per locator key, misses become "expired".
|
|
53
|
+
const readerRef = useRef(reader);
|
|
54
|
+
readerRef.current = reader;
|
|
55
|
+
const inFlight = useRef(new Set());
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
for (const item of plan.items) {
|
|
58
|
+
if (item.kind !== "view")
|
|
59
|
+
continue;
|
|
60
|
+
if (item.mount === null || item.mount.channel !== "locator")
|
|
61
|
+
continue;
|
|
62
|
+
if (resolvedMounts.has(item.key) || inFlight.current.has(item.key))
|
|
63
|
+
continue;
|
|
64
|
+
const read = readerRef.current;
|
|
65
|
+
const locator = item.mount;
|
|
66
|
+
inFlight.current.add(item.key);
|
|
67
|
+
const settle = (value) => {
|
|
68
|
+
inFlight.current.delete(item.key);
|
|
69
|
+
setResolvedMounts((prev) => {
|
|
70
|
+
const next = new Map(prev);
|
|
71
|
+
next.set(item.key, value);
|
|
72
|
+
return next;
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
if (read === undefined) {
|
|
76
|
+
settle("expired");
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
void resolveViewMount(locator, read).then((resolved) => settle(resolved ?? "expired"), () => settle("expired"));
|
|
80
|
+
}
|
|
81
|
+
}, [plan, resolvedMounts]);
|
|
82
|
+
return { plan, toggle, overrides, onViewPhase, resolvedMounts };
|
|
83
|
+
}
|
|
84
|
+
/** How often the escalation clock ticks while a status needs one. */
|
|
85
|
+
const ELAPSED_TICK_MS = 500;
|
|
86
|
+
export function useTranscriptInputs(invoke) {
|
|
87
|
+
// §4: the elapsed clock is a view-model INPUT — this is the renderer-side
|
|
88
|
+
// timer that feeds it. Reset on every status change; ticking only while a
|
|
89
|
+
// status line is showing (R12 escalation + debug detail).
|
|
90
|
+
const [elapsedMs, setElapsedMs] = useState(0);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
setElapsedMs(0);
|
|
93
|
+
if (invoke.status === "ready" || invoke.status === "responding")
|
|
94
|
+
return;
|
|
95
|
+
const startedAt = Date.now();
|
|
96
|
+
const timer = setInterval(() => setElapsedMs(Date.now() - startedAt), ELAPSED_TICK_MS);
|
|
97
|
+
return () => clearInterval(timer);
|
|
98
|
+
}, [invoke.status]);
|
|
99
|
+
// R10 ledger: the hook exposes only the LATEST pending ask; the
|
|
100
|
+
// transcript keeps the record of every ask and its resolution.
|
|
101
|
+
const [prompts, setPrompts] = useState([]);
|
|
102
|
+
const promptSeq = useRef(0);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const request = invoke.profileConsentRequest;
|
|
105
|
+
if (request === null) {
|
|
106
|
+
setPrompts((prev) => prev.some((p) => p.kind === "consent" && p.state === "pending")
|
|
107
|
+
? prev.map((p) => p.kind === "consent" && p.state === "pending" ? { ...p, state: "dismissed" } : p)
|
|
108
|
+
: prev);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
setPrompts((prev) => {
|
|
112
|
+
if (prev.some((p) => p.kind === "consent" && p.state === "pending"))
|
|
113
|
+
return prev;
|
|
114
|
+
return [
|
|
115
|
+
...prev,
|
|
116
|
+
{
|
|
117
|
+
id: `consent.${promptSeq.current++}`,
|
|
118
|
+
kind: "consent",
|
|
119
|
+
appId: request.appId,
|
|
120
|
+
requested: request.requested,
|
|
121
|
+
state: "pending",
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
});
|
|
125
|
+
}, [invoke.profileConsentRequest]);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const request = invoke.profileLinkRequest;
|
|
128
|
+
if (request === null) {
|
|
129
|
+
setPrompts((prev) => prev.some((p) => p.kind === "link" && p.state === "pending")
|
|
130
|
+
? prev.map((p) => p.kind === "link" && p.state === "pending" ? { ...p, state: "dismissed" } : p)
|
|
131
|
+
: prev);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
setPrompts((prev) => {
|
|
135
|
+
if (prev.some((p) => p.kind === "link" && p.state === "pending"))
|
|
136
|
+
return prev;
|
|
137
|
+
return [
|
|
138
|
+
...prev,
|
|
139
|
+
{
|
|
140
|
+
id: `link.${promptSeq.current++}`,
|
|
141
|
+
kind: "link",
|
|
142
|
+
appId: request.appId,
|
|
143
|
+
requested: request.requested,
|
|
144
|
+
state: "pending",
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
});
|
|
148
|
+
}, [invoke.profileLinkRequest]);
|
|
149
|
+
const resolvePrompt = useCallback((id, state) => {
|
|
150
|
+
setPrompts((prev) => prev.map((p) => (p.id === id ? { ...p, state } : p)));
|
|
151
|
+
// Clearing the hook's pending request AFTER the ledger moved keeps the
|
|
152
|
+
// dismissal effect above from double-transitioning it.
|
|
153
|
+
const pending = prompts.find((p) => p.id === id);
|
|
154
|
+
if (pending?.kind === "consent")
|
|
155
|
+
invoke.clearProfileConsentRequest();
|
|
156
|
+
if (pending?.kind === "link")
|
|
157
|
+
invoke.clearProfileLinkRequest();
|
|
158
|
+
}, [invoke, prompts]);
|
|
159
|
+
const inputs = useMemo(() => {
|
|
160
|
+
// Source-ownership split (plan.ts's rules): the trailing assistant
|
|
161
|
+
// entry is the IN-FLIGHT fold (or the abort-kept partial) — it moves to
|
|
162
|
+
// `assistantText` so the plan can mark it streaming/stopped; settled
|
|
163
|
+
// turns stay in `messages`.
|
|
164
|
+
const inFlight = invoke.status !== "ready";
|
|
165
|
+
let messages = invoke.messages;
|
|
166
|
+
let assistantText = "";
|
|
167
|
+
const last = messages[messages.length - 1];
|
|
168
|
+
if (last !== undefined && last.role === "assistant" && (inFlight || invoke.aborted)) {
|
|
169
|
+
assistantText = last.text;
|
|
170
|
+
messages = messages.slice(0, -1);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
result: invoke.reduceResult,
|
|
174
|
+
assistantText,
|
|
175
|
+
status: invoke.status,
|
|
176
|
+
statusElapsedMs: elapsedMs,
|
|
177
|
+
activeTool: invoke.activeTool,
|
|
178
|
+
error: invoke.error !== null ? { message: invoke.error, code: invoke.errorCode } : null,
|
|
179
|
+
prompts,
|
|
180
|
+
messages,
|
|
181
|
+
...(invoke.historyCards.length > 0 ? { historyCards: invoke.historyCards } : {}),
|
|
182
|
+
sendStates: invoke.sendStates,
|
|
183
|
+
aborted: invoke.aborted,
|
|
184
|
+
adopted: invoke.adopted,
|
|
185
|
+
};
|
|
186
|
+
}, [
|
|
187
|
+
invoke.messages,
|
|
188
|
+
invoke.status,
|
|
189
|
+
invoke.activeTool,
|
|
190
|
+
invoke.error,
|
|
191
|
+
invoke.errorCode,
|
|
192
|
+
invoke.reduceResult,
|
|
193
|
+
invoke.historyCards,
|
|
194
|
+
invoke.sendStates,
|
|
195
|
+
invoke.aborted,
|
|
196
|
+
invoke.adopted,
|
|
197
|
+
elapsedMs,
|
|
198
|
+
prompts,
|
|
199
|
+
]);
|
|
200
|
+
return { inputs, resolvePrompt };
|
|
201
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React entry point (`@guuey/chat/react`) — the component kit over the
|
|
3
|
+
* root subpath's headless view-model (wave 3b, guuey#135).
|
|
4
|
+
*
|
|
5
|
+
* The root subpath stays React-free forever; everything React-coupled —
|
|
6
|
+
* the per-category components, `<Transcript>`, the renderer-state owner
|
|
7
|
+
* (`useTranscript`), and the live input assembler (`useTranscriptInputs`)
|
|
8
|
+
* — lives behind this arm, with `react` as an optional peer (the
|
|
9
|
+
* mcp-apps-host precedent).
|
|
10
|
+
*
|
|
11
|
+
* Composition ladder (spec §1 "configurable"): use `<Transcript>` with the
|
|
12
|
+
* default kit as-is → override per-category components → tune policy knobs
|
|
13
|
+
* → drop to `planTranscript` + `attachViewHost` and render it all yourself.
|
|
14
|
+
*/
|
|
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";
|
|
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";
|
|
19
|
+
export { Markdown } from "./react/markdown.js";
|
|
20
|
+
export { themeCssVars, type ThemeMode } from "./react/theme-css.js";
|
|
21
|
+
//# sourceMappingURL=react.d.ts.map
|