Improve message stream auto-scroll during streaming
This commit is contained in:
@@ -15,9 +15,11 @@ interface MessageItemProps {
|
|||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
showAgentMeta?: boolean
|
showAgentMeta?: boolean
|
||||||
}
|
onContentRendered?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MessageItem(props: MessageItemProps) {
|
||||||
|
|
||||||
export default function MessageItem(props: MessageItemProps) {
|
|
||||||
const isUser = () => props.record.role === "user"
|
const isUser = () => props.record.role === "user"
|
||||||
const timestamp = () => {
|
const timestamp = () => {
|
||||||
const createdTime = props.messageInfo?.time?.created ?? props.record.createdAt
|
const createdTime = props.messageInfo?.time?.created ?? props.record.createdAt
|
||||||
@@ -234,6 +236,7 @@ export default function MessageItem(props: MessageItemProps) {
|
|||||||
messageType={props.record.role}
|
messageType={props.record.role}
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
|
onRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ interface MessagePartProps {
|
|||||||
messageType?: "user" | "assistant"
|
messageType?: "user" | "assistant"
|
||||||
instanceId: string
|
instanceId: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
}
|
onRendered?: () => void
|
||||||
export default function MessagePart(props: MessagePartProps) {
|
}
|
||||||
|
export default function MessagePart(props: MessagePartProps) {
|
||||||
|
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
const { preferences } = useConfig()
|
const { preferences } = useConfig()
|
||||||
const partType = () => props.part?.type || ""
|
const partType = () => props.part?.type || ""
|
||||||
@@ -95,11 +97,17 @@ export default function MessagePart(props: MessagePartProps) {
|
|||||||
<Show when={!(props.part.type === "text" && props.part.synthetic) && partHasRenderableText(props.part)}>
|
<Show when={!(props.part.type === "text" && props.part.synthetic) && partHasRenderableText(props.part)}>
|
||||||
<div class={textContainerClass()}>
|
<div class={textContainerClass()}>
|
||||||
<Show
|
<Show
|
||||||
when={isAssistantMessage()}
|
when={isAssistantMessage()}
|
||||||
fallback={<span>{plainTextContent()}</span>}
|
fallback={<span>{plainTextContent()}</span>}
|
||||||
>
|
>
|
||||||
<Markdown part={createTextPartForMarkdown()} isDark={isDark()} size={isAssistantMessage() ? "tight" : "base"} />
|
<Markdown
|
||||||
</Show>
|
part={createTextPartForMarkdown()}
|
||||||
|
isDark={isDark()}
|
||||||
|
size={isAssistantMessage() ? "tight" : "base"}
|
||||||
|
onRendered={props.onRendered}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { useScrollCache } from "../lib/hooks/use-scroll-cache"
|
|||||||
import { setActiveInstanceId } from "../stores/instances"
|
import { setActiveInstanceId } from "../stores/instances"
|
||||||
|
|
||||||
const SCROLL_SCOPE = "session"
|
const SCROLL_SCOPE = "session"
|
||||||
const SCROLL_DIRECTION_THRESHOLD = 10
|
|
||||||
const USER_SCROLL_INTENT_WINDOW_MS = 600
|
const USER_SCROLL_INTENT_WINDOW_MS = 600
|
||||||
const SCROLL_INTENT_KEYS = new Set(["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " ", "Spacebar"])
|
const SCROLL_INTENT_KEYS = new Set(["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " ", "Spacebar"])
|
||||||
|
|
||||||
@@ -174,6 +173,7 @@ interface MessageStreamV2Props {
|
|||||||
loading?: boolean
|
loading?: boolean
|
||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
|
registerScrollToBottom?: (fn: () => void) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContentDisplayItem {
|
interface ContentDisplayItem {
|
||||||
@@ -284,15 +284,10 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const changeToken = createMemo(() => {
|
const changeToken = createMemo(() => {
|
||||||
const revisionValue = sessionRevision()
|
// Any change that can affect layout (new message, part update, revert,
|
||||||
const ids = messageIds()
|
// etc.) should bump the session revision. We use this as the primary
|
||||||
if (ids.length === 0) {
|
// signal for auto-scroll decisions.
|
||||||
return `${revisionValue}:empty`
|
return String(sessionRevision())
|
||||||
}
|
|
||||||
const lastId = ids[ids.length - 1]
|
|
||||||
const lastRecord = store().getMessage(lastId)
|
|
||||||
const tailSignature = lastRecord ? `msg:${lastRecord.id}:${lastRecord.revision}` : `msg:${lastId}:missing`
|
|
||||||
return `${revisionValue}:${tailSignature}`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const scrollCache = useScrollCache({
|
const scrollCache = useScrollCache({
|
||||||
@@ -312,6 +307,10 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
let detachScrollIntentListeners: (() => void) | undefined
|
let detachScrollIntentListeners: (() => void) | undefined
|
||||||
let hasRestoredScroll = false
|
let hasRestoredScroll = false
|
||||||
let hasInitialScroll = false
|
let hasInitialScroll = false
|
||||||
|
// When the user explicitly clicks "scroll to bottom", we want the
|
||||||
|
// smooth scroll animation to complete without being immediately
|
||||||
|
// overridden by the auto-scroll effects that react to new messages.
|
||||||
|
let suppressAutoScrollOnce = false
|
||||||
|
|
||||||
function markUserScrollIntent() {
|
function markUserScrollIntent() {
|
||||||
const now = typeof performance !== "undefined" ? performance.now() : Date.now()
|
const now = typeof performance !== "undefined" ? performance.now() : Date.now()
|
||||||
@@ -372,15 +371,18 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
function scrollToBottom(immediate = false) {
|
function scrollToBottom(immediate = false) {
|
||||||
if (!containerRef) return
|
if (!containerRef) return
|
||||||
const behavior = immediate ? "auto" : "smooth"
|
const behavior = immediate ? "auto" : "smooth"
|
||||||
requestAnimationFrame(() => {
|
if (!immediate) {
|
||||||
if (!containerRef) return
|
// We initiated this scroll (e.g., via the button). Skip the
|
||||||
containerRef.scrollTo({ top: containerRef.scrollHeight, behavior })
|
// next auto-scroll reaction so the smooth animation isn't
|
||||||
setAutoScroll(true)
|
// overridden by changeToken/preference effects.
|
||||||
lastMeasuredScrollHeight = containerRef.scrollHeight
|
suppressAutoScrollOnce = true
|
||||||
lastKnownScrollTop = containerRef.scrollTop
|
}
|
||||||
updateScrollIndicators(containerRef)
|
containerRef.scrollTo({ top: containerRef.scrollHeight, behavior })
|
||||||
scheduleScrollPersist()
|
setAutoScroll(true)
|
||||||
})
|
lastMeasuredScrollHeight = containerRef.scrollHeight
|
||||||
|
lastKnownScrollTop = containerRef.scrollTop
|
||||||
|
updateScrollIndicators(containerRef)
|
||||||
|
scheduleScrollPersist()
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottomAndClamp(immediate = false) {
|
function scrollToBottomAndClamp(immediate = false) {
|
||||||
@@ -396,17 +398,28 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
if (!containerRef) return
|
if (!containerRef) return
|
||||||
const behavior = immediate ? "auto" : "smooth"
|
const behavior = immediate ? "auto" : "smooth"
|
||||||
setAutoScroll(false)
|
setAutoScroll(false)
|
||||||
requestAnimationFrame(() => {
|
containerRef.scrollTo({ top: 0, behavior })
|
||||||
if (!containerRef) return
|
lastMeasuredScrollHeight = containerRef.scrollHeight
|
||||||
containerRef.scrollTo({ top: 0, behavior })
|
lastKnownScrollTop = containerRef.scrollTop
|
||||||
lastMeasuredScrollHeight = containerRef.scrollHeight
|
updateScrollIndicators(containerRef)
|
||||||
lastKnownScrollTop = containerRef.scrollTop
|
scheduleScrollPersist()
|
||||||
updateScrollIndicators(containerRef)
|
|
||||||
scheduleScrollPersist()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleContentRendered() {
|
||||||
|
if (!containerRef) return
|
||||||
|
if (!autoScroll()) return
|
||||||
|
scrollToBottomAndClamp(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.registerScrollToBottom) {
|
||||||
|
props.registerScrollToBottom(() => scrollToBottomAndClamp(true))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
let pendingScrollPersist: number | null = null
|
let pendingScrollPersist: number | null = null
|
||||||
|
|
||||||
|
|
||||||
function scheduleScrollPersist() {
|
function scheduleScrollPersist() {
|
||||||
if (pendingScrollPersist !== null) return
|
if (pendingScrollPersist !== null) return
|
||||||
pendingScrollPersist = requestAnimationFrame(() => {
|
pendingScrollPersist = requestAnimationFrame(() => {
|
||||||
@@ -439,20 +452,22 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
pendingScrollFrame = requestAnimationFrame(() => {
|
pendingScrollFrame = requestAnimationFrame(() => {
|
||||||
pendingScrollFrame = null
|
pendingScrollFrame = null
|
||||||
if (!containerRef) return
|
if (!containerRef) return
|
||||||
const previousTop = lastKnownScrollTop
|
|
||||||
const currentTop = containerRef.scrollTop
|
const currentTop = containerRef.scrollTop
|
||||||
const movingUp = currentTop < previousTop - SCROLL_DIRECTION_THRESHOLD
|
|
||||||
const movingDown = currentTop > previousTop + SCROLL_DIRECTION_THRESHOLD
|
|
||||||
lastKnownScrollTop = currentTop
|
lastKnownScrollTop = currentTop
|
||||||
lastMeasuredScrollHeight = containerRef.scrollHeight
|
lastMeasuredScrollHeight = containerRef.scrollHeight
|
||||||
const atBottom = isNearBottom(containerRef)
|
const atBottom = isNearBottom(containerRef)
|
||||||
|
|
||||||
if (isUserScroll) {
|
if (isUserScroll) {
|
||||||
if (movingUp && !atBottom && autoScroll()) {
|
// If the user scrolls and ends near the bottom, enable auto-scroll.
|
||||||
setAutoScroll(false)
|
// If they scroll away from the bottom by more than our threshold,
|
||||||
} else if (movingDown && atBottom && !autoScroll()) {
|
// disable auto-scroll until they explicitly return.
|
||||||
setAutoScroll(true)
|
if (atBottom) {
|
||||||
|
if (!autoScroll()) setAutoScroll(true)
|
||||||
|
} else {
|
||||||
|
if (autoScroll()) setAutoScroll(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScrollIndicators(containerRef)
|
updateScrollIndicators(containerRef)
|
||||||
scheduleScrollPersist()
|
scheduleScrollPersist()
|
||||||
})
|
})
|
||||||
@@ -465,7 +480,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
if (!target) return
|
if (!target) return
|
||||||
if (loading) return
|
if (loading) return
|
||||||
if (hasRestoredScroll) return
|
if (hasRestoredScroll) return
|
||||||
|
|
||||||
scrollCache.restore(target, {
|
scrollCache.restore(target, {
|
||||||
onApplied: (snapshot) => {
|
onApplied: (snapshot) => {
|
||||||
if (snapshot) {
|
if (snapshot) {
|
||||||
@@ -478,12 +493,12 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
updateScrollIndicators(target)
|
updateScrollIndicators(target)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
hasRestoredScroll = true
|
hasRestoredScroll = true
|
||||||
})
|
})
|
||||||
|
|
||||||
let previousToken: string | undefined
|
let previousToken: string | undefined
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const token = changeToken()
|
const token = changeToken()
|
||||||
const loading = props.loading
|
const loading = props.loading
|
||||||
@@ -493,19 +508,28 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
previousToken = token
|
previousToken = token
|
||||||
|
if (suppressAutoScrollOnce) {
|
||||||
|
suppressAutoScrollOnce = false
|
||||||
|
return
|
||||||
|
}
|
||||||
if (autoScroll()) {
|
if (autoScroll()) {
|
||||||
scrollToBottomAndClamp(true)
|
scrollToBottomAndClamp(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
preferenceSignature()
|
preferenceSignature()
|
||||||
if (props.loading) return
|
if (props.loading) return
|
||||||
if (!autoScroll()) {
|
if (!autoScroll()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (suppressAutoScrollOnce) {
|
||||||
|
suppressAutoScrollOnce = false
|
||||||
|
return
|
||||||
|
}
|
||||||
scrollToBottomAndClamp(true)
|
scrollToBottomAndClamp(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (messageIds().length === 0) {
|
if (messageIds().length === 0) {
|
||||||
@@ -621,18 +645,21 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
|
|||||||
<Index each={messageIds()}>
|
<Index each={messageIds()}>
|
||||||
{(messageId) => (
|
{(messageId) => (
|
||||||
<MessageBlock
|
<MessageBlock
|
||||||
messageId={messageId()}
|
messageId={messageId()}
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
store={store}
|
store={store}
|
||||||
messageIndexMap={messageIndexMap}
|
messageIndexMap={messageIndexMap}
|
||||||
lastAssistantIndex={lastAssistantIndex}
|
lastAssistantIndex={lastAssistantIndex}
|
||||||
showThinking={() => preferences().showThinkingBlocks}
|
showThinking={() => preferences().showThinkingBlocks}
|
||||||
thinkingDefaultExpanded={() => (preferences().thinkingBlocksExpansion ?? "expanded") === "expanded"}
|
thinkingDefaultExpanded={() => (preferences().thinkingBlocksExpansion ?? "expanded") === "expanded"}
|
||||||
showUsageMetrics={showUsagePreference}
|
showUsageMetrics={showUsagePreference}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
/>
|
onContentRendered={handleContentRendered}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</Index>
|
</Index>
|
||||||
</div>
|
</div>
|
||||||
@@ -681,8 +708,10 @@ interface MessageBlockProps {
|
|||||||
showUsageMetrics: () => boolean
|
showUsageMetrics: () => boolean
|
||||||
onRevert?: (messageId: string) => void
|
onRevert?: (messageId: string) => void
|
||||||
onFork?: (messageId?: string) => void
|
onFork?: (messageId?: string) => void
|
||||||
|
onContentRendered?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function MessageBlock(props: MessageBlockProps) {
|
function MessageBlock(props: MessageBlockProps) {
|
||||||
const record = createMemo(() => props.store().getMessage(props.messageId))
|
const record = createMemo(() => props.store().getMessage(props.messageId))
|
||||||
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
const messageInfo = createMemo(() => props.store().getMessageInfo(props.messageId))
|
||||||
@@ -882,7 +911,9 @@ function MessageBlock(props: MessageBlockProps) {
|
|||||||
showAgentMeta={(item as ContentDisplayItem).showAgentMeta}
|
showAgentMeta={(item as ContentDisplayItem).showAgentMeta}
|
||||||
onRevert={props.onRevert}
|
onRevert={props.onRevert}
|
||||||
onFork={props.onFork}
|
onFork={props.onFork}
|
||||||
|
onContentRendered={props.onContentRendered}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item.type === "tool"}>
|
<Match when={item.type === "tool"}>
|
||||||
{(() => {
|
{(() => {
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
const session = () => props.activeSessions.get(props.sessionId)
|
const session = () => props.activeSessions.get(props.sessionId)
|
||||||
const messagesLoading = createMemo(() => isSessionMessagesLoading(props.instanceId, props.sessionId))
|
const messagesLoading = createMemo(() => isSessionMessagesLoading(props.instanceId, props.sessionId))
|
||||||
const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instanceId))
|
const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instanceId))
|
||||||
|
let scrollToBottomHandle: (() => void) | undefined
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const currentSession = session()
|
const currentSession = session()
|
||||||
if (currentSession) {
|
if (currentSession) {
|
||||||
loadMessages(props.instanceId, currentSession.id).catch(console.error)
|
loadMessages(props.instanceId, currentSession.id).catch(console.error)
|
||||||
@@ -34,6 +36,9 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function handleSendMessage(prompt: string, attachments: Attachment[]) {
|
async function handleSendMessage(prompt: string, attachments: Attachment[]) {
|
||||||
|
if (scrollToBottomHandle) {
|
||||||
|
scrollToBottomHandle()
|
||||||
|
}
|
||||||
await sendMessage(props.instanceId, props.sessionId, prompt, attachments)
|
await sendMessage(props.instanceId, props.sessionId, prompt, attachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,12 +142,16 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div class="session-view">
|
<div class="session-view">
|
||||||
<MessageStreamV2
|
<MessageStreamV2
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
sessionId={activeSession.id}
|
sessionId={activeSession.id}
|
||||||
loading={messagesLoading()}
|
loading={messagesLoading()}
|
||||||
onRevert={handleRevert}
|
onRevert={handleRevert}
|
||||||
onFork={handleFork}
|
onFork={handleFork}
|
||||||
/>
|
registerScrollToBottom={(fn) => {
|
||||||
|
scrollToBottomHandle = fn
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
<PromptInput
|
<PromptInput
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
|
|||||||
@@ -618,6 +618,13 @@ function mutateToolPartPermission(
|
|||||||
draft.updatedAt = Date.now()
|
draft.updatedAt = Date.now()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Permission attachment/removal can change the rendered height of the
|
||||||
|
// message list (e.g., permission blocks or diffs), so bump the
|
||||||
|
// session revision to ensure auto-scroll reacts.
|
||||||
|
if (messageRecord.sessionId) {
|
||||||
|
store.setState("sessionRevisions", messageRecord.sessionId, (value: number = 0) => value + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachPermissionToToolPart(instanceId: string, permission: Permission, active: boolean): void {
|
function attachPermissionToToolPart(instanceId: string, permission: Permission, active: boolean): void {
|
||||||
|
|||||||
@@ -439,10 +439,10 @@ export function createInstanceMessageStore(instanceId: string): InstanceMessageS
|
|||||||
bufferPendingPart({ messageId: input.messageId, part: input.part, receivedAt: Date.now() })
|
bufferPendingPart({ messageId: input.messageId, part: input.part, receivedAt: Date.now() })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const partId = ensurePartId(input.messageId, input.part, message.partIds.length)
|
const partId = ensurePartId(input.messageId, input.part, message.partIds.length)
|
||||||
const cloned = clonePart(input.part)
|
const cloned = clonePart(input.part)
|
||||||
|
|
||||||
setState(
|
setState(
|
||||||
"messages",
|
"messages",
|
||||||
input.messageId,
|
input.messageId,
|
||||||
@@ -463,8 +463,13 @@ export function createInstanceMessageStore(instanceId: string): InstanceMessageS
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Any part update can change the rendered height of the message
|
||||||
|
// list, so we treat it as a session revision for scroll purposes.
|
||||||
|
bumpSessionRevision(message.sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function flushPendingParts(messageId: string) {
|
function flushPendingParts(messageId: string) {
|
||||||
const pending = state.pendingParts[messageId]
|
const pending = state.pendingParts[messageId]
|
||||||
if (!pending || pending.length === 0) {
|
if (!pending || pending.length === 0) {
|
||||||
|
|||||||
@@ -81,20 +81,25 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
|||||||
if (event.type === "message.part.updated") {
|
if (event.type === "message.part.updated") {
|
||||||
const rawPart = event.properties?.part
|
const rawPart = event.properties?.part
|
||||||
if (!rawPart) return
|
if (!rawPart) return
|
||||||
|
|
||||||
const part = normalizeMessagePart(rawPart)
|
const part = normalizeMessagePart(rawPart)
|
||||||
const sessionId = typeof part.sessionID === "string" ? part.sessionID : undefined
|
const messageInfo = (event as any)?.properties?.message as MessageInfo | undefined
|
||||||
const messageId = typeof part.messageID === "string" ? part.messageID : undefined
|
|
||||||
|
const fallbackSessionId = typeof messageInfo?.sessionID === "string" ? messageInfo.sessionID : undefined
|
||||||
|
const fallbackMessageId = typeof messageInfo?.id === "string" ? messageInfo.id : undefined
|
||||||
|
|
||||||
|
const sessionId = typeof part.sessionID === "string" ? part.sessionID : fallbackSessionId
|
||||||
|
const messageId = typeof part.messageID === "string" ? part.messageID : fallbackMessageId
|
||||||
if (!sessionId || !messageId) return
|
if (!sessionId || !messageId) return
|
||||||
|
|
||||||
const session = instanceSessions.get(sessionId)
|
const session = instanceSessions.get(sessionId)
|
||||||
if (!session) return
|
if (!session) return
|
||||||
|
|
||||||
const store = messageStoreBus.getOrCreate(instanceId)
|
const store = messageStoreBus.getOrCreate(instanceId)
|
||||||
const messageInfo = (event as any)?.properties?.message as MessageInfo | undefined
|
|
||||||
const role: MessageRole = resolveMessageRole(messageInfo)
|
const role: MessageRole = resolveMessageRole(messageInfo)
|
||||||
const createdAt = typeof messageInfo?.time?.created === "number" ? messageInfo.time.created : Date.now()
|
const createdAt = typeof messageInfo?.time?.created === "number" ? messageInfo.time.created : Date.now()
|
||||||
|
|
||||||
|
|
||||||
let record = store.getMessage(messageId)
|
let record = store.getMessage(messageId)
|
||||||
if (!record) {
|
if (!record) {
|
||||||
const pendingId = findPendingMessageId(store, sessionId, role)
|
const pendingId = findPendingMessageId(store, sessionId, role)
|
||||||
@@ -119,8 +124,9 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
|||||||
if (messageInfo) {
|
if (messageInfo) {
|
||||||
upsertMessageInfoV2(instanceId, messageInfo, { status: "streaming" })
|
upsertMessageInfoV2(instanceId, messageInfo, { status: "streaming" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyPartUpdateV2(instanceId, { ...part, sessionID: sessionId, messageID: messageId })
|
||||||
|
|
||||||
applyPartUpdateV2(instanceId, part)
|
|
||||||
|
|
||||||
updateSessionInfo(instanceId, sessionId)
|
updateSessionInfo(instanceId, sessionId)
|
||||||
refreshPermissionsForSession(instanceId, sessionId)
|
refreshPermissionsForSession(instanceId, sessionId)
|
||||||
|
|||||||
Reference in New Issue
Block a user