mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web): migrate to @assistant-ui/react 0.14 (tap 0.9.8 with update-depth fix)
- @assistant-ui/react ^0.11.53 -> ^0.14.29, react-markdown ^0.11.9 -> ^0.14.7 - resolves @assistant-ui/tap 0.9.8, which ships the upstream fix for bulk message prepends (per-scheduler MAX_UPDATE_DEPTH guard, PR assistant-ui/assistant-ui#5370) that the local patch covered for 0.3.5 - API migration: useAssistantApi -> useAui, useAssistantState -> useAuiState with s.* selector access; TextMessagePart type-guard for content.find; portable DefaultComponentsMap annotation for memoizeMarkdownComponents Verified: tsc clean, 1762 unit tests, history-load e2e 12/12 against the unpatched upstream scheduler.
This commit is contained in:
+2
-2
@@ -13,8 +13,8 @@
|
||||
"test:mermaid-lightbox:e2e": "playwright test e2e/mermaid-lightbox.spec.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@assistant-ui/react": "^0.11.53",
|
||||
"@assistant-ui/react-markdown": "^0.11.9",
|
||||
"@assistant-ui/react": "^0.14.29",
|
||||
"@assistant-ui/react-markdown": "^0.14.7",
|
||||
"@elevenlabs/react": "^0.13.0",
|
||||
"@hapi/protocol": "workspace:*",
|
||||
"@lobehub/icons": "^5.4.0",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { render, fireEvent } from '@testing-library/react'
|
||||
const addAttachment = vi.fn()
|
||||
|
||||
vi.mock('@assistant-ui/react', () => ({
|
||||
useAssistantApi: () => ({
|
||||
useAui: () => ({
|
||||
composer: () => ({ addAttachment }),
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useAssistantApi } from '@assistant-ui/react'
|
||||
import { useAui } from '@assistant-ui/react'
|
||||
import { useDragOver } from '@/hooks/useDragOver'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
|
||||
@@ -10,7 +10,7 @@ export function DragDropZone({
|
||||
children: React.ReactNode
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const api = useAssistantApi()
|
||||
const api = useAui()
|
||||
const isDragging = useDragOver()
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getCodexCollaborationModeOptions, getPermissionModeOptionsForFlavor } from '@hapi/protocol'
|
||||
import { ComposerPrimitive, useAssistantApi, useAssistantState } from '@assistant-ui/react'
|
||||
import { ComposerPrimitive, useAui, useAuiState } from '@assistant-ui/react'
|
||||
import {
|
||||
type ChangeEvent as ReactChangeEvent,
|
||||
type ClipboardEvent as ReactClipboardEvent,
|
||||
@@ -276,12 +276,12 @@ export function HappyComposer(props: {
|
||||
const serviceTier = rawServiceTier ?? null
|
||||
const displayedServiceTier = getDisplayedCodexServiceTier(serviceTier)
|
||||
|
||||
const api = useAssistantApi()
|
||||
const api = useAui()
|
||||
const { composerEnterBehavior } = useComposerEnterBehavior()
|
||||
const composerText = useAssistantState(({ composer }) => composer.text)
|
||||
const attachments = useAssistantState(({ composer }) => composer.attachments)
|
||||
const threadIsRunning = useAssistantState(({ thread }) => thread.isRunning)
|
||||
const threadIsDisabled = useAssistantState(({ thread }) => thread.isDisabled)
|
||||
const composerText = useAuiState((s) => s.composer.text)
|
||||
const attachments = useAuiState((s) => s.composer.attachments)
|
||||
const threadIsRunning = useAuiState((s) => s.thread.isRunning)
|
||||
const threadIsDisabled = useAuiState((s) => s.thread.isDisabled)
|
||||
|
||||
const controlsDisabled = disabled || (!active && !allowSendWhenInactive) || threadIsDisabled
|
||||
const trimmed = composerText.trim()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ThreadPrimitive, useAssistantState } from '@assistant-ui/react'
|
||||
import { ThreadPrimitive, useAuiState } from '@assistant-ui/react'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { HappyRuntimeExtras } from '@/lib/assistant-runtime'
|
||||
import type { Session, SessionMetadataSummary } from '@/types/api'
|
||||
@@ -435,7 +435,7 @@ export function HappyThread(props: {
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { terminalToolDisplayMode } = useTerminalToolDisplayMode()
|
||||
const runtimeExtras = useAssistantState(({ thread }) => thread.extras) as HappyRuntimeExtras | undefined
|
||||
const runtimeExtras = useAuiState((s) => s.thread.extras) as HappyRuntimeExtras | undefined
|
||||
const appliedMessagesVersion = runtimeExtras?.messagesVersion ?? props.messagesVersion
|
||||
const appliedHistoryVersion = runtimeExtras?.historyVersion ?? props.historyVersion
|
||||
const viewportRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAssistantApi } from '@assistant-ui/react'
|
||||
import { useAui } from '@assistant-ui/react'
|
||||
import { useCallback, useMemo, useSyncExternalStore } from 'react'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import { getMessageWindowState, subscribeMessageWindow } from '@/lib/message-window-store'
|
||||
@@ -162,7 +162,7 @@ export function QueuedMessagesBar({
|
||||
onEdit?: (params: { text: string; pendingSchedule: PendingSchedule | null }) => void
|
||||
}) {
|
||||
const queued = useQueuedMessages(sessionId)
|
||||
const assistantApi = useAssistantApi()
|
||||
const assistantApi = useAui()
|
||||
const cancelMutation = useCancelQueuedMessage(api)
|
||||
const { t } = useTranslation()
|
||||
const { addToast } = useToast()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MessagePrimitive, useAssistantState } from '@assistant-ui/react'
|
||||
import { MessagePrimitive, useAuiState, type TextMessagePart } from '@assistant-ui/react'
|
||||
import { MarkdownText } from '@/components/assistant-ui/markdown-text'
|
||||
import { Reasoning, ReasoningGroup } from '@/components/assistant-ui/reasoning'
|
||||
import { HappyToolMessage } from '@/components/AssistantChat/messages/ToolMessage'
|
||||
@@ -21,35 +21,35 @@ const MESSAGE_PART_COMPONENTS = {
|
||||
} as const
|
||||
|
||||
export function HappyAssistantMessage() {
|
||||
const messageId = useAssistantState(({ message }) => message.id)
|
||||
const messageId = useAuiState((s) => s.message.id)
|
||||
const elementId = getConversationMessageAnchorId(messageId)
|
||||
const isCliOutput = useAssistantState(({ message }) => {
|
||||
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
const isCliOutput = useAuiState((s) => {
|
||||
const custom = s.message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
return custom?.kind === 'cli-output'
|
||||
})
|
||||
const codexReview = useAssistantState(({ message }) => {
|
||||
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
const codexReview = useAuiState((s) => {
|
||||
const custom = s.message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
return custom?.kind === 'codex-review' ? custom.review : undefined
|
||||
})
|
||||
const cliText = useAssistantState(({ message }) => {
|
||||
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
const cliText = useAuiState((s) => {
|
||||
const custom = s.message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
if (custom?.kind !== 'cli-output') return ''
|
||||
return message.content.find((part) => part.type === 'text')?.text ?? ''
|
||||
return s.message.content.find((part): part is TextMessagePart => part.type === 'text')?.text ?? ''
|
||||
})
|
||||
const toolOnly = useAssistantState(({ message }) => {
|
||||
if (message.role !== 'assistant') return false
|
||||
const parts = message.content
|
||||
const toolOnly = useAuiState((s) => {
|
||||
if (s.message.role !== 'assistant') return false
|
||||
const parts = s.message.content
|
||||
return parts.length > 0 && parts.every((part) => part.type === 'tool-call')
|
||||
})
|
||||
const copyText = useAssistantState(({ message }) => {
|
||||
if (message.role !== 'assistant') return ''
|
||||
return getAssistantCopyText(message.content)
|
||||
const copyText = useAuiState((s) => {
|
||||
if (s.message.role !== 'assistant') return ''
|
||||
return getAssistantCopyText(s.message.content)
|
||||
})
|
||||
|
||||
const durationMs = useAssistantState(({ message }) => (message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined)?.durationMs)
|
||||
const usage = useAssistantState(({ message }) => (message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined)?.usage)
|
||||
const messageModel = useAssistantState(({ message }) => (message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined)?.model)
|
||||
const turnCount = useAssistantState(({ message }) => (message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined)?.turnCount)
|
||||
const durationMs = useAuiState(({ message }) => (message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined)?.durationMs)
|
||||
const usage = useAuiState(({ message }) => (message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined)?.usage)
|
||||
const messageModel = useAuiState(({ message }) => (message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined)?.model)
|
||||
const turnCount = useAuiState(({ message }) => (message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined)?.turnCount)
|
||||
|
||||
const metadata = { durationMs, usage, model: messageModel ?? null, turnCount }
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MessageActions } from './MessageActions'
|
||||
const copy = vi.fn()
|
||||
|
||||
vi.mock('@assistant-ui/react', () => ({
|
||||
useAssistantState: (selector: (state: { message: { createdAt: Date } }) => unknown) => selector({
|
||||
useAuiState: (selector: (state: { message: { createdAt: Date } }) => unknown) => selector({
|
||||
message: { createdAt: new Date(2026, 6, 12, 10, 30) }
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Popover from '@radix-ui/react-popover'
|
||||
import { useAssistantState } from '@assistant-ui/react'
|
||||
import { useAuiState } from '@assistant-ui/react'
|
||||
import { CheckIcon, CopyIcon, InfoIcon } from '@/components/icons'
|
||||
import { useCopyToClipboard } from '@/hooks/useCopyToClipboard'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
@@ -18,7 +18,7 @@ type MessageActionsProps = {
|
||||
export function MessageActions({ align, copyText, metadata, messageElementId }: MessageActionsProps) {
|
||||
const { copied, copy } = useCopyToClipboard()
|
||||
const { t } = useTranslation()
|
||||
const threadIsRunning = useAssistantState(({ thread }) => thread?.isRunning ?? false)
|
||||
const threadIsRunning = useAuiState(({ thread }) => thread?.isRunning ?? false)
|
||||
const canCopy = Boolean(copyText)
|
||||
const hasMetadata = metadata ? buildMessageMetadataLabels(metadata).length > 0 : false
|
||||
const shareButton = messageElementId && !threadIsRunning ? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAssistantState } from '@assistant-ui/react'
|
||||
import { useAuiState } from '@assistant-ui/react'
|
||||
import { formatMessageTimestamp, formatMessageTimestampTitle } from '@/chat/presentation'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -7,7 +7,7 @@ type MessageTimestampProps = {
|
||||
}
|
||||
|
||||
export function MessageTimestamp(props: MessageTimestampProps) {
|
||||
const createdAt = useAssistantState(({ message }) => message.createdAt)
|
||||
const createdAt = useAuiState((s) => s.message.createdAt)
|
||||
|
||||
return (
|
||||
<time
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAssistantState } from '@assistant-ui/react'
|
||||
import { useAuiState } from '@assistant-ui/react'
|
||||
import { useOptionalHappyChatContext } from '@/components/AssistantChat/context'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
|
||||
@@ -30,7 +30,7 @@ export function ShareTurnButton(props: {
|
||||
}) {
|
||||
const ctx = useOptionalHappyChatContext()
|
||||
const { t } = useTranslation()
|
||||
const fallbackUserText = useAssistantState(({ message, thread }) => {
|
||||
const fallbackUserText = useAuiState(({ message, thread }) => {
|
||||
const currentIndex = thread.messages.findIndex((candidate) => candidate.id === message.id)
|
||||
if (currentIndex < 0) return ''
|
||||
for (let index = currentIndex; index >= 0; index -= 1) {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { MessagePrimitive, useAssistantState } from '@assistant-ui/react'
|
||||
import { MessagePrimitive, useAuiState } from '@assistant-ui/react'
|
||||
import { getEventPresentation } from '@/chat/presentation'
|
||||
import type { HappyChatMessageMetadata } from '@/lib/assistant-runtime'
|
||||
import { getConversationMessageAnchorId } from '@/chat/outline'
|
||||
import { MessageTimestamp } from '@/components/AssistantChat/messages/MessageTimestamp'
|
||||
|
||||
export function HappySystemMessage() {
|
||||
const role = useAssistantState(({ message }) => message.role)
|
||||
const messageId = useAssistantState(({ message }) => message.id)
|
||||
const text = useAssistantState(({ message }) => {
|
||||
if (message.role !== 'system') return ''
|
||||
return message.content[0]?.type === 'text' ? message.content[0].text : ''
|
||||
const role = useAuiState((s) => s.message.role)
|
||||
const messageId = useAuiState((s) => s.message.id)
|
||||
const text = useAuiState((s) => {
|
||||
if (s.message.role !== 'system') return ''
|
||||
return s.message.content[0]?.type === 'text' ? s.message.content[0].text : ''
|
||||
})
|
||||
const icon = useAssistantState(({ message }) => {
|
||||
if (message.role !== 'system') return null
|
||||
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
const icon = useAuiState((s) => {
|
||||
if (s.message.role !== 'system') return null
|
||||
const custom = s.message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
const event = custom?.kind === 'event' ? custom.event : undefined
|
||||
return event ? getEventPresentation(event).icon : null
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MessagePrimitive, useAssistantState } from '@assistant-ui/react'
|
||||
import { MessagePrimitive, useAuiState, type TextMessagePart } from '@assistant-ui/react'
|
||||
import { useHappyChatContext } from '@/components/AssistantChat/context'
|
||||
import type { HappyChatMessageMetadata } from '@/lib/assistant-runtime'
|
||||
import { MessageStatusIndicator } from '@/components/AssistantChat/messages/MessageStatusIndicator'
|
||||
@@ -10,36 +10,36 @@ import { MessageActions } from '@/components/AssistantChat/messages/MessageActio
|
||||
|
||||
export function HappyUserMessage() {
|
||||
const ctx = useHappyChatContext()
|
||||
const role = useAssistantState(({ message }) => message.role)
|
||||
const messageId = useAssistantState(({ message }) => message.id)
|
||||
const role = useAuiState((s) => s.message.role)
|
||||
const messageId = useAuiState((s) => s.message.id)
|
||||
const elementId = getConversationMessageAnchorId(messageId)
|
||||
const text = useAssistantState(({ message }) => {
|
||||
if (message.role !== 'user') return ''
|
||||
return message.content.find((part) => part.type === 'text')?.text ?? ''
|
||||
const text = useAuiState((s) => {
|
||||
if (s.message.role !== 'user') return ''
|
||||
return s.message.content.find((part): part is TextMessagePart => part.type === 'text')?.text ?? ''
|
||||
})
|
||||
const status = useAssistantState(({ message }) => {
|
||||
if (message.role !== 'user') return undefined
|
||||
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
const status = useAuiState((s) => {
|
||||
if (s.message.role !== 'user') return undefined
|
||||
const custom = s.message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
return custom?.status
|
||||
})
|
||||
const localId = useAssistantState(({ message }) => {
|
||||
if (message.role !== 'user') return null
|
||||
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
const localId = useAuiState((s) => {
|
||||
if (s.message.role !== 'user') return null
|
||||
const custom = s.message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
return custom?.localId ?? null
|
||||
})
|
||||
const attachments = useAssistantState(({ message }) => {
|
||||
if (message.role !== 'user') return undefined
|
||||
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
const attachments = useAuiState((s) => {
|
||||
if (s.message.role !== 'user') return undefined
|
||||
const custom = s.message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
return custom?.attachments
|
||||
})
|
||||
const isCliOutput = useAssistantState(({ message }) => {
|
||||
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
const isCliOutput = useAuiState((s) => {
|
||||
const custom = s.message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
return custom?.kind === 'cli-output'
|
||||
})
|
||||
const cliText = useAssistantState(({ message }) => {
|
||||
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
const cliText = useAuiState((s) => {
|
||||
const custom = s.message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
if (custom?.kind !== 'cli-output') return ''
|
||||
return message.content.find((part) => part.type === 'text')?.text ?? ''
|
||||
return s.message.content.find((part): part is TextMessagePart => part.type === 'text')?.text ?? ''
|
||||
})
|
||||
if (role !== 'user') return null
|
||||
const canRetry = status === 'failed' && typeof localId === 'string' && Boolean(ctx.onRetryMessage)
|
||||
|
||||
@@ -31,7 +31,7 @@ const mockSessionId = 'sess-test'
|
||||
|
||||
const setText = vi.fn()
|
||||
vi.mock('@assistant-ui/react', () => ({
|
||||
useAssistantApi: () => ({
|
||||
useAui: () => ({
|
||||
composer: () => ({ setText }),
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { flushSync } from 'react-dom'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { AssistantRuntimeProvider, useAssistantApi, useAssistantState } from '@assistant-ui/react'
|
||||
import { AssistantRuntimeProvider, useAui, useAuiState } from '@assistant-ui/react'
|
||||
import { DragDropZone } from '@/components/AssistantChat/DragDropZone'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type {
|
||||
@@ -243,8 +243,8 @@ function isUninvokedScheduledMessage(message: DecryptedMessage): boolean {
|
||||
* refresh of /sessions/:id doesn't re-attach the same payload.
|
||||
*/
|
||||
function ShareSeedConsumer(props: { sessionId: string; sessionActive: boolean }) {
|
||||
const assistantApi = useAssistantApi()
|
||||
const composerText = useAssistantState(({ composer }) => composer.text)
|
||||
const assistantApi = useAui()
|
||||
const composerText = useAuiState((s) => s.composer.text)
|
||||
const composerTextRef = useRef(composerText)
|
||||
const initRef = useRef(false)
|
||||
const transferIdRef = useRef<string | null>(null)
|
||||
@@ -332,7 +332,7 @@ export function ScratchlistDrawerHost(props: {
|
||||
onSend: (text: string, attachments?: AttachmentMetadata[], scheduledAt?: number | null) => Promise<boolean>
|
||||
onExitScratchlistMode: () => void
|
||||
}) {
|
||||
const assistantApi = useAssistantApi()
|
||||
const assistantApi = useAui()
|
||||
const handlePromoteToComposer = useCallback(async (entry: ScratchlistEntry) => {
|
||||
assistantApi.composer().setText(entry.text)
|
||||
// Exit scratchlist mode before rehydrating attachments so addAttachment
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import '@assistant-ui/react-markdown/styles/dot.css'
|
||||
|
||||
import type { ComponentPropsWithoutRef, MouseEvent, ReactNode } from 'react'
|
||||
import type { ComponentPropsWithoutRef, ComponentType, MouseEvent, ReactNode } from 'react'
|
||||
import { useState, useCallback, useEffect, useMemo, createContext, useContext } from 'react'
|
||||
import {
|
||||
MarkdownTextPrimitive,
|
||||
@@ -763,7 +763,15 @@ function Image(props: ComponentPropsWithoutRef<'img'>) {
|
||||
return <img {...props} className={cn('aui-md-img my-3 max-w-full rounded-xl', props.className)} />
|
||||
}
|
||||
|
||||
export const defaultComponents = memoizeMarkdownComponents({
|
||||
type DefaultComponentsMap = {
|
||||
[key: string]: ComponentType<any>
|
||||
code: ComponentType<any>
|
||||
pre: ComponentType<any>
|
||||
CodeHeader: ComponentType<any>
|
||||
SyntaxHighlighter: ComponentType<any>
|
||||
}
|
||||
|
||||
export const defaultComponents: DefaultComponentsMap = memoizeMarkdownComponents({
|
||||
SyntaxHighlighter,
|
||||
CodeHeader,
|
||||
pre: Pre,
|
||||
@@ -790,7 +798,7 @@ export const defaultComponents = memoizeMarkdownComponents({
|
||||
th: Th,
|
||||
td: Td,
|
||||
img: Image,
|
||||
} as const)
|
||||
} as const) as unknown as DefaultComponentsMap
|
||||
|
||||
export function MarkdownText() {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user