diff --git a/docs/api-reference/veryfront/chat.md b/docs/api-reference/veryfront/chat.md index acaf3ef1af..c8920027c6 100644 --- a/docs/api-reference/veryfront/chat.md +++ b/docs/api-reference/veryfront/chat.md @@ -82,30 +82,30 @@ Options accepted by use chat. `useChat` result -| Property | Type | Description | Source | -| -------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | -| `messages` | `ChatMessage[]` | All messages in the conversation | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L95) | -| `input` | `string` | Current input value | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L96) | -| `isLoading` | `boolean` | Whether a request is in flight | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L98) | -| `status` | `ChatStatus` | Streaming lifecycle of the current turn (AI-SDK parity). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L100) | -| `streamingMessageId` | `string \| null` | Id of the assistant message currently streaming, or `null` when idle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L102) | -| `error` | `Error \| null` | Last error (if any) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L103) | -| `model` | `string \| undefined` | Current model override (undefined = use agent default) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L105) | -| `activeModel` | `string \| undefined` | The actual model being used after auto-upgrade (e.g. "Anthropic/claude-sonnet-4-20250514") | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L107) | -| `inferenceMode` | `InferenceMode` | Where inference is currently happening | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L109) | -| `setInput` | (input: string) => void | Set input value | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L110) | -| `setModel` | (model: string | undefined) => void | Change the model for subsequent requests | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L112) | -| `sendMessage` | (message: { text: string; files?: ChatFilePart[] }) => Promise<void> | Send a message programmatically | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L113) | -| `editMessage` | (messageId: string, newText: string) => Promise<void> | Edit a user message and resubmit - truncates history to that point | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L115) | -| `getBranches` | (messageId: string) => BranchInfo | Get branch info for a message (returns { current, total }; total=1 if no branches) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L117) | -| `switchBranch` | (messageId: string, branchIndex: number) => void | Switch to a different branch at a given message | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L119) | -| `reload` | () => Promise<void> | Re-send last user message | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L120) | -| `stop` | () => void | Abort current request | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L121) | -| `setMessages` | (messages: ChatMessage[]) => void | Replace message history | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L122) | -| `addToolOutput` | (output: ToolOutput) => void | Submit client-side tool result | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L123) | -| `data?` | `unknown` | Extra data from server response | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L124) | -| `handleInputChange` | (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void | Bind to input onChange | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L125) | -| `handleSubmit` | (e?: React.FormEvent) => Promise<void> | Submit current input | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L126) | +| Property | Type | Description | Source | +| -------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| `messages` | `ChatMessage[]` | All messages in the conversation | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L95) | +| `input` | `string` | Current input value | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L96) | +| `isLoading` | `boolean` | Whether a request is in flight | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L98) | +| `status` | `ChatStatus` | Streaming lifecycle of the current turn (AI-SDK parity). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L100) | +| `streamingMessageId` | `string \| null` | Id of the assistant message currently streaming, or `null` when idle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L102) | +| `error` | `Error \| null` | Last error (if any) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L103) | +| `model` | `string \| undefined` | Current model override (undefined = use agent default) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L105) | +| `activeModel` | `string \| undefined` | The actual model being used after auto-upgrade (e.g. "Anthropic/claude-sonnet-4-20250514") | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L107) | +| `inferenceMode` | `InferenceMode` | Where inference is currently happening | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L109) | +| `setInput` | (input: string) => void | Set input value | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L110) | +| `setModel` | (model: string | undefined) => void | Change the model for subsequent requests | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L112) | +| `sendMessage` | (message: { text: string; files?: ChatFilePart[]; model?: string }) => Promise<void> | Send a message. A message model overrides the session model for this request only. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L114) | +| `editMessage` | (messageId: string, newText: string) => Promise<void> | Edit a user message and resubmit - truncates history to that point | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L116) | +| `getBranches` | (messageId: string) => BranchInfo | Get branch info for a message (returns { current, total }; total=1 if no branches) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L118) | +| `switchBranch` | (messageId: string, branchIndex: number) => void | Switch to a different branch at a given message | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L120) | +| `reload` | () => Promise<void> | Re-send last user message | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L121) | +| `stop` | () => void | Abort current request | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L122) | +| `setMessages` | (messages: ChatMessage[]) => void | Replace message history | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L123) | +| `addToolOutput` | (output: ToolOutput) => void | Submit client-side tool result | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L124) | +| `data?` | `unknown` | Extra data from server response | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L125) | +| `handleInputChange` | (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void | Bind to input onChange | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L126) | +| `handleSubmit` | (e?: React.FormEvent) => Promise<void> | Submit current input | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/types.ts#L127) | ### `UseAgentOptions` @@ -137,79 +137,79 @@ Result returned from use agent. ### Components -| Name | Description | Source | -| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| `AgentAvatar` | Render agent avatar, falling back to model identity when agent identity is absent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/agent-avatar.tsx#L24) | -| `AgentCard` | AgentCard - render `` for the default card, or compose `AgentCard.Header` / `Reasoning` / `Tools` / `Body` for a custom layout. Mirrors the `ToolCall` compound: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/agent-card.tsx#L270) | -| `AgentPicker` | AgentPicker - render `` for the default data-driven combobox, or compose `AgentPicker.Trigger`, `Content`, `Search`, `List`, `Item`, `Create`, and `Manage` for a custom menu. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/agent-picker.tsx#L420) | -| `AppShell` | Compound AppShell. Compose: | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/app-shell.tsx#L628) | -| `AttachmentPill` | AttachmentPill - render `` for the default chip, or compose `AttachmentPill.Root` + `.Thumbnail` / `.Icon` / `.Label` / `.Retry` / `.Remove` for a custom layout. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachment-pill.tsx#L495) | -| `AttachmentsPanel` | AttachmentsPanel - render `` for the default panel, or compose `AttachmentsPanel.Root` + `List` / `Item` / `Empty` / `Action` for a custom layout. Mirrors the `ToolCall` / `Sources` compounds: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L650) | -| `BranchPicker` | Branch picker with addressable previous, count, and next leaves. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/branch-picker.tsx#L192) | -| `Chat` | Render chat components through the preset or its composable sub-parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/chat-preset.tsx#L37) | -| `ChatActions` | ChatActions - render `` for the default preset menu, or compose `ChatActions.Trigger` / `Content` / `Item` (each reads `useChatActions()`) for a custom menu. Mirrors the `ToolCall` compound: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat-actions.tsx#L197) | -| `ChatAgentPicker` | Render the connected agent switcher, or nothing when there's nothing to switch. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat-agent-picker.tsx#L63) | -| `ChatContextProvider` | Render chat context provider. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/chat-context.tsx#L106) | -| `ChatEmpty` | Render the chat empty state. It renders whatever it is given and never hides itself: in a custom layout, gate it on the thread being empty with ` ctx.isEmpty}>`. The `` preset does this for you. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-empty.tsx#L50) | -| `ChatEmptyState` | Compound empty state. Use the namespaced parts to compose the view: `Root`, `Avatar`, `Heading`, `Suggestions`, `Suggestion`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-empty-state.tsx#L169) | -| `ChatIf` | Render chat if. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-if.tsx#L19) | -| `ChatInput` | ChatInput - render `` for the default composer, or compose `ChatInput.Field` + `ChatInput.Send`/`Stop`/`Voice`/`Model`/`Attach`/`Export`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.tsx#L327) | -| `ChatInputAttach` | Attachment `+` control. When attaching files is the only action, `+` opens the file dialog directly. When `onSelectAttachment` is also set it becomes a portalled `+` menu (Studio `PromptForm`'s `PlusMenu`) with "Add photos & files" and "Select document". | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-input-attach.tsx#L30) | -| `ChatInputContextProvider` | Provider for `` context (RFC 2980 name). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/composer-context.tsx#L87) | -| `ChatInputExport` | Download the supplied conversation as Markdown. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.tsx#L92) | -| `ChatInputField` | The multiline text editor. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.tsx#L48) | -| `ChatInputModel` | Model selector - shows when models are configured. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.tsx#L74) | -| `ChatInputRoot` | `ChatInput.Root` - the provider shell for a fully custom composer. Supplies `ChatInputContext` from props and renders your children, so you arrange `ChatInput.Field` + the toolbar sub-parts yourself (like `Message.Root`). The default `` is exactly this Root plus the standard body. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.tsx#L149) | -| `ChatInputSend` | Send button, shown while the composer is idle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-input-actions.tsx#L40) | -| `ChatInputStop` | Stop button, shown while the composer is streaming. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-input-actions.tsx#L80) | -| `ChatInputSubmit` | Switch between send and stop states with one control. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-input-submit.tsx#L22) | -| `ChatInputToolbar` | `ChatInput.Toolbar` - a semantic layout slot for the composer's action row. Group/reorder the action sub-parts (`ChatInput.Attach`/`.Model`/`.Export`/ `.Voice`/`.Send`) inside it without re-implementing the composer. Pure layout: the children read their own `ChatInputContext`, so `` just mirrors the default action-row wrapper classes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.tsx#L123) | -| `ChatInputVoice` | Voice button, shown when the idle composer is empty. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-input-actions.tsx#L118) | -| `ChatMessageList` | Render the default message list or compose its centered `Content` column. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-message-list.tsx#L264) | -| `ChatMessagesSkeleton` | Render the loading skeleton for a chat thread. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/chat-messages-skeleton.tsx#L41) | -| `ChatRoot` | Render chat root. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-root.tsx#L87) | -| `ChatSidebar` | Render a chat sidebar - usable as `` or `…`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/sidebar.tsx#L621) | -| `ChatThemeScope` | Wrap chat primitives in the `[data-vf-ui]` token scope so they're themed. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat-theme-scope.tsx#L30) | -| `CodeBlock` | Render escaped source or delegate to explicit syntax/diagram capabilities. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/code-block.tsx#L270) | -| `CodeSurface` | Render through an explicit extension capability or escaped plain source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/code-block.tsx#L239) | -| `ComposerContextProvider` | Render composer context provider. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/composer-context.tsx#L70) | -| `CONVERSATION_STORAGE_LIMITS` | Explicit resource limits applied on both the read and write paths. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/persistence/conversation-codec.ts#L48) | -| `ConversationEmptyState` | State for conversation empty. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/empty-state.tsx#L71) | -| `ConversationsContextProvider` | Low-level context provider (value supplied by the caller). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/conversations-context.tsx#L45) | -| `ConversationScrollButton` | Render conversation scroll button. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/empty-state.tsx#L116) | -| `ConversationsProvider` | ConversationsProvider - calls `useConversations` once with your `store` / `id` / `onSelect` and shares it via `ConversationsContext`. Declare persistence + router wiring here, once, at the app layout; children read it with `useConversationsContext`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/conversations-context.tsx#L58) | -| `CopyButton` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/code-block.tsx#L191) | -| `DEFAULT_CHAT_STREAM_IDLE_TIMEOUT_MS` | Default value for chat stream idle timeout ms. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L13) | -| `DEFAULT_CHAT_STREAM_TOOL_RUNNING_TIMEOUT_MS` | Default value for chat stream tool running timeout ms. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L15) | -| `DropZoneOverlay` | Drag overlay shown over the composer while files are dragged onto it - the glyph-in-a-circle + "Drop files" from Studio's `PromptForm`. Rendered inside a `relative` card; fills it and blurs the content behind. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/drop-zone.tsx#L18) | -| `ErrorBanner` | Render error banner. Mirrors Studio's inline chat retry banner: an amber `warning` alert with a leading triangle icon and a small filled "Try again" button (not a low-emphasis error pill). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/error-banner.tsx#L38) | -| `FadeIn` | Render fade in. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/animations.tsx#L37) | -| `InferenceBadge` | Render inference badge. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/inference-badge.tsx#L15) | -| `InlineCitation` | Render the default citation or compose its `Trigger` and `Card` parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/inline-citation.tsx#L308) | -| `Loader` | Render loader. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/animations.tsx#L14) | -| `Markdown` | Present Markdown source using an injected rich renderer or the explicit dependency-free plain-source contract. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/markdown.tsx#L175) | -| `Message` | Message - render `` for the default turn, or compose `Message.Root` + `Message.Header`/`Content`/`Actions`/… for a custom layout. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/message.tsx#L775) | -| `MessageActionBar` | Context-free message actions with addressable `Copy`, `Copied`, `Regenerate`, and `Edit` icon leaves. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/message-actions.tsx#L211) | -| `MessageContextProvider` | Render message context provider. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/message-context.tsx#L123) | -| `MessageEditForm` | Render message edit form. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/message-edit-form.tsx#L18) | -| `MessageFeedback` | Message feedback with addressable positive and negative action leaves. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/message-feedback.tsx#L168) | -| `ModelAvatar` | Render model avatar. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/model-avatar.tsx#L70) | -| `ModelSelector` | ModelSelector - render `` for the default data-driven combobox, or compose `ModelSelector.Trigger`, `Content`, `Search`, `List`, and `Item` for a custom menu. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/model-selector.tsx#L446) | -| `QuickActions` | Render quick actions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/quick-actions.tsx#L20) | -| `Reasoning` | Reasoning - render `` for the default disclosure, or compose `Reasoning.Trigger` + `Reasoning.Content` for a custom layout. Mirrors the `Message` / `ToolCall` compounds: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/reasoning.tsx#L233) | -| `RichCodeBlock` | Render rich code block. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/code-block.tsx#L25) | -| `Shimmer` | Render shimmer. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/animations.tsx#L5) | -| `SkillBadge` | Render skill badge. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/skill-badge.tsx#L23) | -| `SourcePill` | Render a single source pill with hover preview and score-color behaviour. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/sources.tsx#L165) | -| `Sources` | Sources - render `` for the default row, or compose `Sources.Root` + `Sources.List` + `Sources.Pill` for a custom layout. Mirrors the `ToolCall` / `Reasoning` compounds: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/sources.tsx#L225) | -| `StepIndicator` | StepIndicator - render `` for the default divider, or compose `StepIndicator.Root` + `.Rule` / `.Label` for a custom layout. Mirrors the `ToolCall` / `Sources` compounds. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/step-indicator.tsx#L141) | -| `Suggestion` | Render suggestion. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/empty-state.tsx#L14) | -| `Suggestions` | Render suggestions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/empty-state.tsx#L49) | -| `Tabs` | Tablist container - delegates selection to the adapter, owns the size + look. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/tabs.tsx#L66) | -| `TabsItem` | Individual tab - renders as a button, or an anchor when `href` is set. The adapter's `tabs.Tab` supplies the `role="tab"` / `aria-selected` / `data-state` mechanics and composes selection onto the caller's `onClick` (caller's runs first, then the tab activates); this skin renders the visual pill and forwards native props/ref. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/tabs.tsx#L108) | -| `TabSwitcher` | Render tab switcher. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/tab-switcher.tsx#L28) | -| `ToolCall` | ToolCall - render `` for the default card. `invoke_agent` calls use a child-agent card by default. Pass children to replace the default, or compose `ToolCall.Trigger` / `Body` / `Input` / `Output` / `Error` for a custom layout. Mirrors the `Message` compound: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/tool-ui.tsx#L913) | -| `ToolStatusBadge` | Render tool status badge. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/tool-ui.tsx#L100) | +| Name | Description | Source | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `AgentAvatar` | Render agent avatar, falling back to model identity when agent identity is absent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/agent-avatar.tsx#L24) | +| `AgentCard` | AgentCard - render `` for the default card, or compose `AgentCard.Header` / `Reasoning` / `Tools` / `Body` for a custom layout. Mirrors the `ToolCall` compound: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/agent-card.tsx#L270) | +| `AgentPicker` | AgentPicker - render `` for the default data-driven combobox, or compose `AgentPicker.Trigger`, `Content`, `Search`, `List`, `Item`, `Create`, and `Manage` for a custom menu. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/agent-picker.tsx#L420) | +| `AppShell` | Compound AppShell. Compose: | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/app-shell.tsx#L628) | +| `AttachmentPill` | AttachmentPill - render `` for the default chip, or compose `AttachmentPill.Root` + `.Thumbnail` / `.Icon` / `.Label` / `.Retry` / `.Remove` for a custom layout. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachment-pill.tsx#L495) | +| `AttachmentsPanel` | AttachmentsPanel - render `` for the default panel, or compose `AttachmentsPanel.Root` + `List` / `Item` / `Empty` / `Action` for a custom layout. Mirrors the `ToolCall` / `Sources` compounds: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L650) | +| `BranchPicker` | Branch picker with addressable previous, count, and next leaves. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/branch-picker.tsx#L192) | +| `Chat` | Render chat components through the preset or its composable sub-parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/chat-preset.tsx#L37) | +| `ChatActions` | ChatActions - render `` for the default preset menu, or compose `ChatActions.Trigger` / `Content` / `Item` (each reads `useChatActions()`) for a custom menu. Mirrors the `ToolCall` compound: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat-actions.tsx#L197) | +| `ChatAgentPicker` | Render the connected agent switcher, or nothing when there's nothing to switch. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat-agent-picker.tsx#L63) | +| `ChatContextProvider` | Render chat context provider. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/chat-context.tsx#L110) | +| `ChatEmpty` | Render the chat empty state. It renders whatever it is given and never hides itself: in a custom layout, gate it on the thread being empty with ` ctx.isEmpty}>`. The `` preset does this for you. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-empty.tsx#L50) | +| `ChatEmptyState` | Compound empty state. Use the namespaced parts to compose the view: `Root`, `Avatar`, `Heading`, `Suggestions`, `Suggestion`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-empty-state.tsx#L169) | +| `ChatIf` | Render chat if. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-if.tsx#L19) | +| `ChatInput` | ChatInput - render `` for the default composer, or compose `ChatInput.Field` + `ChatInput.Send`/`Stop`/`Voice`/`Model`/`Attach`/`Export`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.tsx#L337) | +| `ChatInputAttach` | Attachment `+` control. When attaching files is the only action, `+` opens the file dialog directly. When `onSelectAttachment` is also set it becomes a portalled `+` menu (Studio `PromptForm`'s `PlusMenu`) with "Add photos & files" and "Select document". | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-input-attach.tsx#L30) | +| `ChatInputContextProvider` | Provider for `` context (RFC 2980 name). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/composer-context.tsx#L87) | +| `ChatInputExport` | Download the supplied conversation as Markdown. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.tsx#L92) | +| `ChatInputField` | The multiline text editor. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.tsx#L48) | +| `ChatInputModel` | Model selector - shows when models are configured. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.tsx#L74) | +| `ChatInputRoot` | `ChatInput.Root` - the provider shell for a fully custom composer. Supplies `ChatInputContext` from props and renders your children, so you arrange `ChatInput.Field` + the toolbar sub-parts yourself (like `Message.Root`). The default `` is exactly this Root plus the standard body. Omitted props fall back to the surrounding `ChatContext` (``), so a propless Root wires itself to the shared session; explicit props win. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.tsx#L151) | +| `ChatInputSend` | Send button, shown while the composer is idle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-input-actions.tsx#L40) | +| `ChatInputStop` | Stop button, shown while the composer is streaming. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-input-actions.tsx#L80) | +| `ChatInputSubmit` | Switch between send and stop states with one control. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-input-submit.tsx#L22) | +| `ChatInputToolbar` | `ChatInput.Toolbar` - a semantic layout slot for the composer's action row. Group/reorder the action sub-parts (`ChatInput.Attach`/`.Model`/`.Export`/ `.Voice`/`.Send`) inside it without re-implementing the composer. Pure layout: the children read their own `ChatInputContext`, so `` just mirrors the default action-row wrapper classes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.tsx#L123) | +| `ChatInputVoice` | Voice button, shown when the idle composer is empty. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-input-actions.tsx#L118) | +| `ChatMessageList` | Render the default message list or compose its centered `Content` column. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-message-list.tsx#L264) | +| `ChatMessagesSkeleton` | Render the loading skeleton for a chat thread. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/chat-messages-skeleton.tsx#L41) | +| `ChatRoot` | Render chat root. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-root.tsx#L102) | +| `ChatSidebar` | Render a chat sidebar - usable as `` or `…`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/sidebar.tsx#L621) | +| `ChatThemeScope` | Wrap chat primitives in the `[data-vf-ui]` token scope so they're themed. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat-theme-scope.tsx#L30) | +| `CodeBlock` | Render escaped source or delegate to explicit syntax/diagram capabilities. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/code-block.tsx#L270) | +| `CodeSurface` | Render through an explicit extension capability or escaped plain source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/code-block.tsx#L239) | +| `ComposerContextProvider` | Render composer context provider. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/composer-context.tsx#L70) | +| `CONVERSATION_STORAGE_LIMITS` | Explicit resource limits applied on both the read and write paths. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/persistence/conversation-codec.ts#L48) | +| `ConversationEmptyState` | State for conversation empty. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/empty-state.tsx#L71) | +| `ConversationsContextProvider` | Low-level context provider (value supplied by the caller). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/conversations-context.tsx#L45) | +| `ConversationScrollButton` | Render conversation scroll button. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/empty-state.tsx#L116) | +| `ConversationsProvider` | ConversationsProvider - calls `useConversations` once with your `store` / `id` / `onSelect` and shares it via `ConversationsContext`. Declare persistence + router wiring here, once, at the app layout; children read it with `useConversationsContext`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/conversations-context.tsx#L58) | +| `CopyButton` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/code-block.tsx#L191) | +| `DEFAULT_CHAT_STREAM_IDLE_TIMEOUT_MS` | Default value for chat stream idle timeout ms. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L13) | +| `DEFAULT_CHAT_STREAM_TOOL_RUNNING_TIMEOUT_MS` | Default value for chat stream tool running timeout ms. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L15) | +| `DropZoneOverlay` | Drag overlay shown over the composer while files are dragged onto it - the glyph-in-a-circle + "Drop files" from Studio's `PromptForm`. Rendered inside a `relative` card; fills it and blurs the content behind. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/drop-zone.tsx#L18) | +| `ErrorBanner` | Render error banner. Mirrors Studio's inline chat retry banner: an amber `warning` alert with a leading triangle icon and a small filled "Try again" button (not a low-emphasis error pill). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/error-banner.tsx#L38) | +| `FadeIn` | Render fade in. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/animations.tsx#L37) | +| `InferenceBadge` | Render inference badge. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/inference-badge.tsx#L15) | +| `InlineCitation` | Render the default citation or compose its `Trigger` and `Card` parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/inline-citation.tsx#L308) | +| `Loader` | Render loader. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/animations.tsx#L14) | +| `Markdown` | Present Markdown source using an injected rich renderer or the explicit dependency-free plain-source contract. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/markdown.tsx#L175) | +| `Message` | Message - render `` for the default turn, or compose `Message.Root` + `Message.Header`/`Content`/`Actions`/… for a custom layout. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/message.tsx#L775) | +| `MessageActionBar` | Context-free message actions with addressable `Copy`, `Copied`, `Regenerate`, and `Edit` icon leaves. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/message-actions.tsx#L211) | +| `MessageContextProvider` | Render message context provider. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/message-context.tsx#L123) | +| `MessageEditForm` | Render message edit form. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/message-edit-form.tsx#L18) | +| `MessageFeedback` | Message feedback with addressable positive and negative action leaves. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/message-feedback.tsx#L168) | +| `ModelAvatar` | Render model avatar. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/model-avatar.tsx#L70) | +| `ModelSelector` | ModelSelector - render `` for the default data-driven combobox, or compose `ModelSelector.Trigger`, `Content`, `Search`, `List`, and `Item` for a custom menu. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/model-selector.tsx#L446) | +| `QuickActions` | Render quick actions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/quick-actions.tsx#L20) | +| `Reasoning` | Reasoning - render `` for the default disclosure, or compose `Reasoning.Trigger` + `Reasoning.Content` for a custom layout. Mirrors the `Message` / `ToolCall` compounds: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/reasoning.tsx#L233) | +| `RichCodeBlock` | Render rich code block. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/code-block.tsx#L25) | +| `Shimmer` | Render shimmer. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/animations.tsx#L5) | +| `SkillBadge` | Render skill badge. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/skill-badge.tsx#L23) | +| `SourcePill` | Render a single source pill with hover preview and score-color behaviour. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/sources.tsx#L165) | +| `Sources` | Sources - render `` for the default row, or compose `Sources.Root` + `Sources.List` + `Sources.Pill` for a custom layout. Mirrors the `ToolCall` / `Reasoning` compounds: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/sources.tsx#L225) | +| `StepIndicator` | StepIndicator - render `` for the default divider, or compose `StepIndicator.Root` + `.Rule` / `.Label` for a custom layout. Mirrors the `ToolCall` / `Sources` compounds. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/step-indicator.tsx#L141) | +| `Suggestion` | Render suggestion. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/empty-state.tsx#L14) | +| `Suggestions` | Render suggestions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/empty-state.tsx#L49) | +| `Tabs` | Tablist container - delegates selection to the adapter, owns the size + look. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/tabs.tsx#L66) | +| `TabsItem` | Individual tab - renders as a button, or an anchor when `href` is set. The adapter's `tabs.Tab` supplies the `role="tab"` / `aria-selected` / `data-state` mechanics and composes selection onto the caller's `onClick` (caller's runs first, then the tab activates); this skin renders the visual pill and forwards native props/ref. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/tabs.tsx#L108) | +| `TabSwitcher` | Render tab switcher. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/tab-switcher.tsx#L28) | +| `ToolCall` | ToolCall - render `` for the default card. `invoke_agent` calls use a child-agent card by default. Pass children to replace the default, or compose `ToolCall.Trigger` / `Body` / `Input` / `Output` / `Error` for a custom layout. Mirrors the `Message` compound: render it, or compose it. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/tool-ui.tsx#L913) | +| `ToolStatusBadge` | Render tool status badge. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/tool-ui.tsx#L100) | ### Functions @@ -248,7 +248,7 @@ Result returned from use agent. | `useAgents` | React hook that lists the browser-safe agents a project exposes, via `GET /api/agents`. Companion to `useAgentMetadata` (single agent) - use it to drive an agent switcher, e.g. only rendering a picker when `agents.length > 1`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-agents.ts#L53) | | `useAttachments` | `useAttachments` - the headless state hook for chat attachments: a persistent, cross-conversation registry of uploaded files with the upload / remove / list actions. This is the domain primitive; render any UI on top of it (the `AttachmentsPanel` / `AttachmentPill` components are one skin - bring your own). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-uploads-registry.ts#L353) | | `useChat` | The core chat session hook: manages messages, streaming status, input, submit, regenerate, and branch navigation for a conversation. Powers `` (L1) and is the L3 headless entry point for building a fully custom chat UI. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-chat/use-chat.ts#L132) | -| `useChatContextOptional` | React hook for chat context optional. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/chat-context.tsx#L101) | +| `useChatContextOptional` | React hook for chat context optional. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/chat-context.tsx#L105) | | `useChatErrorHandler` | Handler for use chat error. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/error-boundary.tsx#L92) | | `useChatInput` | L3 headless composer hook. Must be used within a `` / ``. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-chat-input.ts#L155) | | `useChatScroll` | `useChatScroll` is the canonical chat scroll hook (RFC 2980). A superset of `useStickToBottom`: same `scrollRef`/`contentRef`/`isAtBottom`/ `scrollToBottom`, plus `viewportRef`, `scrollToStart`/`scrollToEnd`, `scrollToMessage(id)`, and `getViewportProps()` for headless composition. Backward-compatible. Prefer this name in new code. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/hooks/use-stick-to-bottom.ts#L177) | @@ -348,8 +348,8 @@ Result returned from use agent. | `ChatInputExportProps` | Props accepted by ``. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.types.ts#L121) | | `ChatInputFieldProps` | Props accepted by ``. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.types.ts#L18) | | `ChatInputModelProps` | Props accepted by ``. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.types.ts#L105) | -| `ChatInputProps` | Props accepted by `ChatInput`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.types.ts#L142) | -| `ChatInputRootProps` | Props accepted by ``. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.types.ts#L216) | +| `ChatInputProps` | Props accepted by `ChatInput`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.types.ts#L149) | +| `ChatInputRootProps` | Props accepted by ``. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.types.ts#L223) | | `ChatInputSendProps` | Props accepted by ``. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.types.ts#L77) | | `ChatInputSlottedActionProps` | Literal slotted action contract with an element-specific ref and event. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.types.ts#L55) | | `ChatInputSlottedSubmitProps` | Literal slotted props for the unified `ChatInputSubmit` control. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-composer.types.ts#L97) | @@ -366,7 +366,7 @@ Result returned from use agent. | `ChatMessagesSkeletonProps` | Props accepted by ``. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/chat-messages-skeleton.tsx#L36) | | `ChatProps` | Props accepted by chat. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/chat-props.ts#L34) | | `ChatReasoningPart` | Chat message part that carries reasoning text. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/protocol.ts#L18) | -| `ChatRootProps` | Props accepted by chat root. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-root.tsx#L31) | +| `ChatRootProps` | Props accepted by chat root. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/chat-root.tsx#L39) | | `ChatSidebarComponent` | Compound type - the preset plus its namespaced sub-components. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/sidebar.tsx#L611) | | `ChatSidebarEmptyProps` | Props accepted by `ChatSidebarEmpty`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/sidebar.types.ts#L132) | | `ChatSidebarGroupProps` | Props accepted by `ChatSidebarGroup`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/sidebar.types.ts#L122) | @@ -509,7 +509,7 @@ Result returned from use agent. | `useAttachmentPill` | Read the derived per-pill state provided by `AttachmentPill.Root`. Use it to build a custom pill part; throws if called outside an `AttachmentPill`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachment-pill.tsx#L203) | | `useAttachmentsPanel` | Read the panel state provided by `AttachmentsPanel.Root` (uploads + handlers). Use it to build a custom panel part; throws outside an `AttachmentsPanel`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/attachments-panel.tsx#L85) | | `useChatActions` | Read the current `ChatActions` preset configuration from a composed `ChatActions.*` part. Throws outside `ChatActions` / `ChatActions.Root`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat-actions.tsx#L59) | -| `useChatContext` | Read the enclosing chat's shared state (messages, input, submit/stop, model, attachments, branches, theme). Provided by `` / ``; throws when used outside one. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/chat-context.tsx#L98) | +| `useChatContext` | Read the enclosing chat's shared state (messages, input, submit/stop, model, attachments, branches, theme). Provided by `` / ``; throws when used outside one. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/chat-context.tsx#L102) | | `useChatInputContext` | Read the enclosing `` context; throws outside one. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/composer-context.tsx#L83) | | `useChatInputContextOptional` | Read the enclosing `` context, or null outside one. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/composer-context.tsx#L85) | | `useChatSidebarItem` | Read the enclosing ``'s row state (the conversation summary, active flag, rename availability + `startRename`, `remove`, and the `…` menu open state) from a custom item sub-part. Throws outside a ``. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/sidebar.tsx#L277) | diff --git a/src/agent/react/use-chat/types.ts b/src/agent/react/use-chat/types.ts index f4eaecda69..c8fd477461 100644 --- a/src/agent/react/use-chat/types.ts +++ b/src/agent/react/use-chat/types.ts @@ -110,7 +110,8 @@ export interface UseChatResult { setInput: (input: string) => void; /** Change the model for subsequent requests */ setModel: (model: string | undefined) => void; - sendMessage: (message: { text: string; files?: ChatFilePart[] }) => Promise; + /** Send a message. A message model overrides the session model for this request only. */ + sendMessage: (message: { text: string; files?: ChatFilePart[]; model?: string }) => Promise; /** Edit a user message and resubmit — truncates history to that point */ editMessage: (messageId: string, newText: string) => Promise; /** Get branch info for a message (returns { current, total }; total=1 if no branches) */ diff --git a/src/agent/react/use-chat/use-chat.status.test.tsx b/src/agent/react/use-chat/use-chat.status.test.tsx index 6994445b65..a0615697dd 100644 --- a/src/agent/react/use-chat/use-chat.status.test.tsx +++ b/src/agent/react/use-chat/use-chat.status.test.tsx @@ -192,6 +192,39 @@ describe("react/agent/useChat status lifecycle", () => { } }); + it("uses a per-message model override for the request and response metadata", async () => { + const restoreDom = installDom(); + const originalFetch = globalThis.fetch; + let requestBody: { model?: string } | undefined; + globalThis.fetch = (_input, init) => { + requestBody = JSON.parse(String((init as { body?: unknown } | undefined)?.body)); + return Promise.resolve(sseResponse()); + }; + let latest: UseChatResult | null = null; + + function Capture(): null { + latest = useChat({ api: "/api/ag-ui", model: "session-model" }); + return null; + } + + const root = createRoot(document.getElementById("root")!); + try { + flushSync(() => root.render()); + await latest!.sendMessage({ text: "Hello", model: "flat-model" }); + await settle(); + + assertEquals(requestBody?.model, "flat-model"); + const assistant = latest!.messages.find((message) => message.role === "assistant"); + assert(assistant, "the streamed assistant response should be retained"); + assertEquals(assistant.metadata?.model, "flat-model"); + } finally { + flushSync(() => root.unmount()); + await settle(); + globalThis.fetch = originalFetch; + restoreDom(); + } + }); + it("keeps a successful client tool output persistable", async () => { const restoreDom = installDom(); let latest: UseChatResult | null = null; diff --git a/src/agent/react/use-chat/use-chat.ts b/src/agent/react/use-chat/use-chat.ts index b6d9013b12..5c512944cb 100644 --- a/src/agent/react/use-chat/use-chat.ts +++ b/src/agent/react/use-chat/use-chat.ts @@ -215,6 +215,7 @@ function useChatState(options: UseChatOptions): ResettableUseChatResult { message: { text: string; files?: ChatFilePart[]; + model?: string; baseMessages?: ChatMessage[]; userMessageId?: string; }, @@ -241,6 +242,7 @@ function useChatState(options: UseChatOptions): ResettableUseChatResult { let didError = false; try { const allMessages = [...base, userMessage]; + const requestModel = message.model ?? model; const abortController = new AbortController(); abortControllerRef.current = abortController; @@ -257,7 +259,7 @@ function useChatState(options: UseChatOptions): ResettableUseChatResult { credentials: options.credentials, body: JSON.stringify({ messages: allMessages, - ...(model ? { model } : {}), + ...(requestModel ? { model: requestModel } : {}), ...options.body, }), signal: abortController.signal, @@ -301,7 +303,7 @@ function useChatState(options: UseChatOptions): ResettableUseChatResult { setStreamingMessageId(currentMessageIdRef.current); }; // Mutable local — updated by onData before onMessage/onUpdate use it. - let serverModel: string | undefined = model; + let serverModel: string | undefined = requestModel; setActiveModel((current) => isLatestRequest(requestIdRef.current, requestId) ? undefined : current ); diff --git a/src/react/components/chat/chat/composition/chat-composer.shared-context.test.tsx b/src/react/components/chat/chat/composition/chat-composer.shared-context.test.tsx new file mode 100644 index 0000000000..94171a1c4a --- /dev/null +++ b/src/react/components/chat/chat/composition/chat-composer.shared-context.test.tsx @@ -0,0 +1,787 @@ +/** + * One shared chat context (issue veryfront-issue-inbox#69): `ChatInput.Root` + * (via `useComposerValue`) falls back to the surrounding `ChatContext` when its + * explicit props are omitted, so `` wires the + * composer without re-threading the session. Explicit props always win, and a + * standalone `ChatInput.Root` (own props, no `Chat.Root`) is unchanged. + */ +import { flushSync } from "react-dom"; +import { createRoot, type Root } from "react-dom/client"; +import { renderToString } from "react-dom/server"; +import type { ChangeEvent, FormEvent } from "react"; +import { JSDOM } from "npm:jsdom@28.0.0"; +import { unmountReactRoot } from "#veryfront/react/react-root.test-helpers.ts"; +import { assert, assertEquals, assertStringIncludes } from "#veryfront/testing/assert.ts"; +import { describe, it } from "#veryfront/testing/bdd.ts"; +import type { UseChatResult } from "#veryfront/agent/react"; +import { Chat } from "../chat-preset.tsx"; +import { ChatInput } from "./chat-composer.tsx"; +import { useChatContext } from "../contexts/chat-context.tsx"; +import { useChatInputContext } from "../contexts/composer-context.tsx"; +import type { ChatContextValue } from "../contexts/chat-context.tsx"; + +function makeChat(overrides: Partial = {}): UseChatResult { + return { + messages: [], + input: "", + isLoading: false, + status: "ready", + streamingMessageId: null, + error: null, + model: undefined, + activeModel: undefined, + inferenceMode: "cloud", + setInput: () => {}, + setModel: () => {}, + sendMessage: () => Promise.resolve(), + editMessage: () => Promise.resolve(), + getBranches: () => ({ current: 0, total: 1 }), + switchBranch: () => {}, + reload: () => Promise.resolve(), + stop: () => {}, + setMessages: () => {}, + addToolOutput: () => {}, + handleInputChange: () => {}, + handleSubmit: () => Promise.resolve(), + ...overrides, + }; +} + +function installDomGlobals(dom: JSDOM): () => void { + const window = dom.window; + const previous = { + window: globalThis.window, + document: globalThis.document, + navigator: globalThis.navigator, + self: globalThis.self, + Node: globalThis.Node, + Element: globalThis.Element, + HTMLElement: globalThis.HTMLElement, + KeyboardEvent: globalThis.KeyboardEvent, + MouseEvent: globalThis.MouseEvent, + }; + + Object.assign(globalThis, { + window, + document: window.document, + navigator: window.navigator, + self: window, + Node: window.Node, + Element: window.Element, + HTMLElement: window.HTMLElement, + KeyboardEvent: window.KeyboardEvent, + MouseEvent: window.MouseEvent, + }); + + return () => { + Object.assign(globalThis, previous); + dom.window.close(); + }; +} + +describe("react/components/chat/chat/composition/chat-composer shared context", () => { + it("a propless ChatInput.Root reads the session input from the enclosing Chat.Root", () => { + const html = renderToString( + + + + + + , + ); + + assertStringIncludes( + html, + "draft from session", + "the field must show the shared session input without explicit props", + ); + }); + + it("a propless ChatInput.Submit dispatches through the shared session", async () => { + const dom = new JSDOM( + '
', + { url: "https://example.com/" }, + ); + const restore = installDomGlobals(dom); + let sessionSubmits = 0; + const chat = makeChat({ + input: "ready", + handleSubmit: (_e?: FormEvent) => { + sessionSubmits += 1; + return Promise.resolve(); + }, + }); + let root: Root | undefined; + + try { + const rootElement = document.getElementById("root"); + assert(rootElement, "Expected root element to exist"); + const createdRoot = createRoot(rootElement); + root = createdRoot; + flushSync(() => { + createdRoot.render( + + + + + + , + ); + }); + + const sendButton = document.querySelector('button[aria-label="Send"]'); + assert(sendButton, "Expected the send control to render from shared context"); + flushSync(() => sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + + assertEquals(sessionSubmits, 1, "submit must dispatch through the shared session"); + } finally { + if (root) await unmountReactRoot(root); + restore(); + } + }); + + it("submits context attachments through the shared session", async () => { + const dom = new JSDOM( + '
', + { url: "https://example.com/" }, + ); + const restore = installDomGlobals(dom); + let submitted: Parameters[0] | undefined; + let textOnlySubmits = 0; + const chat = makeChat({ + sendMessage: (message) => { + submitted = message; + return Promise.resolve(); + }, + handleSubmit: () => { + textOnlySubmits += 1; + return Promise.resolve(); + }, + }); + let root: Root | undefined; + + try { + const rootElement = document.getElementById("root"); + assert(rootElement, "Expected root element to exist"); + const createdRoot = createRoot(rootElement); + root = createdRoot; + flushSync(() => { + createdRoot.render( + + + + + + , + ); + }); + + const sendButton = document.querySelector('button[aria-label="Send"]'); + assert(sendButton, "Expected an attachment-only send control to render"); + flushSync(() => sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + + assertEquals(textOnlySubmits, 0, "attachment submission must bypass text-only handleSubmit"); + assertEquals(submitted, { + text: "", + files: [{ + type: "file", + mediaType: "application/pdf", + url: "https://example.com/notes.pdf", + filename: "notes.pdf", + }], + }); + } finally { + if (root) await unmountReactRoot(root); + restore(); + } + }); + + it("honors an explicit attachment cleanup with inherited submit state", () => { + let composer: ReturnType | undefined; + let submitted: Parameters[0] | undefined; + let clearCalls = 0; + function ComposerProbe(): null { + composer = useChatInputContext(); + return null; + } + + renderToString( + { + submitted = message; + return Promise.resolve(); + }, + })} + attachments={[{ + id: "att-1", + name: "notes.pdf", + state: "uploaded", + type: "application/pdf", + url: "https://example.com/notes.pdf", + }]} + > + clearCalls += 1}> + + + , + ); + + assert(composer, "Expected the nested composer context to be available"); + composer.onSubmit({ preventDefault() {} } as FormEvent); + + assertEquals(submitted, { + text: "", + files: [{ + type: "file", + mediaType: "application/pdf", + url: "https://example.com/notes.pdf", + filename: "notes.pdf", + }], + }); + assertEquals(clearCalls, 1); + }); + + it("submits the resolved flat input instead of the session draft", async () => { + const dom = new JSDOM( + '
', + { url: "https://example.com/" }, + ); + const restore = installDomGlobals(dom); + let submitted: Parameters[0] | undefined; + let textOnlySubmits = 0; + let clearedInput: string | undefined; + const chat = makeChat({ + input: "session draft", + sendMessage: (message) => { + submitted = message; + return Promise.resolve(); + }, + handleSubmit: () => { + textOnlySubmits += 1; + return Promise.resolve(); + }, + }); + let root: Root | undefined; + + try { + const rootElement = document.getElementById("root"); + assert(rootElement, "Expected root element to exist"); + const createdRoot = createRoot(rootElement); + root = createdRoot; + flushSync(() => { + createdRoot.render( + clearedInput = value} + > + + + + + , + ); + }); + + const sendButton = document.querySelector('button[aria-label="Send"]'); + assert(sendButton, "Expected the send control to render for the flat draft"); + flushSync(() => sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + + assertEquals(textOnlySubmits, 0, "flat input submission must not use the session draft"); + assertEquals(submitted, { text: "flat draft" }); + assertEquals(clearedInput, ""); + } finally { + if (root) await unmountReactRoot(root); + restore(); + } + }); + + it("submits with the resolved flat model instead of the session model", () => { + let composer: ReturnType | undefined; + let submitted: { text: string; model?: string } | undefined; + let textOnlySubmits = 0; + function ComposerProbe(): null { + composer = useChatInputContext(); + return null; + } + + renderToString( + { + submitted = message; + return Promise.resolve(); + }, + handleSubmit: () => { + textOnlySubmits += 1; + return Promise.resolve(); + }, + })} + model="flat-model" + onModelChange={() => {}} + > + + + + , + ); + + assert(composer, "Expected the nested composer context to be available"); + composer.onSubmit({ preventDefault() {} } as FormEvent); + + assertEquals(textOnlySubmits, 0, "flat model submission must bypass the session submitter"); + assertEquals(submitted, { text: "model-specific draft", model: "flat-model" }); + }); + + it("submits explicit nested composer state instead of the enclosing session state", async () => { + let composer: ReturnType | undefined; + let submitted: Parameters[0] | undefined; + let clearedInput: string | undefined; + const removedAttachments: string[] = []; + function ComposerProbe(): null { + composer = useChatInputContext(); + return null; + } + + renderToString( + { + submitted = message; + return Promise.resolve(); + }, + })} + attachments={[]} + > + clearedInput = value} + attachments={[{ + id: "nested-attachment", + name: "nested.pdf", + state: "uploaded", + type: "application/pdf", + url: "https://example.com/nested.pdf", + }]} + onRemoveAttachment={(id) => removedAttachments.push(id)} + > + + + , + ); + + assert(composer, "Expected the nested composer context to be available"); + composer.onSubmit({ preventDefault() {} } as FormEvent); + + assertEquals(submitted, { + text: "nested draft", + files: [{ + type: "file", + mediaType: "application/pdf", + url: "https://example.com/nested.pdf", + filename: "nested.pdf", + }], + }); + assertEquals(clearedInput, ""); + assertEquals(removedAttachments, ["nested-attachment"]); + }); + + it("routes inherited input changes through an explicit composer setter", () => { + let composer: ReturnType | undefined; + let explicitInput: string | undefined; + let sessionInput: string | undefined; + function ComposerProbe(): null { + composer = useChatInputContext(); + return null; + } + + renderToString( + sessionInput = value })} + > + explicitInput = value} + > + + + , + ); + + assert(composer, "Expected the nested composer context to be available"); + composer.onChange?.({ target: { value: "edited draft" } } as ChangeEvent); + + assertEquals(explicitInput, "edited draft"); + assertEquals(sessionInput, undefined); + }); + + it("preserves an enclosing explicit submit override for nested composer state", () => { + let composer: ReturnType | undefined; + let parentSubmits = 0; + let sessionSends = 0; + function ComposerProbe(): null { + composer = useChatInputContext(); + return null; + } + + renderToString( + { + sessionSends += 1; + return Promise.resolve(); + }, + })} + onSubmit={() => { + parentSubmits += 1; + }} + > + {}}> + + + , + ); + + assert(composer, "Expected the nested composer context to be available"); + composer.onSubmit({ preventDefault() {} } as FormEvent); + + assertEquals(parentSubmits, 1); + assertEquals(sessionSends, 0); + }); + + it("explicit ChatInput.Root props win over the surrounding context values", async () => { + const html = renderToString( + + {}}> + + + , + ); + assertStringIncludes(html, "explicit value", "the explicit input prop wins"); + assert( + !html.includes("context value"), + "the context input must not leak past an explicit prop", + ); + + const dom = new JSDOM( + '
', + { url: "https://example.com/" }, + ); + const restore = installDomGlobals(dom); + let sessionSubmits = 0; + let explicitSubmits = 0; + const chat = makeChat({ + input: "ready", + handleSubmit: () => { + sessionSubmits += 1; + return Promise.resolve(); + }, + }); + let root: Root | undefined; + + try { + const rootElement = document.getElementById("root"); + assert(rootElement, "Expected root element to exist"); + const createdRoot = createRoot(rootElement); + root = createdRoot; + flushSync(() => { + createdRoot.render( + + explicitSubmits += 1}> + + + + , + ); + }); + + const sendButton = document.querySelector('button[aria-label="Send"]'); + assert(sendButton, "Expected the send control to render"); + flushSync(() => sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + + assertEquals(explicitSubmits, 1, "the explicit onSubmit prop wins"); + assertEquals(sessionSubmits, 0, "the session submit must not fire past an explicit prop"); + } finally { + if (root) await unmountReactRoot(root); + restore(); + } + }); + + it("uses the session sender when isLoading false explicitly re-enables submission", () => { + let composer: ReturnType | undefined; + let guardedSubmits = 0; + let sessionSends = 0; + function ComposerProbe(): null { + composer = useChatInputContext(); + return null; + } + + renderToString( + { + guardedSubmits += 1; + return Promise.resolve(); + }, + sendMessage: () => { + sessionSends += 1; + return Promise.resolve(); + }, + })} + > + + + + , + ); + + assert(composer, "Expected the nested composer context to be available"); + assertEquals(composer.isLoading, false); + assertEquals(composer.canSubmit, true); + composer.onSubmit({ preventDefault() {} } as FormEvent); + + assertEquals(sessionSends, 1, "the explicitly enabled composer must use the session sender"); + assertEquals(guardedSubmits, 0, "the inherited loading-guarded submitter must not run"); + }); + + it("a standalone ChatInput.Root with full explicit props behaves exactly as before", async () => { + const html = renderToString( + {}} + onSubmit={() => {}} + models={[{ value: "model-1", label: "Model One" }]} + model="model-1" + onModelChange={() => {}} + attachments={[]} + onRemoveAttachment={() => {}} + stop={() => {}} + onAttach={() => {}} + > + + + , + ); + assertStringIncludes(html, "standalone draft", "standalone composer keeps its explicit input"); + + const dom = new JSDOM( + '
', + { url: "https://example.com/" }, + ); + const restore = installDomGlobals(dom); + let submits = 0; + let root: Root | undefined; + + try { + const rootElement = document.getElementById("root"); + assert(rootElement, "Expected root element to exist"); + const createdRoot = createRoot(rootElement); + root = createdRoot; + flushSync(() => { + createdRoot.render( + {}} + onSubmit={() => submits += 1} + models={[{ value: "model-1", label: "Model One" }]} + model="model-1" + onModelChange={() => {}} + attachments={[]} + onRemoveAttachment={() => {}} + stop={() => {}} + onAttach={() => {}} + > + + + , + ); + }); + + const sendButton = document.querySelector('button[aria-label="Send"]'); + assert(sendButton, "Expected the standalone send control to render"); + flushSync(() => sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + + assertEquals(submits, 1, "standalone submit stays caller-owned"); + } finally { + if (root) await unmountReactRoot(root); + restore(); + } + }); + + it("the batteries ChatInput inherits the session loading state when isLoading is omitted", () => { + const html = renderToString( + + + , + ); + + assertStringIncludes( + html, + 'aria-label="Stop"', + "an omitted isLoading must inherit the streaming session, showing Stop", + ); + assert( + !html.includes('aria-label="Send"'), + "Send must stay hidden while the shared session is streaming", + ); + }); + + it("an explicit isLoading prop still wins over the streaming session", () => { + const html = renderToString( + + + , + ); + + assertStringIncludes(html, 'aria-label="Send"', "the explicit isLoading={false} prop wins"); + assert( + !html.includes('aria-label="Stop"'), + "the session loading state must not leak past an explicit isLoading prop", + ); + }); + + it("the batteries ChatInput renders attachments resolved from the shared context", () => { + const html = renderToString( + {}} + > + + , + ); + + assertStringIncludes( + html, + "notes.pdf", + "a context-resolved attachment must render its pill in the composer body", + ); + assertStringIncludes( + html, + 'aria-label="Remove notes.pdf"', + "the context-resolved onRemoveAttachment must surface the removal control", + ); + }); + + it("restores field focus after selecting an attachment inherited from Chat.Root", async () => { + const dom = new JSDOM( + '
', + { url: "https://example.com/" }, + ); + const restore = installDomGlobals(dom); + let attachCalls = 0; + let root: Root | undefined; + + try { + const rootElement = document.getElementById("root"); + assert(rootElement, "Expected root element to exist"); + const createdRoot = createRoot(rootElement); + root = createdRoot; + flushSync(() => { + createdRoot.render( + attachCalls += 1}> + + , + ); + }); + + const field = document.querySelector("textarea"); + const upload = document.querySelector('input[aria-label="Upload file"]'); + assert(field, "Expected the composer field to render"); + assert(upload, "Expected the inherited attachment picker to render"); + + Object.defineProperties(field, { + attachEvent: { value: () => {} }, + detachEvent: { value: () => {} }, + }); + upload.focus(); + Object.defineProperty(upload, "files", { + configurable: true, + value: { + 0: new dom.window.File(["notes"], "notes.txt", { type: "text/plain" }), + length: 1, + item: (index: number) => index === 0 ? upload.files?.[0] ?? null : null, + }, + }); + flushSync(() => upload.dispatchEvent(new dom.window.Event("change", { bubbles: true }))); + + assertEquals(attachCalls, 1, "the inherited attachment handler must run once"); + assert( + document.activeElement === field, + "attachment selection must restore field focus", + ); + } finally { + if (root) await unmountReactRoot(root); + restore(); + } + }); + + it("an explicit null flat prop overrides the session value instead of inheriting it", () => { + let observed: ChatContextValue | undefined; + function ContextProbe(): null { + observed = useChatContext(); + return null; + } + + renderToString( + + + , + ); + + assert(observed, "Expected the probe to observe the chat context"); + assertEquals(observed.error, null, "an explicit error={null} must clear the session error"); + assertEquals( + observed.streamingMessageId, + null, + "an explicit streamingMessageId={null} must clear the session streaming id", + ); + }); + + it("an omitted flat prop still inherits the session error and streaming id", () => { + let observed: ChatContextValue | undefined; + function ContextProbe(): null { + observed = useChatContext(); + return null; + } + + const sessionError = new Error("session failure"); + renderToString( + + + , + ); + + assert(observed, "Expected the probe to observe the chat context"); + assertEquals(observed.error, sessionError, "an omitted error prop inherits the session error"); + assertEquals( + observed.streamingMessageId, + "msg-1", + "an omitted streamingMessageId prop inherits the session streaming id", + ); + }); +}); diff --git a/src/react/components/chat/chat/composition/chat-composer.tsx b/src/react/components/chat/chat/composition/chat-composer.tsx index 80b6a73ae0..ae6b21f80c 100644 --- a/src/react/components/chat/chat/composition/chat-composer.tsx +++ b/src/react/components/chat/chat/composition/chat-composer.tsx @@ -144,7 +144,9 @@ ChatInputToolbar.displayName = "ChatInput.Toolbar"; * `ChatInput.Root` — the provider shell for a fully custom composer. Supplies * `ChatInputContext` from props and renders your children, so you arrange * `ChatInput.Field` + the toolbar sub-parts yourself (like `Message.Root`). The - * default `` is exactly this Root plus the standard body. + * default `` is exactly this Root plus the standard body. Omitted + * props fall back to the surrounding `ChatContext` (``), + * so a propless Root wires itself to the shared session; explicit props win. */ export function ChatInputRoot( { className, children, ref, ...state }: ChatInputRootProps, @@ -152,7 +154,7 @@ export function ChatInputRoot( const baseCtxValue = useComposerValue(state); const { contextValue, fileInput } = useChatInputAttachmentPicker( baseCtxValue, - state.onAttach, + baseCtxValue.onAttach, state.attachAccept, ); return ( @@ -173,7 +175,10 @@ function ChatInputBase( onChange, setInput, onSubmit, - isLoading = false, + // No `= false` default: an omitted `isLoading` must stay `undefined` so + // `useComposerValue` can inherit the surrounding session's loading state + // (a default would always win over the context and hide Stop mid-turn). + isLoading, placeholder = "Type a message...", theme, stop, @@ -215,13 +220,6 @@ function ChatInputBase( ); const handleAttach = withFocus(onAttach); - const { - isDragActive, - onDragEnter, - onDragLeave, - onDragOver, - onDrop: onFileDrop, - } = useDropZone(withFocus(onDrop ?? onAttach)); const baseCtxValue = useComposerValue({ input, onChange, @@ -241,9 +239,21 @@ function ChatInputBase( attachments, onRemoveAttachment, }); + // Attach + drop fall back to the context-resolved handler (like + // `ChatInput.Root`), so a propless composer inside `` + // keeps a working picker and drop zone. Both explicit and inherited + // handlers restore field focus after a selection. + const resolvedAttach = handleAttach ?? withFocus(baseCtxValue.onAttach); + const { + isDragActive, + onDragEnter, + onDragLeave, + onDragOver, + onDrop: onFileDrop, + } = useDropZone(withFocus(onDrop) ?? resolvedAttach); const { contextValue, fileInput } = useChatInputAttachmentPicker( baseCtxValue, - handleAttach, + resolvedAttach, attachAccept, ); @@ -276,13 +286,13 @@ function ChatInputBase( /* Pending attachments — inside the composer card, above the editor (Studio PromptForm), not floating as a separate row. */ } - {attachments && attachments.length > 0 && ( + {contextValue.attachments.length > 0 && (
- {attachments.map((file) => ( + {contextValue.attachments.map((file) => ( ))} diff --git a/src/react/components/chat/chat/composition/chat-composer.types.ts b/src/react/components/chat/chat/composition/chat-composer.types.ts index 1e42f59a30..1213c580f0 100644 --- a/src/react/components/chat/chat/composition/chat-composer.types.ts +++ b/src/react/components/chat/chat/composition/chat-composer.types.ts @@ -138,12 +138,19 @@ export interface ChatInputToolbarProps extends React.HTMLAttributes; } -/** Props accepted by `ChatInput`. */ +/** + * Props accepted by `ChatInput`. + * + * Every prop is optional: omitted state falls back to the surrounding + * `ChatContext` (``), so a propless `` wires + * itself to the shared session. Explicit props always win, and a standalone + * composer outside a `` still supplies its own `input`/`onChange`. + */ export interface ChatInputProps { - /** Current text value of the composer input (controlled). */ - input: string; - /** Fired as the user edits the input. */ - onChange: (e: React.ChangeEvent) => void; + /** Current text value of the composer input (controlled). Falls back to `ChatContext.input`. */ + input?: string; + /** Fired as the user edits the input. Falls back to `ChatContext.setInput`. */ + onChange?: (e: React.ChangeEvent) => void; /** * Update the controlled input value for headless consumers. The preset wires * this automatically. Direct consumers must provide it before calling diff --git a/src/react/components/chat/chat/composition/chat-root.tsx b/src/react/components/chat/chat/composition/chat-root.tsx index 65fb2216cf..ac43bbb734 100644 --- a/src/react/components/chat/chat/composition/chat-root.tsx +++ b/src/react/components/chat/chat/composition/chat-root.tsx @@ -9,7 +9,7 @@ import * as React from "react"; import { ChatContainer } from "#veryfront/react/primitives/index.ts"; -import type { ChatMessage, ChatStatus } from "#veryfront/agent/react"; +import type { ChatMessage, ChatStatus, UseChatResult } from "#veryfront/agent/react"; import type { ChatTheme } from "../../theme.ts"; import { useDocumentNonce } from "../../../ui/csp-nonce.ts"; import { @@ -26,16 +26,31 @@ import type { Source } from "../components/sources.tsx"; import type { BranchInfo } from "#veryfront/agent/react"; import { ChatContextProvider } from "../contexts/chat-context.tsx"; import type { ChatContextValue } from "../contexts/chat-context.tsx"; +import { attachmentsToFileParts, hasPendingAttachments } from "../chat-attachments.ts"; -/** Props accepted by chat root. */ +/** + * Props accepted by chat root. + * + * Supply either `chat` (a `useChat()` session) or the flat props + * (`messages`, `input`, `onSubmit`, …). Both are optional so the two modes can + * mix, but a `` given neither renders an empty chat whose + * `setInput`/`onSubmit` are inert no-ops. + */ export interface ChatRootProps extends Omit, "children"> { children: React.ReactNode; /** React 19: ref is a regular prop. */ ref?: React.Ref; + /** + * Drive the chat surface from a `useChat()` session you own: + * ``. Folds the session state into the shared + * context; the flat props below stay as an explicit override path. + */ + chat?: UseChatResult; + // Messages - messages: ChatMessage[]; + messages?: ChatMessage[]; isLoading?: boolean; /** Streaming lifecycle of the current turn (`useChat().status`). */ status?: ChatStatus; @@ -44,7 +59,7 @@ export interface ChatRootProps extends Omit error?: Error | null; // Input - input: string; + input?: string; setInput?: (value: string) => void; // Submit / Stop @@ -87,26 +102,27 @@ export interface ChatRootProps extends Omit export function ChatRoot( { children, - messages, - isLoading = false, - status, - streamingMessageId, - error = null, - input, - setInput, - onSubmit, - onStop, - onReload, - model, + chat, + messages: messagesProp, + isLoading: isLoadingProp, + status: statusProp, + streamingMessageId: streamingMessageIdProp, + error: errorProp, + input: inputProp, + setInput: setInputProp, + onSubmit: onSubmitProp, + onStop: onStopProp, + onReload: onReloadProp, + model: modelProp, models = [], - onModelChange, + onModelChange: onModelChangeProp, agent, attachments = [], onAttach, onRemoveAttachment, - editMessage, - getBranches, - switchBranch, + editMessage: editMessageProp, + getBranches: getBranchesProp, + switchBranch: switchBranchProp, onFeedback, onSourceClick, theme: userTheme, @@ -117,6 +133,63 @@ export function ChatRoot( ...containerProps }: ChatRootProps, ): React.ReactElement { + // `chat` folds the session's flat state into the context; each explicit flat + // prop wins over the session value (issue #69's override path). + const messages = messagesProp ?? chat?.messages ?? []; + const isLoading = isLoadingProp ?? chat?.isLoading ?? false; + const status = statusProp ?? chat?.status; + // Nullable props compare against `undefined`, not nullish: `error={null}` and + // `streamingMessageId={null}` are explicit overrides that clear the session + // value, so `??` would wrongly restore it from `chat`. + const streamingMessageId = streamingMessageIdProp !== undefined + ? streamingMessageIdProp + : chat?.streamingMessageId; + const error = errorProp !== undefined ? errorProp : (chat?.error ?? null); + const input = inputProp ?? chat?.input ?? ""; + const setInput = setInputProp ?? chat?.setInput; + const model = modelProp ?? chat?.model; + const hasFlatSubmitState = inputProp !== undefined || setInputProp !== undefined || + isLoadingProp !== undefined || modelProp !== undefined; + const submitSession = React.useCallback((e?: React.FormEvent) => { + if (!chat) return; + if (hasPendingAttachments(attachments)) { + e?.preventDefault(); + return; + } + const files = attachmentsToFileParts(attachments); + if (files.length === 0 && !hasFlatSubmitState) return chat.handleSubmit(e); + + e?.preventDefault(); + if (isLoading) return; + const text = input.trim(); + if (!text && files.length === 0) return; + + setInput?.(""); + for (const attachment of attachments) { + if (attachment.url) onRemoveAttachment?.(attachment.id); + } + return chat.sendMessage({ + text, + ...(files.length > 0 ? { files } : {}), + ...(model !== undefined ? { model } : {}), + }); + }, [ + attachments, + chat, + hasFlatSubmitState, + input, + isLoading, + model, + onRemoveAttachment, + setInput, + ]); + const onSubmit = onSubmitProp ?? (chat ? submitSession : undefined); + const onStop = onStopProp ?? chat?.stop; + const onReload = onReloadProp ?? chat?.reload; + const onModelChange = onModelChangeProp ?? chat?.setModel; + const editMessage = editMessageProp ?? chat?.editMessage; + const getBranches = getBranchesProp ?? chat?.getBranches; + const switchBranch = switchBranchProp ?? chat?.switchBranch; const theme = React.useMemo(() => mergeThemes(defaultChatTheme, userTheme), [userTheme]); const nonce = useDocumentNonce(); const tokenCSS = React.useMemo(() => generateTokenCSS(), []); @@ -140,6 +213,7 @@ export function ChatRoot( input, setInput: setInput ?? (() => {}), onSubmit: onSubmit ?? (() => {}), + sendMessage: onSubmitProp === undefined ? chat?.sendMessage : undefined, onStop, onReload, model, @@ -168,6 +242,8 @@ export function ChatRoot( input, setInput, onSubmit, + chat?.sendMessage, + onSubmitProp, onStop, onReload, model, diff --git a/src/react/components/chat/chat/composition/use-composer-value.ts b/src/react/components/chat/chat/composition/use-composer-value.ts index 23877dfb9c..8db84820af 100644 --- a/src/react/components/chat/chat/composition/use-composer-value.ts +++ b/src/react/components/chat/chat/composition/use-composer-value.ts @@ -7,6 +7,7 @@ import * as React from "react"; import type { ChatFilePart } from "#veryfront/agent/react"; +import { useChatContextOptional } from "../contexts/chat-context.tsx"; import type { ChatInputContextValue } from "../contexts/composer-context.tsx"; import type { ModelOption } from "../../model-selector.tsx"; import type { AttachmentInfo } from "../components/attachment-pill.tsx"; @@ -14,8 +15,10 @@ import { attachmentsToFileParts, hasPendingAttachments } from "../chat-attachmen /** State shared by controlled and composer-owned submit modes. */ interface ComposerStateBaseProps { - input: string; - onChange: ( + /** Falls back to the surrounding `ChatContext` input when omitted. */ + input?: string; + /** Falls back to `ChatContext.setInput` when omitted. */ + onChange?: ( e: React.ChangeEvent, ) => void; /** Clear pending attachments after a composer-owned submit sends. */ @@ -51,7 +54,7 @@ interface ComposerSubmitProps { * Send directly through composer-owned submission. When supplied, `setInput` * clears the controlled input after this handler runs. */ - sendMessage?: (message: { text: string; files?: ChatFilePart[] }) => void; + sendMessage?: (message: { text: string; files?: ChatFilePart[]; model?: string }) => void; /** Update the controlled input value for headless context consumers. */ setInput?: (value: string) => void; } @@ -69,7 +72,39 @@ function missingSetInput(): never { ); } -export function useComposerValue(p: ComposerStateProps): ChatInputContextValue { +export function useComposerValue(props: ComposerStateProps): ChatInputContextValue { + // One shared chat context (issue #69): when a `` is above, + // omitted props fall back to its `ChatContext` (the shared session), so a + // propless `` wires itself. Explicit props always win, and a + // standalone composer (no ChatContext) keeps the props-only behavior. + const chat = useChatContextOptional(); + const resolvedSetInput = props.setInput ?? chat?.setInput; + const fallbackOnChange = React.useCallback( + (e: React.ChangeEvent) => + resolvedSetInput?.(e.target.value), + [resolvedSetInput], + ); + const hasExplicitSubmitState = props.input !== undefined || + props.setInput !== undefined || props.attachments !== undefined || + props.onRemoveAttachment !== undefined || props.onClearAttachments !== undefined || + props.isLoading !== undefined || props.model !== undefined; + const p = { + ...props, + input: props.input ?? chat?.input ?? "", + onChange: props.onChange ?? fallbackOnChange, + setInput: resolvedSetInput, + onSubmit: props.onSubmit ?? chat?.onSubmit, + sendMessage: props.sendMessage ?? + (props.onSubmit === undefined && hasExplicitSubmitState ? chat?.sendMessage : undefined), + isLoading: props.isLoading ?? chat?.isLoading, + stop: props.stop ?? chat?.onStop, + model: props.model ?? chat?.model, + models: props.models ?? chat?.models, + onModelChange: props.onModelChange ?? chat?.onModelChange, + attachments: props.attachments ?? chat?.attachments, + onAttach: props.onAttach ?? chat?.onAttach, + onRemoveAttachment: props.onRemoveAttachment ?? chat?.onRemoveAttachment, + }; const hasResolvedAttachment = p.attachments?.some((attachment) => Boolean(attachment.url) && attachment.state !== "uploading" && @@ -83,7 +118,7 @@ export function useComposerValue(p: ComposerStateProps): ChatInputContextValue { // When `sendMessage` is supplied the composer owns submit: trim, wait for // in-flight uploads, fold resolved attachments into file parts, send, clear. // Otherwise fall back to the caller's explicit `onSubmit` (controlled mode). - const { sendMessage, setInput, onClearAttachments, onSubmit } = p; + const { sendMessage, setInput, onClearAttachments, onSubmit, onRemoveAttachment } = p; const onSubmitEffective = React.useCallback((e?: React.FormEvent) => { if (!sendMessage) { onSubmit?.(e); @@ -95,10 +130,30 @@ export function useComposerValue(p: ComposerStateProps): ChatInputContextValue { const text = p.input.trim(); const files = attachmentsToFileParts(attachments); if (!text && files.length === 0) return; - sendMessage({ text, ...(files.length > 0 ? { files } : {}) }); + sendMessage({ + text, + ...(files.length > 0 ? { files } : {}), + ...(p.model !== undefined ? { model: p.model } : {}), + }); setInput?.(""); - onClearAttachments?.(); - }, [canSubmit, sendMessage, onSubmit, setInput, onClearAttachments, p.input, p.attachments]); + if (onClearAttachments) { + onClearAttachments(); + } else { + for (const attachment of attachments) { + if (attachment.url) onRemoveAttachment?.(attachment.id); + } + } + }, [ + canSubmit, + sendMessage, + onSubmit, + setInput, + onClearAttachments, + onRemoveAttachment, + p.input, + p.attachments, + p.model, + ]); return React.useMemo(() => ({ input: p.input, diff --git a/src/react/components/chat/chat/contexts/chat-context.tsx b/src/react/components/chat/chat/contexts/chat-context.tsx index 2860e14549..d1b01467a1 100644 --- a/src/react/components/chat/chat/contexts/chat-context.tsx +++ b/src/react/components/chat/chat/contexts/chat-context.tsx @@ -8,7 +8,7 @@ import * as React from "react"; import { createStrictContext } from "../../../create-strict-context.ts"; -import type { ChatMessage, ChatStatus } from "#veryfront/agent/react"; +import type { ChatFilePart, ChatMessage, ChatStatus } from "#veryfront/agent/react"; import type { ChatTheme } from "../../theme.ts"; import type { ModelOption } from "../../model-selector.tsx"; import type { AttachmentInfo } from "../components/attachment-pill.tsx"; @@ -37,6 +37,10 @@ export interface ChatContextValue { // Submit / Stop onSubmit: (e?: React.FormEvent) => void | Promise; + /** Send resolved composer text, attachments, and an optional request model through the session. */ + sendMessage?: (message: { text: string; files?: ChatFilePart[]; model?: string }) => + | void + | Promise; onStop?: () => void; onReload?: () => void; diff --git a/src/server/handlers/dev/framework-candidates.generated.ts b/src/server/handlers/dev/framework-candidates.generated.ts index 49894794c2..7bc028d4e3 100644 --- a/src/server/handlers/dev/framework-candidates.generated.ts +++ b/src/server/handlers/dev/framework-candidates.generated.ts @@ -86,6 +86,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "!cancelled", "!candidate.matches(", "!cardVisible", + "!chat)", "!child)", "!childTool)", "!complete", @@ -175,6 +176,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "!h-auto", "!hasError", "!hasFinalText)", + "!hasFlatSubmitState)", "!hasInput", "!hasLegacyButtonSemantics", "!hasOutput)", @@ -1047,6 +1049,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "64px).", "65,536.", "65_536,", + "69's", + "69):", "6;", "6FB57C", "6v14a2", @@ -2226,7 +2230,10 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "ChatContainerProps", "ChatContainerProps>(", "ChatContext", + "ChatContext)", "ChatContext.Provider;", + "ChatContext.input", + "ChatContext.setInput", "ChatContextProvider", "ChatContextProvider,", "ChatContextProvider>", @@ -2281,6 +2288,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "ChatFilePart", "ChatFilePart,", "ChatFilePart[]", + "ChatFilePart[];", "ChatGPT).", "ChatIdleView", "ChatIf", @@ -3715,6 +3723,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "Focus-trapped", "FocusEvent)", "Fold", + "Folds", "Follow", "Follows", "Footer", @@ -4835,6 +4844,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "Notifications", "Notified", "November", + "Nullable", "Number", "Number(declaredLength);", "Number(draft.trim());", @@ -4948,6 +4958,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "Omit,", "Omit", + "chat={", "chat={chat}", "chat={props.chat}", "chat={useChat()}", "chat={useChat()}>", "chat?.agent?.avatarUrl", "chat?.agent?.name", + "chat?.attachments,", "chat?.editMessage;", + "chat?.error", "chat?.getBranches;", + "chat?.input", + "chat?.isLoading", + "chat?.isLoading,", + "chat?.messages", + "chat?.model,", + "chat?.model;", + "chat?.models,", + "chat?.onAttach,", "chat?.onFeedback;", + "chat?.onModelChange,", "chat?.onReload;", + "chat?.onRemoveAttachment,", "chat?.onSourceClick}", + "chat?.onStop,", + "chat?.onSubmit,", + "chat?.reload;", + "chat?.sendMessage", + "chat?.sendMessage,", + "chat?.setInput;", + "chat?.setModel;", "chat?.status", "chat?.status,", + "chat?.status;", + "chat?.stop;", "chat?.streamingMessageId", + "chat?.streamingMessageId;", "chat?.switchBranch;", "chat?.theme.message?.assistant", "chat?.theme.message?.user", @@ -9985,6 +10020,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "common", "compact", "companion", + "compare", "compat", "compatibility", "compatibility.", @@ -10289,6 +10325,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "context-bound", "context-free", "context-menu.tsx", + "context-resolved", "context.", "context.activeConversation", "context.activeConversation.id", @@ -10373,6 +10410,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "contextOwnsId,", "contextValue", "contextValue,", + "contextValue.attachments.length", + "contextValue.attachments.map((file)", "contextValue:", "contextmenu", "contexts", @@ -11818,6 +11857,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "editMessage:", "editMessage={editMessage}", "editMessage?:", + "editMessageProp", + "editMessageProp,", "editValue.trim();", "edited.", "editing)", @@ -12098,6 +12139,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "error:", "error;", "error={error}", + "error={null}", "error?:", "errorId", "errorId,", @@ -12110,6 +12152,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "errorPresent", "errorPresent,", "errorPresent:", + "errorProp", + "errorProp,", "errorText", "errorText,", "errorText:", @@ -12389,6 +12433,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "fallbackHeadingId", "fallbackIds,", "fallbackIds:", + "fallbackOnChange", + "fallbackOnChange,", "fallbackResponse", "fallbackResponse;", "fallbacks", @@ -12702,6 +12748,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "focuses", "focusin", "fold", + "folds", "follow", "following,", "follows", @@ -12885,6 +12932,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "getBranches={getBranches}", "getBranches?.(message.id)", "getBranches?:", + "getBranchesProp", + "getBranchesProp,", "getChatInputActionType(", "getChatInputActionType,", "getChatTokensCSS", @@ -13255,7 +13304,10 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "hasError", "hasError,", "hasError:", + "hasExplicitSubmitState", "hasFinalText", + "hasFlatSubmitState", + "hasFlatSubmitState,", "hasGroups", "hasHeading", "hasInput,", @@ -13272,6 +13324,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "hasOwn(value,", "hasPendingAttachment", "hasPendingAttachments", + "hasPendingAttachments(attachments))", "hasPendingAttachments(items:", "hasPendingAttachments(p.attachments", "hasPendingAttachments(submittedAttachments))", @@ -13806,6 +13859,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "input.getSubmitProps()}>Send", "input.name", "input.trim(),", + "input.trim();", "input.tsx", "input.type", "input/button", @@ -13826,6 +13880,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "inputMode,", "inputMode=", "inputMode={resolvedInputMode}", + "inputProp", + "inputProp,", "inputProps", "inputProps}", "inputRef", @@ -14103,11 +14159,14 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "isListening?:", "isLoaded", "isLoading", + "isLoading)", "isLoading,", "isLoading:", "isLoading={Boolean(isLoading)}", "isLoading={isLoading}", "isLoading?:", + "isLoadingProp", + "isLoadingProp,", "isMessageCreatedAtPath(path:", "isMessageStreaming)", "isMessageStreaming,", @@ -14199,6 +14258,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "isn't", "isolated", "isolation", + "issue", "issued", "it", "it's", @@ -15333,6 +15393,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "messages:", "messages={messages}", "messages?:", + "messagesProp", + "messagesProp,", "messages[messages.length", "metadata", "metadata.", @@ -15354,6 +15416,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "microtask", "mid-load:", "mid-stream.", + "mid-turn).", "midnight.", "migration", "migration.", @@ -15414,6 +15477,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "mistake", "mistake:", "mistaken", + "mix,", "ml-0.5", "ml-auto", "mobile", @@ -15487,6 +15551,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "model?.value.split(", "model?:", "modelId:", + "modelProp", + "modelProp,", "modelValue:", "modeled", "modelled", @@ -15508,6 +15574,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "models?:", "models[0];", "model}", + "modes", "modes.", "modified:", "module", @@ -15782,6 +15849,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "no-explicit-any", "no-files", "no-op", + "no-ops.", "no-persistence)", "no-referrer", "no-state)", @@ -15915,6 +15983,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "null>;", "null>>", "null>>;", + "nullish:", "null}", "num", "num(u.inputTokens),", @@ -16011,7 +16080,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "onAttach", "onAttach(e.target.files);", "onAttach(event.target.files);", - "onAttach));", "onAttach,", "onAttach:", "onAttach={effectiveOnAttach}", @@ -16087,8 +16155,9 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "onCitationClick:", "onCitationClick?.(index);", "onCitationClick?:", + "onClearAttachments();", + "onClearAttachments)", "onClearAttachments,", - "onClearAttachments?.();", "onClearAttachments?:", "onClick", "onClick(e,", @@ -16289,10 +16358,13 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "onManage?.();", "onManage?:", "onManage]);", + "onModelChange", "onModelChange,", "onModelChange:", "onModelChange={onModelChange}", "onModelChange?:", + "onModelChangeProp", + "onModelChangeProp,", "onMouseDown,", "onMouseDown={(event)", "onMouseDown?.(event);", @@ -16382,11 +16454,14 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "onRegenerate],", "onReload", "onReload,", + "onReload:", "onReload={reload}", "onReload?:", + "onReloadProp", + "onReloadProp,", "onRemove", "onRemove,", - "onRemove={onRemoveAttachment}", + "onRemove={contextValue.onRemoveAttachment}", "onRemove?.(attachment.id)}", "onRemove?:", "onRemoveAttachment", @@ -16394,6 +16469,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "onRemoveAttachment:", "onRemoveAttachment={effectiveOnRemove}", "onRemoveAttachment={manageAttachments", + "onRemoveAttachment?.(attachment.id);", "onRemoveAttachment?:", "onRemoveUpload", "onRemoveUpload(attachment.id)}", @@ -16453,11 +16529,14 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "onSourceClick={onSourceClick", "onSourceClick={onSourceClick}", "onSourceClick?:", + "onStop", "onStop();", "onStop,", "onStop:", "onStop={stop}", "onStop?:", + "onStopProp", + "onStopProp,", "onSubmit", "onSubmit();", "onSubmit()}", @@ -16474,6 +16553,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "onSubmit?:", "onSubmitEffective", "onSubmitEffective,", + "onSubmitProp", + "onSubmitProp,", "onSuggestionClick", "onSuggestionClick,", "onSuggestionClick={onSuggestionClick}", @@ -16669,6 +16750,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "optionLabels]);", "optional", "optional.", + "optional:", "optionalBoolean(record,", "optionalBoolean(record:", "optionalBoolean(value,", @@ -16913,7 +16995,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "p.attachments", "p.attachments,", "p.attachments?.some((attachment)", - "p.attachments]);", "p.input,", "p.input.trim().length", "p.input.trim();", @@ -16921,6 +17002,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "p.isListening,", "p.isLoading", "p.isLoading,", + "p.model", "p.model,", "p.models", "p.models,", @@ -17132,6 +17214,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "patched", "path", "path))", + "path).", "path);", "path,", "path.", @@ -17655,14 +17738,17 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "propertyPath),", "propertyPath);", "propertyPath,", + "propless", "props", "props)", "props**", "props,", "props-based", + "props-only", "props.", "props.activeId", "props.asChild", + "props.attachments", "props.chat", "props.children))", "props.children.props.id;", @@ -17670,13 +17756,26 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "props.conversations", "props.id", "props.id;", + "props.input", + "props.isLoading", + "props.model", + "props.models", + "props.onAttach", + "props.onChange", + "props.onClearAttachments", "props.onDelete", + "props.onModelChange", "props.onNew", + "props.onRemoveAttachment", "props.onRename", "props.onSelect", + "props.onSubmit", "props.ref", "props.renderItem;", + "props.sendMessage", + "props.setInput", "props.skeleton", + "props.stop", "props/ref.", "props:", "props;", @@ -18675,6 +18774,9 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "resolvedAnchor", "resolvedAnchor,", "resolvedAnchor;", + "resolvedAttach", + "resolvedAttach);", + "resolvedAttach,", "resolvedContentId", "resolvedContentId]);", "resolvedDescribedBy", @@ -18692,6 +18794,9 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "resolvedMode:", "resolvedMode;", "resolvedMode={resolvedMode}", + "resolvedSetInput", + "resolvedSetInput,", + "resolvedSetInput?.(e.target.value),", "resolvedStreaming", "resolvedStreaming,", "resolved[key]", @@ -19381,8 +19486,11 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "serverNonce;", "session", "session's", + "session)", + "session),", "session,", "session.", + "session;", "sessionChat,", "sessionChat:", "sessionKey", @@ -19520,6 +19628,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "setInput={setValue}", "setInput?.(", "setInput?:", + "setInputProp", + "setInputProp,", "setInstructionsOpen(phase", "setInstructionsOpen]", "setInternal((current)", @@ -20239,7 +20349,6 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "state.maxBytes", "state.maxBytes}", "state.nodes", - "state.onAttach,", "state.toast,", "state.toast;", "state.toast],", @@ -20289,6 +20398,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "status?:", "statusPresentation(", "statusPresentation(status);", + "statusProp", + "statusProp,", "statusProps", "statusProps}", "stay", @@ -20404,8 +20515,11 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "streamingMessageId", "streamingMessageId,", "streamingMessageId:", + "streamingMessageId={null}", "streamingMessageId={streamingMessageId}", "streamingMessageId?:", + "streamingMessageIdProp", + "streamingMessageIdProp,", "streams", "stretched", "strict", @@ -20505,6 +20619,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "submit:", "submit={manageAttachments", "submit?:", + "submitSession", "submitWithAttachments", "submits", "submitted", @@ -20639,6 +20754,8 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "switchBranch:", "switchBranch={switchBranch}", "switchBranch?:", + "switchBranchProp", + "switchBranchProp,", "switchThumbVariants", "switchTrackVariants", "switchTrackVariants,", @@ -21994,7 +22111,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "useComposerContextOptional;", "useComposerContext]", "useComposerValue", - "useComposerValue(p:", + "useComposerValue(props:", "useComposerValue(state);", "useComposerValue({", "useConversation", @@ -22039,7 +22156,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "useDrawer:", "useDropZone", "useDropZone(", - "useDropZone(withFocus(onDrop", + "useDropZone(withFocus(onDrop)", "useDropZone}.", "useEffectHook", "useEffectHook(effect,", @@ -22667,8 +22784,11 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "will", "will.", "win", + "win,", + "win.", "window", "window.open.", + "wins", "wins,", "wins.", "wins;", @@ -22685,6 +22805,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "with.", "with?", "withFocus", + "withFocus(baseCtxValue.onAttach);", "withFocus(onAttach);", "within", "without", @@ -22734,6 +22855,7 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "writers", "writes", "wrong", + "wrongly", "x)", "x,", "x-axis", @@ -22806,4 +22928,5 @@ export const FRAMEWORK_CANDIDATES: readonly string[] = [ "zero-size", "zero-size,", "zone", + "zone.", ];