@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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/dist/history-inputs.d.ts +24 -0
  4. package/dist/history-inputs.d.ts.map +1 -0
  5. package/dist/history-inputs.js +34 -0
  6. package/dist/index.d.ts +19 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +17 -0
  9. package/dist/plan.d.ts +5 -0
  10. package/dist/plan.d.ts.map +1 -0
  11. package/dist/plan.js +629 -0
  12. package/dist/policy.d.ts +110 -0
  13. package/dist/policy.d.ts.map +1 -0
  14. package/dist/policy.js +75 -0
  15. package/dist/react/components.d.ts +87 -0
  16. package/dist/react/components.d.ts.map +1 -0
  17. package/dist/react/components.js +270 -0
  18. package/dist/react/guuey-chat.d.ts +84 -0
  19. package/dist/react/guuey-chat.d.ts.map +1 -0
  20. package/dist/react/guuey-chat.js +103 -0
  21. package/dist/react/markdown.d.ts +32 -0
  22. package/dist/react/markdown.d.ts.map +1 -0
  23. package/dist/react/markdown.js +40 -0
  24. package/dist/react/theme-css.d.ts +16 -0
  25. package/dist/react/theme-css.d.ts.map +1 -0
  26. package/dist/react/theme-css.js +37 -0
  27. package/dist/react/transcript.d.ts +42 -0
  28. package/dist/react/transcript.d.ts.map +1 -0
  29. package/dist/react/transcript.js +88 -0
  30. package/dist/react/use-transcript.d.ts +39 -0
  31. package/dist/react/use-transcript.d.ts.map +1 -0
  32. package/dist/react/use-transcript.js +201 -0
  33. package/dist/react.d.ts +21 -0
  34. package/dist/react.d.ts.map +1 -0
  35. package/dist/react.js +20 -0
  36. package/dist/strings.d.ts +74 -0
  37. package/dist/strings.d.ts.map +1 -0
  38. package/dist/strings.js +45 -0
  39. package/dist/theme.d.ts +99 -0
  40. package/dist/theme.d.ts.map +1 -0
  41. package/dist/theme.js +182 -0
  42. package/dist/types.d.ts +283 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +1 -0
  45. package/package.json +87 -0
  46. package/src/corpus/README.md +40 -0
  47. package/src/corpus/__snapshots__/corpus.test.ts.snap +1590 -0
  48. package/src/corpus/capture.ts +67 -0
  49. package/src/corpus/captures/issue2627-render-capture.coalesced.sse.txt +173 -0
  50. package/src/corpus/drive.ts +184 -0
  51. package/src/corpus/fixtures.ts +338 -0
  52. package/src/history-inputs.ts +48 -0
  53. package/src/index.ts +58 -0
  54. package/src/plan.ts +740 -0
  55. package/src/policy.ts +146 -0
  56. package/src/react/components.tsx +655 -0
  57. package/src/react/guuey-chat.tsx +227 -0
  58. package/src/react/markdown.tsx +114 -0
  59. package/src/react/theme-css.ts +50 -0
  60. package/src/react/transcript.tsx +187 -0
  61. package/src/react/use-transcript.ts +274 -0
  62. package/src/react.tsx +51 -0
  63. package/src/strings.ts +144 -0
  64. package/src/theme.ts +195 -0
  65. package/src/types.ts +320 -0
  66. 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
+ }
@@ -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