lemma-sdk 0.2.11 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -223,6 +223,40 @@ The intended split is:
223
223
  - SDK: `useAssistantController`, message/tool normalization, plan parsing, tool rollups, and assistant UI primitives.
224
224
  - App: modal shell, fullscreen behavior, route navigation, workspace/file viewers, and product-specific renderers.
225
225
 
226
+ Useful UI primitives exported from `lemma-sdk/react`:
227
+
228
+ - `AssistantHeader`
229
+ - `AssistantConversationList`
230
+ - `AssistantModelPicker`
231
+ - `AssistantShellLayout`
232
+ - `AssistantComposer`
233
+ - `AssistantMessageViewport`
234
+ - `AssistantAskOverlay`
235
+ - `AssistantPendingFileChip`
236
+ - `AssistantStatusPill`
237
+ - `MessageGroup`
238
+ - `PlanSummaryStrip`
239
+
240
+ For a direct plug-and-play component, `AssistantEmbedded` wires `useAssistantController` into the default assistant experience:
241
+
242
+ ```tsx
243
+ import { AssistantEmbedded } from "lemma-sdk/react";
244
+
245
+ function SupportAssistant() {
246
+ return (
247
+ <AssistantEmbedded
248
+ client={client}
249
+ assistantId="support_assistant"
250
+ podId="pod_123"
251
+ title="Support Assistant"
252
+ subtitle="Ask questions about this pod."
253
+ placeholder="Message Support Assistant"
254
+ showConversationList
255
+ />
256
+ );
257
+ }
258
+ ```
259
+
226
260
  ### Assistant names (resource key)
227
261
 
228
262
  Assistant CRUD is name-based:
