fix(loading): cursor positioning, render-phase defaultValue sync, remove unnecessary useMemo (#4396)

* fix(loading): fix spans and draft chat state

* fix(user-input): document auto-focus RAF ordering relative to draft restore

* chore(user-input): remove extraneous comments
This commit is contained in:
Waleed
2026-05-01 21:00:58 -07:00
committed by GitHub
parent af859cd508
commit dc195d3aed
2 changed files with 22 additions and 20 deletions
@@ -1,6 +1,6 @@
'use client'
import { useLayoutEffect, useMemo, useRef } from 'react'
import { useLayoutEffect, useRef } from 'react'
import { cn } from '@/lib/core/utils/cn'
import { MessageActions } from '@/app/workspace/[workspaceId]/components'
import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments'
@@ -111,17 +111,14 @@ export function MothershipChat({
const { staged: stagedMessages, isStaging } = useProgressiveList(messages, stagingKey)
const stagedMessageCount = stagedMessages.length
const stagedOffset = messages.length - stagedMessages.length
const precedingUserContentByIndex = useMemo(() => {
const contentByIndex: Array<string | undefined> = []
let lastUserContent: string | undefined
for (const [index, message] of messages.entries()) {
contentByIndex[index] = lastUserContent
if (message.role === 'user') {
lastUserContent = message.content
}
const precedingUserContentByIndex: Array<string | undefined> = []
let lastUserContent: string | undefined
for (const [index, message] of messages.entries()) {
precedingUserContentByIndex[index] = lastUserContent
if (message.role === 'user') {
lastUserContent = message.content
}
return contentByIndex
}, [messages])
}
const initialScrollDoneRef = useRef(false)
const userInputRef = useRef<UserInputHandle>(null)
@@ -153,12 +153,13 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
const overlayRef = useRef<HTMLDivElement>(null)
const plusMenuRef = useRef<PlusMenuHandle>(null)
const prevDefaultValueRef = useRef(defaultValue)
useEffect(() => {
if (defaultValue === prevDefaultValueRef.current) return
prevDefaultValueRef.current = defaultValue
if (defaultValue) setValue(defaultValue)
}, [defaultValue])
const [prevDefaultValue, setPrevDefaultValue] = useState(defaultValue)
if (defaultValue && defaultValue !== prevDefaultValue) {
setPrevDefaultValue(defaultValue)
setValue(defaultValue)
} else if (!defaultValue && prevDefaultValue) {
setPrevDefaultValue(defaultValue)
}
const files = useFileAttachments({
userId: userId || session?.user?.id,
@@ -206,10 +207,15 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
}))
)
}
if (draft.text) {
const textarea = textareaRef.current
if (textarea) {
textarea.focus()
textarea.setSelectionRange(draft.text.length, draft.text.length)
}
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps -- intentional mount-only restore
// Skip the initial save — restore's setState calls haven't propagated yet, so
// files/contexts are still empty and would transiently wipe a file-only draft.
const isFirstSaveRef = useRef(true)
useEffect(() => {
if (isFirstSaveRef.current) {
@@ -418,7 +424,6 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
const newValue = `${before}${insertText}${after}`
pendingCursorRef.current = newPos
// Eagerly sync refs so successive drop-handler iterations see the updated position
valueRef.current = newValue
atInsertPosRef.current = newPos
mentionRangeRef.current = null