@@ -0,0 +1,77 @@
1
+ import { type ComponentPropsWithoutRef, type ReactNode } from "react";
2
+ import type { AssistantConversationListItem, AssistantConversationRenderArgs } from "./assistant-types.js";
3
+ export interface AssistantHeaderProps {
4
+ title: ReactNode;
5
+ subtitle?: ReactNode;
6
+ badge?: ReactNode;
7
+ controls?: ReactNode;
8
+ className?: string;
9
+ }
10
+ export interface AssistantMessageViewportProps extends ComponentPropsWithoutRef<"div"> {
11
+ innerClassName?: string;
12
+ children: ReactNode;
13
+ }
14
+ export declare const AssistantMessageViewport: import("react").ForwardRefExoticComponent<AssistantMessageViewportProps & import("react").RefAttributes<HTMLDivElement>>;
15
+ export interface AssistantShellLayoutProps {
16
+ sidebar?: ReactNode;
17
+ sidebarVisible?: boolean;
18
+ main: ReactNode;
19
+ className?: string;
20
+ }
21
+ export declare function AssistantShellLayout({ sidebar, sidebarVisible, main, className, }: AssistantShellLayoutProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function AssistantHeader({ title, subtitle, badge, controls, className, }: AssistantHeaderProps): import("react/jsx-runtime").JSX.Element;
23
+ export interface AssistantConversationListProps {
24
+ conversations: AssistantConversationListItem[];
25
+ activeConversationId: string | null;
26
+ onSelectConversation: (conversationId: string) => void;
27
+ onNewConversation?: () => void;
28
+ renderConversationLabel?: (args: AssistantConversationRenderArgs) => ReactNode;
29
+ title?: ReactNode;
30
+ newLabel?: ReactNode;
31
+ className?: string;
32
+ }
33
+ export declare function AssistantConversationList({ conversations, activeConversationId, onSelectConversation, onNewConversation, renderConversationLabel, title, newLabel, className, }: AssistantConversationListProps): import("react/jsx-runtime").JSX.Element;
34
+ export interface AssistantModelPickerProps<TValue extends string = string> {
35
+ value: TValue | null;
36
+ options: TValue[];
37
+ disabled?: boolean;
38
+ autoLabel?: ReactNode;
39
+ getOptionLabel?: (value: TValue) => ReactNode;
40
+ onChange: (value: TValue | null) => void;
41
+ className?: string;
42
+ }
43
+ export declare function AssistantModelPicker<TValue extends string = string>({ value, options, disabled, autoLabel, getOptionLabel, onChange, className, }: AssistantModelPickerProps<TValue>): import("react/jsx-runtime").JSX.Element;
44
+ export interface AssistantAskOverlayProps {
45
+ questionNumber: number;
46
+ totalQuestions: number;
47
+ question: ReactNode;
48
+ options: string[];
49
+ selectedOptions: string[];
50
+ canContinue: boolean;
51
+ continueLabel: ReactNode;
52
+ onSelectOption: (option: string) => void;
53
+ onContinue?: () => void;
54
+ onSkip?: () => void;
55
+ mode?: "single_select" | "multi_select" | "rank_priorities";
56
+ }
57
+ export declare function AssistantAskOverlay({ questionNumber, totalQuestions, question, options, selectedOptions, canContinue, continueLabel, onSelectOption, onContinue, onSkip, mode, }: AssistantAskOverlayProps): import("react/jsx-runtime").JSX.Element;
58
+ export interface AssistantPendingFileChipProps {
59
+ label: ReactNode;
60
+ onRemove?: () => void;
61
+ className?: string;
62
+ }
63
+ export interface AssistantComposerProps {
64
+ floating?: ReactNode;
65
+ status?: ReactNode;
66
+ pendingFiles?: ReactNode;
67
+ children: ReactNode;
68
+ className?: string;
69
+ }
70
+ export declare function AssistantComposer({ floating, status, pendingFiles, children, className, }: AssistantComposerProps): import("react/jsx-runtime").JSX.Element;
71
+ export declare function AssistantPendingFileChip({ label, onRemove, className, }: AssistantPendingFileChipProps): import("react/jsx-runtime").JSX.Element;
72
+ export interface AssistantStatusPillProps {
73
+ label: ReactNode;
74
+ subtle?: boolean;
75
+ className?: string;
76
+ }
77
+ export declare function AssistantStatusPill({ label, subtle, className, }: AssistantStatusPillProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ function cx(...values) {
4
+ return values.filter(Boolean).join(" ");
5
+ }
6
+ export const AssistantMessageViewport = forwardRef(function AssistantMessageViewport({ className, innerClassName, children, ...props }, ref) {
7
+ return (_jsx("div", { ref: ref, className: cx("min-h-0 flex-1 overflow-y-auto bg-[var(--bg-surface)] px-4 py-4", className), ...props, children: _jsx("div", { className: cx("mx-auto flex w-full max-w-5xl flex-col gap-3", innerClassName), children: children }) }));
8
+ });
9
+ export function AssistantShellLayout({ sidebar, sidebarVisible = false, main, className, }) {
10
+ return (_jsxs("div", { className: cx("mx-auto flex h-full w-full min-h-0 flex-col gap-3 font-sans antialiased", !!sidebar && sidebarVisible && "lg:grid lg:grid-cols-[280px_minmax(0,1fr)] lg:gap-3", className), children: [sidebar && sidebarVisible ? (_jsx("div", { className: "hidden h-full min-h-0 lg:block", children: sidebar })) : null, main] }));
11
+ }
12
+ export function AssistantHeader({ title, subtitle, badge, controls, className, }) {
13
+ return (_jsxs("div", { className: cx("flex items-center justify-between border-b border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] px-4 py-3", className), children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [badge ? (_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-full bg-[linear-gradient(135deg,var(--brand-primary),var(--brand-secondary))] shadow-[var(--shadow-xs)]", children: badge })) : null, _jsxs("div", { children: [_jsx("h3", { className: "text-[13px] font-semibold leading-tight text-[var(--text-primary)]", children: title }), subtitle ? (_jsx("p", { className: "text-[11px] text-[var(--text-tertiary)]", children: subtitle })) : null] })] }), controls ? (_jsx("div", { className: "flex items-center gap-1", children: controls })) : null] }));
14
+ }
15
+ export function AssistantConversationList({ conversations, activeConversationId, onSelectConversation, onNewConversation, renderConversationLabel, title = "Conversations", newLabel = "New", className, }) {
16
+ return (_jsxs("aside", { className: cx("flex h-full min-h-0 flex-col overflow-hidden rounded-2xl border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] shadow-[var(--shadow-lg)]", className), children: [_jsx("div", { className: "border-b border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] px-4 py-3", children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-[13px] font-semibold text-[var(--text-primary)]", children: title }), _jsxs("div", { className: "mt-1 text-[11px] text-[var(--text-tertiary)]", children: [conversations.length, " total"] })] }), onNewConversation ? (_jsx("button", { type: "button", onClick: onNewConversation, className: "rounded-full border border-[var(--border-default)] bg-[var(--bg-surface)] px-3 py-1.5 text-[11px] font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]", children: newLabel })) : null] }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-2", children: conversations.map((conversation) => {
17
+ const isActive = conversation.id === activeConversationId;
18
+ return (_jsxs("button", { type: "button", onClick: () => onSelectConversation(conversation.id), className: cx("w-full rounded-xl border px-3 py-2.5 text-left transition-colors", isActive
19
+ ? "border-[color:color-mix(in_srgb,_var(--brand-primary)_44%,_var(--border-default))] bg-[color:color-mix(in_srgb,_var(--brand-glow)_42%,_var(--bg-surface))]"
20
+ : "border-[var(--border-default)] bg-[var(--bg-surface)] hover:bg-[var(--bg-subtle)]"), children: [_jsx("div", { className: "truncate text-[12px] font-medium text-[var(--text-primary)]", children: renderConversationLabel
21
+ ? renderConversationLabel({ conversation, isActive })
22
+ : (conversation.title || "Untitled conversation") }), _jsx("div", { className: "mt-1 text-[10px] uppercase tracking-[0.08em] text-[var(--text-tertiary)]", children: (conversation.status || "waiting").toLowerCase() })] }, conversation.id));
23
+ }) })] }));
24
+ }
25
+ export function AssistantModelPicker({ value, options, disabled, autoLabel = "Auto", getOptionLabel, onChange, className, }) {
26
+ const autoValue = "__AUTO__";
27
+ return (_jsxs("select", { value: value ?? autoValue, onChange: (event) => onChange(event.target.value === autoValue ? null : event.target.value), disabled: disabled, className: cx("h-8 rounded-full border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] px-3 text-[11px] text-[var(--text-secondary)]", className), "aria-label": "Conversation model", title: "Conversation model", children: [_jsx("option", { value: autoValue, children: autoLabel }), options.map((option) => (_jsx("option", { value: option, children: getOptionLabel ? getOptionLabel(option) : option }, option)))] }));
28
+ }
29
+ export function AssistantAskOverlay({ questionNumber, totalQuestions, question, options, selectedOptions, canContinue, continueLabel, onSelectOption, onContinue, onSkip, mode = "single_select", }) {
30
+ return (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsxs("div", { className: "text-[11px] uppercase tracking-[0.12em] text-[var(--text-tertiary)]", children: ["Question ", questionNumber, " of ", totalQuestions] }), _jsx("p", { className: "mt-1 text-[14px] font-medium leading-6 text-[var(--text-primary)]", children: question })] }), onSkip ? (_jsx("button", { type: "button", onClick: onSkip, className: "rounded-md px-2 py-1 text-[12px] text-[var(--text-tertiary)] transition-colors hover:bg-[var(--bg-subtle)] hover:text-[var(--text-primary)]", children: "Skip" })) : null] }), _jsx("div", { className: "max-h-[260px] space-y-1.5 overflow-y-auto pr-1", children: options.map((option, optionIndex) => {
31
+ const isSelected = selectedOptions.includes(option);
32
+ const rankLabel = mode === "rank_priorities" && isSelected
33
+ ? selectedOptions.indexOf(option) + 1
34
+ : null;
35
+ return (_jsx("button", { type: "button", onClick: () => onSelectOption(option), className: cx("w-full rounded-lg border px-2.5 py-2 text-left text-[13px] transition-colors", isSelected
36
+ ? "border-[color:color-mix(in_srgb,_var(--brand-primary)_64%,_var(--border-subtle))] bg-[color:color-mix(in_srgb,_var(--brand-primary)_14%,_transparent)] text-[var(--text-primary)]"
37
+ : "border-[var(--border-default)] bg-[var(--bg-canvas)] text-[var(--text-secondary)] hover:bg-[var(--bg-subtle)] hover:text-[var(--text-primary)]"), children: _jsxs("span", { className: "inline-flex items-center gap-2", children: [rankLabel ? (_jsx("span", { className: "inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-[var(--brand-primary)] px-1 text-[10px] font-semibold text-[var(--text-on-brand)]", children: rankLabel })) : (_jsx("span", { className: cx("inline-block h-2.5 w-2.5 rounded-full border", isSelected
38
+ ? "border-[var(--brand-primary)] bg-[var(--brand-primary)]"
39
+ : "border-[var(--border-default)] bg-transparent") })), option] }) }, `${option}-${optionIndex}`));
40
+ }) }), onContinue ? (_jsx("div", { className: "flex justify-end", children: _jsx("button", { type: "button", onClick: onContinue, disabled: !canContinue, className: cx("rounded-md px-2.5 py-1.5 text-[12px] font-medium transition-colors", canContinue
41
+ ? "bg-[var(--brand-primary)] text-[var(--text-on-brand)] hover:bg-[color:color-mix(in_srgb,_var(--brand-primary)_88%,_var(--text-primary))]"
42
+ : "bg-[var(--bg-subtle)] text-[var(--text-tertiary)]"), children: continueLabel }) })) : null] }));
43
+ }
44
+ export function AssistantComposer({ floating, status, pendingFiles, children, className, }) {
45
+ return (_jsxs("div", { className: cx("relative rounded-2xl border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] p-2 shadow-[var(--shadow-md)]", className), children: [floating ? (_jsx("div", { className: "absolute bottom-[calc(100%+8px)] left-0 right-0 z-20", children: floating })) : null, _jsx("div", { className: "min-h-[34px] px-2 pb-1", children: _jsx("div", { className: "flex min-h-[26px] items-center transition-opacity duration-200", children: status || _jsx("span", { "aria-hidden": "true", className: "inline-block h-[30px]" }) }) }), pendingFiles ? (_jsx("div", { className: "flex flex-wrap items-center gap-1.5 px-1 pb-1.5", children: pendingFiles })) : null, children] }));
46
+ }
47
+ export function AssistantPendingFileChip({ label, onRemove, className, }) {
48
+ return (_jsxs("span", { className: cx("inline-flex max-w-full items-center gap-1.5 rounded-full bg-[var(--bg-subtle)] px-2 py-1 text-[11px] text-[var(--text-secondary)]", className), children: [_jsx("span", { className: "truncate max-w-[180px]", children: label }), onRemove ? (_jsx("button", { type: "button", onClick: onRemove, className: "inline-flex h-4 w-4 items-center justify-center rounded-full hover:bg-[var(--bg-canvas)]", title: "Remove file", children: "\u00D7" })) : null] }));
49
+ }
50
+ export function AssistantStatusPill({ label, subtle = false, className, }) {
51
+ return (_jsxs("div", { className: cx("inline-flex min-h-[30px] max-w-full items-center gap-2 rounded-full px-3 py-1.5 text-[12px] transition-all duration-200", subtle
52
+ ? "border border-[color:color-mix(in_srgb,_var(--border-default)_72%,_transparent)] bg-[color:color-mix(in_srgb,_var(--bg-surface)_90%,_transparent)] text-[var(--text-tertiary)]"
53
+ : "border border-[color:color-mix(in_srgb,_var(--brand-primary)_24%,_var(--border-default))] bg-[color:color-mix(in_srgb,_var(--brand-glow)_28%,_var(--bg-surface))] text-[var(--text-secondary)]", className), children: [_jsxs("span", { className: "relative inline-flex h-2.5 w-2.5 shrink-0", children: [_jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--brand-primary)]/45" }), _jsx("span", { className: "relative inline-flex h-2.5 w-2.5 rounded-full bg-[var(--brand-primary)]" })] }), _jsx("span", { className: "truncate", children: label })] }));
54
+ }
@@ -0,0 +1,8 @@
1
+ import type { LemmaClient } from "../../client.js";
2
+ import { type AssistantConversationScope } from "../useAssistantController.js";
3
+ import { type AssistantExperienceViewProps } from "./AssistantExperience.js";
4
+ export interface AssistantEmbeddedProps extends Omit<AssistantExperienceViewProps, "controller">, AssistantConversationScope {
5
+ client: LemmaClient;
6
+ enabled?: boolean;
7
+ }
8
+ export declare function AssistantEmbedded({ client, podId, assistantId, organizationId, enabled, ...props }: AssistantEmbeddedProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useAssistantController } from "../useAssistantController.js";
3
+ import { AssistantExperienceView } from "./AssistantExperience.js";
4
+ export function AssistantEmbedded({ client, podId, assistantId, organizationId, enabled = true, ...props }) {
5
+ const controller = useAssistantController({
6
+ client,
7
+ podId: podId ?? undefined,
8
+ assistantId: assistantId ?? undefined,
9
+ organizationId: organizationId ?? undefined,
10
+ enabled,
11
+ });
12
+ return _jsx(AssistantExperienceView, { controller: controller, ...props });
13
+ }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
3
3
  import { AvailableModels } from "../../types.js";
4
+ import { AssistantAskOverlay, AssistantMessageViewport, } from "./AssistantChrome.js";
4
5
  function cx(...values) {
5
6
  return values.filter(Boolean).join(" ");
6
7
  }
@@ -997,26 +998,14 @@ export function AssistantExperienceView({ controller, title = "Lemma Assistant",
997
998
  return (_jsxs("button", { type: "button", onClick: () => controller.selectConversation(conversation.id), className: cx("w-full rounded-xl border px-3 py-2.5 text-left transition-colors", isActive
998
999
  ? "border-[color:color-mix(in_srgb,_var(--brand-primary)_44%,_var(--border-default))] bg-[color:color-mix(in_srgb,_var(--brand-glow)_42%,_var(--bg-surface))]"
999
1000
  : "border-[var(--border-default)] bg-[var(--bg-surface)] hover:bg-[var(--bg-subtle)]"), children: [_jsx("div", { className: "text-[12px] font-medium text-[var(--text-primary)]", children: renderConversationLabel({ conversation, isActive }) }), _jsx("div", { className: "mt-1 text-[10px] uppercase tracking-[0.08em] text-[var(--text-tertiary)]", children: (conversation.status || "waiting").toLowerCase() })] }, conversation.id));
1000
- }) })] })) : null, _jsxs("div", { className: "flex h-full min-h-0 flex-col gap-3", children: [_jsxs("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] shadow-[var(--shadow-lg)]", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] px-4 py-3", children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("div", { className: "h-7 w-7 rounded-full bg-[linear-gradient(135deg,var(--brand-primary),var(--brand-secondary))] flex items-center justify-center shadow-[var(--shadow-xs)]", children: _jsx("span", { className: "text-[var(--text-on-brand)] text-xs", children: "\u2728" }) }), _jsxs("div", { children: [_jsx("h3", { className: "font-semibold text-[var(--text-primary)] text-[13px] leading-tight", children: title }), _jsx("p", { className: "text-[11px] text-[var(--text-tertiary)]", children: subtitle })] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("select", { value: controller.conversationModel || "", onChange: (event) => { void handleModelChange(event.target.value || null); }, disabled: isConversationBusy || isUpdatingModel, className: "h-8 rounded-full border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] px-3 text-[11px] text-[var(--text-secondary)]", children: [_jsx("option", { value: "", children: "Auto" }), availableModels.map((availableModel) => (_jsx("option", { value: availableModel, children: availableModel }, availableModel)))] }), _jsx("button", { type: "button", onClick: controller.clearMessages, title: "New conversation", className: "inline-flex h-8 w-8 items-center justify-center rounded-full text-[var(--text-tertiary)] hover:bg-[var(--bg-subtle)] hover:text-[var(--text-secondary)]", children: "\u21BA" })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-3 min-h-[180px] bg-[var(--bg-surface)]", ref: messagesContainerRef, onScroll: updatePinnedState, children: [controller.messages.length === 0 && !isConversationBusy ? (emptyState || _jsx(EmptyState, { onSendMessage: (message) => { void controller.sendMessage(message); } })) : null, (controller.isLoadingMessages && controller.messages.length === 0) ? (_jsx("div", { className: "flex justify-center py-6", children: _jsx("span", { className: "text-[var(--text-tertiary)] text-sm", children: "Loading\u2026" }) })) : null, (controller.isLoadingOlderMessages && controller.messages.length > 0) ? (_jsx("div", { className: "flex justify-center py-1", children: _jsx("span", { className: "text-[var(--text-tertiary)] text-xs", children: "Loading older\u2026" }) })) : null, displayMessageRows.map((row, index) => {
1001
+ }) })] })) : null, _jsxs("div", { className: "flex h-full min-h-0 flex-col gap-3", children: [_jsxs("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] shadow-[var(--shadow-lg)]", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] px-4 py-3", children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("div", { className: "h-7 w-7 rounded-full bg-[linear-gradient(135deg,var(--brand-primary),var(--brand-secondary))] flex items-center justify-center shadow-[var(--shadow-xs)]", children: _jsx("span", { className: "text-[var(--text-on-brand)] text-xs", children: "\u2728" }) }), _jsxs("div", { children: [_jsx("h3", { className: "font-semibold text-[var(--text-primary)] text-[13px] leading-tight", children: title }), _jsx("p", { className: "text-[11px] text-[var(--text-tertiary)]", children: subtitle })] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("select", { value: controller.conversationModel || "", onChange: (event) => { void handleModelChange(event.target.value || null); }, disabled: isConversationBusy || isUpdatingModel, className: "h-8 rounded-full border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] px-3 text-[11px] text-[var(--text-secondary)]", children: [_jsx("option", { value: "", children: "Auto" }), availableModels.map((availableModel) => (_jsx("option", { value: availableModel, children: availableModel }, availableModel)))] }), _jsx("button", { type: "button", onClick: controller.clearMessages, title: "New conversation", className: "inline-flex h-8 w-8 items-center justify-center rounded-full text-[var(--text-tertiary)] hover:bg-[var(--bg-subtle)] hover:text-[var(--text-secondary)]", children: "\u21BA" })] })] }), _jsxs(AssistantMessageViewport, { className: "min-h-[180px]", ref: messagesContainerRef, onScroll: updatePinnedState, children: [controller.messages.length === 0 && !isConversationBusy ? (emptyState || _jsx(EmptyState, { onSendMessage: (message) => { void controller.sendMessage(message); } })) : null, (controller.isLoadingMessages && controller.messages.length === 0) ? (_jsx("div", { className: "flex justify-center py-6", children: _jsx("span", { className: "text-[var(--text-tertiary)] text-sm", children: "Loading\u2026" }) })) : null, (controller.isLoadingOlderMessages && controller.messages.length > 0) ? (_jsx("div", { className: "flex justify-center py-1", children: _jsx("span", { className: "text-[var(--text-tertiary)] text-xs", children: "Loading older\u2026" }) })) : null, displayMessageRows.map((row, index) => {
1001
1002
  const previousRow = index > 0 ? displayMessageRows[index - 1] : null;
1002
1003
  const showAssistantHeader = row.message.role !== "assistant"
1003
1004
  ? false
1004
1005
  : previousRow?.message.role !== "assistant";
1005
1006
  const includesLastRawMessage = row.sourceIndexes.includes(controller.messages.length - 1);
1006
1007
  return (_jsx(MessageGroup, { message: row.message, onNavigateResource: onNavigateResource, onWidgetSendPrompt: handleWidgetSendPrompt, conversationId: controller.activeConversationId, isStreaming: isConversationBusy && includesLastRawMessage && row.message.role === "assistant", showAssistantHeader: showAssistantHeader, renderMessageContent: renderMessageContent, renderPresentedFile: renderPresentedFile, renderToolInvocation: renderToolInvocation }, row.id || index));
1007
- }), isConversationBusy && controller.messages.length > 0 && !activeToolBanner && !lastMessageHasContent ? (_jsx(ThinkingIndicator, {})) : null, controller.error ? (_jsx("div", { className: "bg-[color:color-mix(in_srgb,_var(--state-error)_12%,_transparent)] border border-[color:color-mix(in_srgb,_var(--state-error)_48%,_var(--border-subtle))] rounded-lg p-3 text-xs text-[var(--state-error)] flex items-start gap-2.5", children: _jsxs("div", { children: [_jsx("p", { className: "font-medium", children: "Something went wrong" }), _jsx("p", { className: "text-[var(--state-error)] mt-1", children: controller.error })] }) })) : null, (controller.messages.length > 0 || isConversationBusy || !!controller.error) ? (_jsx("div", { "aria-hidden": "true", className: "h-14 shrink-0" })) : null] })] }), _jsxs("div", { className: "relative rounded-2xl border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] p-2 shadow-[var(--shadow-md)]", children: [planSummary ? (_jsx("div", { className: "absolute bottom-[calc(100%+8px)] left-0 right-0 z-20", children: isPlanHidden ? (_jsxs("button", { type: "button", onClick: () => setIsPlanHidden(false), className: "inline-flex items-center gap-2 rounded-lg border border-[var(--border-default)] bg-[var(--bg-surface)] px-3 py-1.5 text-[11px] font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-subtle)] transition-colors", children: ["Show plan (", planSummary.completedCount, "/", planSummary.steps.length, ")"] })) : (_jsx(PlanSummaryStrip, { plan: planSummary, onHide: () => setIsPlanHidden(true) })) })) : null, isConversationBusy && activeToolBanner ? (_jsx("div", { className: "px-2 pb-1", children: _jsx("div", { className: "inline-flex max-w-full items-center gap-1.5 text-[11px] text-[var(--text-tertiary)] animate-in fade-in duration-200", children: _jsx("span", { className: "truncate", children: activeToolBanner.summary }) }) })) : null, activeAskQuestion && effectiveAskOverlayState && pendingAskUserInput ? (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsxs("div", { className: "text-[11px] uppercase tracking-[0.12em] text-[var(--text-tertiary)]", children: ["Question ", effectiveAskOverlayState.currentQuestionIndex + 1, " of ", pendingAskUserInput.questions.length] }), _jsx("p", { className: "mt-1 text-[14px] font-medium text-[var(--text-primary)] leading-6", children: activeAskQuestion.question })] }), _jsx("button", { type: "button", onClick: () => dismissAskOverlay(effectiveAskOverlayState.toolCallId), className: "rounded-md px-2 py-1 text-[12px] text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-subtle)] transition-colors", children: "Skip" })] }), _jsx("div", { className: "max-h-[260px] overflow-y-auto space-y-1.5 pr-1", children: activeAskQuestion.options.map((option, optionIndex) => {
1008
- const isSelected = activeAskAnswers.includes(option);
1009
- const rankLabel = activeAskQuestion.type === "rank_priorities" && isSelected
1010
- ? activeAskAnswers.indexOf(option) + 1
1011
- : null;
1012
- return (_jsx("button", { type: "button", onClick: () => updateAskAnswer(option), className: cx("w-full rounded-lg border px-2.5 py-2 text-left text-[13px] transition-colors", isSelected
1013
- ? "border-[color:color-mix(in_srgb,_var(--brand-primary)_64%,_var(--border-subtle))] bg-[color:color-mix(in_srgb,_var(--brand-primary)_14%,_transparent)] text-[var(--text-primary)]"
1014
- : "border-[var(--border-default)] bg-[var(--bg-canvas)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-subtle)]"), children: _jsxs("span", { className: "inline-flex items-center gap-2", children: [rankLabel ? (_jsx("span", { className: "inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-[var(--brand-primary)] px-1 text-[10px] font-semibold text-[var(--text-on-brand)]", children: rankLabel })) : (_jsx("span", { className: cx("inline-block h-2.5 w-2.5 rounded-full border", isSelected
1015
- ? "border-[var(--brand-primary)] bg-[var(--brand-primary)]"
1016
- : "border-[var(--border-default)] bg-transparent") })), option] }) }, `${option}-${optionIndex}`));
1017
- }) }), (activeAskQuestion.type !== "single_select" || pendingAskUserInput.questions.length > 1) ? (_jsx("div", { className: "flex justify-end", children: _jsx("button", { type: "button", onClick: continueAskQuestions, disabled: !canContinueAsk, className: cx("rounded-md px-2.5 py-1.5 text-[12px] font-medium transition-colors", canContinueAsk
1018
- ? "bg-[var(--brand-primary)] text-[var(--text-on-brand)] hover:bg-[color:color-mix(in_srgb,_var(--brand-primary)_88%,_var(--text-primary))]"
1019
- : "bg-[var(--bg-subtle)] text-[var(--text-tertiary)]"), children: effectiveAskOverlayState.currentQuestionIndex >= pendingAskUserInput.questions.length - 1 ? "Use answers" : "Continue" }) })) : null] })) : (_jsxs("div", { className: "space-y-1.5", children: [controller.pendingFiles.length > 0 ? (_jsx("div", { className: "flex flex-wrap items-center gap-1.5 px-1", children: controller.pendingFiles.map((file) => {
1008
+ }), isConversationBusy && controller.messages.length > 0 && !activeToolBanner && !lastMessageHasContent ? (_jsx(ThinkingIndicator, {})) : null, controller.error ? (_jsx("div", { className: "bg-[color:color-mix(in_srgb,_var(--state-error)_12%,_transparent)] border border-[color:color-mix(in_srgb,_var(--state-error)_48%,_var(--border-subtle))] rounded-lg p-3 text-xs text-[var(--state-error)] flex items-start gap-2.5", children: _jsxs("div", { children: [_jsx("p", { className: "font-medium", children: "Something went wrong" }), _jsx("p", { className: "text-[var(--state-error)] mt-1", children: controller.error })] }) })) : null, (controller.messages.length > 0 || isConversationBusy || !!controller.error) ? (_jsx("div", { "aria-hidden": "true", className: "h-14 shrink-0" })) : null] })] }), _jsxs("div", { className: "relative rounded-2xl border border-[color:color-mix(in_srgb,_var(--border-default)_80%,_transparent)] bg-[var(--bg-surface)] p-2 shadow-[var(--shadow-md)]", children: [planSummary ? (_jsx("div", { className: "absolute bottom-[calc(100%+8px)] left-0 right-0 z-20", children: isPlanHidden ? (_jsxs("button", { type: "button", onClick: () => setIsPlanHidden(false), className: "inline-flex items-center gap-2 rounded-lg border border-[var(--border-default)] bg-[var(--bg-surface)] px-3 py-1.5 text-[11px] font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-subtle)] transition-colors", children: ["Show plan (", planSummary.completedCount, "/", planSummary.steps.length, ")"] })) : (_jsx(PlanSummaryStrip, { plan: planSummary, onHide: () => setIsPlanHidden(true) })) })) : null, isConversationBusy && activeToolBanner ? (_jsx("div", { className: "px-2 pb-1", children: _jsx("div", { className: "inline-flex max-w-full items-center gap-1.5 text-[11px] text-[var(--text-tertiary)] animate-in fade-in duration-200", children: _jsx("span", { className: "truncate", children: activeToolBanner.summary }) }) })) : null, activeAskQuestion && effectiveAskOverlayState && pendingAskUserInput ? (_jsx(AssistantAskOverlay, { questionNumber: effectiveAskOverlayState.currentQuestionIndex + 1, totalQuestions: pendingAskUserInput.questions.length, question: activeAskQuestion.question, options: activeAskQuestion.options, selectedOptions: activeAskAnswers, canContinue: canContinueAsk, continueLabel: effectiveAskOverlayState.currentQuestionIndex >= pendingAskUserInput.questions.length - 1 ? "Use answers" : "Continue", onSelectOption: updateAskAnswer, onContinue: activeAskQuestion.type !== "single_select" || pendingAskUserInput.questions.length > 1 ? continueAskQuestions : undefined, onSkip: () => dismissAskOverlay(effectiveAskOverlayState.toolCallId), mode: activeAskQuestion.type })) : (_jsxs("div", { className: "space-y-1.5", children: [controller.pendingFiles.length > 0 ? (_jsx("div", { className: "flex flex-wrap items-center gap-1.5 px-1", children: controller.pendingFiles.map((file) => {
1020
1009
  const fileKey = `${file.name}:${file.size}:${file.lastModified}`;
1021
1010
  return (_jsx("div", { children: renderPendingFile({
1022
1011
  file,
@@ -13,8 +13,12 @@ export type { UseAssistantRuntimeOptions, UseAssistantRuntimeResult, } from "./u
13
13
  export { useAssistantController } from "./useAssistantController.js";
14
14
  export type { AssistantAction, AssistantConversationScope, AssistantMessagePart, AssistantRenderableMessage, AssistantToolInvocation, UseAssistantControllerOptions, UseAssistantControllerResult, } from "./useAssistantController.js";
15
15
  export type { AssistantConversationRenderArgs, AssistantControllerView, AssistantExperienceCustomizationProps, AssistantMessageRenderArgs, AssistantPendingFileRenderArgs, AssistantPresentedFileRenderArgs, AssistantToolRenderArgs, } from "./components/assistant-types.js";
16
+ export { AssistantAskOverlay, AssistantComposer, AssistantConversationList, AssistantHeader, AssistantMessageViewport, AssistantModelPicker, AssistantPendingFileChip, AssistantShellLayout, AssistantStatusPill, } from "./components/AssistantChrome.js";
17
+ export type { AssistantAskOverlayProps, AssistantComposerProps, AssistantConversationListProps, AssistantHeaderProps, AssistantMessageViewportProps, AssistantModelPickerProps, AssistantPendingFileChipProps, AssistantShellLayoutProps, AssistantStatusPillProps, } from "./components/AssistantChrome.js";
16
18
  export { AssistantExperienceView } from "./components/AssistantExperience.js";
17
19
  export type { ActiveToolBanner, AskUserInputQuestion, AssistantExperienceViewProps, DisplayMessageRow, PendingAskUserInput, PlanStepState, PlanSummaryState, } from "./components/AssistantExperience.js";
20
+ export { AssistantEmbedded } from "./components/AssistantEmbedded.js";
21
+ export type { AssistantEmbeddedProps } from "./components/AssistantEmbedded.js";
18
22
  export { buildDisplayMessageRows, dedupToolInvocations, EmptyState, findPendingAskUserInput, formatAskUserInputAnswers, getActiveToolBanner, extractPresentFilePathsFromInvocation, latestPlanSummary, MessageGroup, PlanSummaryStrip, ThinkingIndicator, } from "./components/AssistantExperience.js";
19
23
  export { useTaskSession } from "./useTaskSession.js";
20
24
  export type { CreateTaskInput, UseTaskSessionOptions, UseTaskSessionResult, } from "./useTaskSession.js";
@@ -5,7 +5,9 @@ export { useAssistantRun } from "./useAssistantRun.js";
5
5
  export { useAssistantSession } from "./useAssistantSession.js";
6
6
  export { useAssistantRuntime } from "./useAssistantRuntime.js";
7
7
  export { useAssistantController } from "./useAssistantController.js";
8
+ export { AssistantAskOverlay, AssistantComposer, AssistantConversationList, AssistantHeader, AssistantMessageViewport, AssistantModelPicker, AssistantPendingFileChip, AssistantShellLayout, AssistantStatusPill, } from "./components/AssistantChrome.js";
8
9
  export { AssistantExperienceView } from "./components/AssistantExperience.js";
10
+ export { AssistantEmbedded } from "./components/AssistantEmbedded.js";
9
11
  export { buildDisplayMessageRows, dedupToolInvocations, EmptyState, findPendingAskUserInput, formatAskUserInputAnswers, getActiveToolBanner, extractPresentFilePathsFromInvocation, latestPlanSummary, MessageGroup, PlanSummaryStrip, ThinkingIndicator, } from "./components/AssistantExperience.js";
10
12
  export { useTaskSession } from "./useTaskSession.js";
11
13
  export { useFunctionSession } from "./useFunctionSession.js";
@@ -708,14 +708,16 @@ export function useAssistantController({ client, podId, assistantId, organizatio
708
708
  }, [conversations]);
709
709
  useEffect(() => {
710
710
  const conversationId = activeConversationIdRef.current;
711
- if (!conversationId)
712
- return;
713
- if (!runtimeMessages || runtimeMessages.length === 0)
711
+ if (!conversationId) {
712
+ setMessages([]);
714
713
  return;
714
+ }
715
715
  const normalized = sortMessagesByCreatedAt(runtimeMessages)
716
- .filter((message) => !message.conversation_id || message.conversation_id === conversationId);
717
- if (normalized.length === 0)
716
+ .filter((message) => message.conversation_id === conversationId);
717
+ if (normalized.length === 0) {
718
+ setMessages([]);
718
719
  return;
720
+ }
719
721
  const nextMessages = mapConversationMessages(normalized);
720
722
  const pendingText = sessionStreamingText.trim();
721
723
  if (pendingText.length > 0) {
@@ -43,8 +43,15 @@ function upsertRuntimeMessage(previous, incoming) {
43
43
  next.push(incoming);
44
44
  return next;
45
45
  }
46
- function toRuntimeMessage(message) {
47
- return message;
46
+ function toRuntimeMessage(message, fallbackConversationId) {
47
+ const runtimeMessage = message;
48
+ if (runtimeMessage.conversation_id || !fallbackConversationId) {
49
+ return runtimeMessage;
50
+ }
51
+ return {
52
+ ...runtimeMessage,
53
+ conversation_id: fallbackConversationId,
54
+ };
48
55
  }
49
56
  function buildOptimisticId() {
50
57
  if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
@@ -56,16 +63,16 @@ export function useAssistantRuntime({ conversationId = null, sessionMessages = [
56
63
  const [runtimeMessages, setRuntimeMessages] = useState([]);
57
64
  const mergeMessages = useCallback((messages) => {
58
65
  setRuntimeMessages((previous) => {
59
- const merged = messages.reduce((accumulator, message) => upsertRuntimeMessage(accumulator, toRuntimeMessage(message)), previous);
66
+ const merged = messages.reduce((accumulator, message) => upsertRuntimeMessage(accumulator, toRuntimeMessage(message, conversationId)), previous);
60
67
  return [...merged].sort((a, b) => messageTime(a) - messageTime(b));
61
68
  });
62
- }, []);
69
+ }, [conversationId]);
63
70
  const replaceLoadedMessages = useCallback((messages) => {
64
- setRuntimeMessages((previous) => {
65
- const next = messages.reduce((accumulator, message) => upsertRuntimeMessage(accumulator, toRuntimeMessage(message)), previous);
66
- return [...next].sort((a, b) => messageTime(a) - messageTime(b));
67
- });
68
- }, []);
71
+ const normalized = messages
72
+ .map((message) => toRuntimeMessage(message, conversationId))
73
+ .filter((message) => !conversationId || message.conversation_id === conversationId);
74
+ setRuntimeMessages([...normalized].sort((a, b) => messageTime(a) - messageTime(b)));
75
+ }, [conversationId]);
69
76
  const appendOptimisticUserMessage = useCallback((content, options) => {
70
77
  const trimmed = content.trim();
71
78
  const optimisticConversationId = options?.conversationId ?? conversationId ?? undefined;
@@ -98,8 +105,8 @@ export function useAssistantRuntime({ conversationId = null, sessionMessages = [
98
105
  if (sessionMessages.length === 0)
99
106
  return;
100
107
  const normalized = sessionMessages
101
- .map((message) => toRuntimeMessage(message))
102
- .filter((message) => !conversationId || !message.conversation_id || message.conversation_id === conversationId);
108
+ .map((message) => toRuntimeMessage(message, conversationId))
109
+ .filter((message) => !conversationId || message.conversation_id === conversationId);
103
110
  if (normalized.length === 0)
104
111
  return;
105
112
  mergeMessages(normalized);
@@ -57,6 +57,7 @@ export function useAssistantSession(options) {
57
57
  const [isStreaming, setIsStreaming] = useState(false);
58
58
  const [error, setError] = useState(null);
59
59
  const abortRef = useRef(null);
60
+ const conversationIdRef = useRef(externalConversationId);
60
61
  const statusRef = useRef(undefined);
61
62
  const streamingTextRef = useRef("");
62
63
  const autoResumedKeyRef = useRef(null);
@@ -65,20 +66,30 @@ export function useAssistantSession(options) {
65
66
  const onMessageRef = useRef(onMessage);
66
67
  const onErrorRef = useRef(onError);
67
68
  const setConversationId = useCallback((nextConversationId) => {
68
- setConversationIdState(nextConversationId);
69
- autoResumedKeyRef.current = null;
70
- streamingTextRef.current = "";
71
- setStreamingText("");
72
- if (!nextConversationId) {
69
+ abortRef.current?.abort();
70
+ abortRef.current = null;
71
+ setConversationIdState((currentConversationId) => {
72
+ if (currentConversationId === nextConversationId) {
73
+ return currentConversationId;
74
+ }
75
+ autoResumedKeyRef.current = null;
76
+ streamingTextRef.current = "";
77
+ setStreamingText("");
73
78
  setConversation(null);
74
79
  setStatus(undefined);
75
80
  statusRef.current = undefined;
76
81
  setMessages([]);
77
- }
82
+ setError(null);
83
+ setIsStreaming(false);
84
+ return nextConversationId;
85
+ });
78
86
  }, []);
79
87
  useEffect(() => {
80
- setConversationIdState(externalConversationId);
81
- }, [externalConversationId]);
88
+ setConversationId(externalConversationId);
89
+ }, [externalConversationId, setConversationId]);
90
+ useEffect(() => {
91
+ conversationIdRef.current = conversationId;
92
+ }, [conversationId]);
82
93
  useEffect(() => {
83
94
  onEventRef.current = onEvent;
84
95
  }, [onEvent]);
@@ -206,6 +217,13 @@ export function useAssistantSession(options) {
206
217
  page_token: input.pageToken,
207
218
  });
208
219
  const nextMessages = response.items ?? [];
220
+ if (conversationIdRef.current !== id) {
221
+ return {
222
+ items: nextMessages,
223
+ limit: response.limit ?? input.limit ?? 20,
224
+ next_page_token: response.next_page_token,
225
+ };
226
+ }
209
227
  setMessages((previous) => nextMessages.reduce((accumulator, message) => upsertConversationMessage(accumulator, message), previous));
210
228
  return {
211
229
  items: nextMessages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemma-sdk",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "Official TypeScript SDK for Lemma pod-scoped APIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",