Skip to content

Commit 3af6c25

Browse files
authored
fix(mothership): catch draft restore errors instead of crashing /home (#4433)
* fix(mothership): catch draft restore errors instead of crashing /home Wrap the mount-time draft restore in try/catch with clearDraft on throw, and coerce text to a string in the useState initializer. A corrupt entry in mothership-drafts:v1 localStorage previously took down the entire workspace via the error boundary. * fix(mothership): defer state writes and log restore failures Build the restored state in locals first and only apply on success so a partial throw can't leave stale contexts in the UI with the draft already cleared. Switch the empty catch to logger.error so corrupt-draft incidents surface in production logs.
1 parent 57dc745 commit 3af6c25

1 file changed

Lines changed: 28 additions & 12 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/home/components/user-input

apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
useRef,
1212
useState,
1313
} from 'react'
14+
import { createLogger } from '@sim/logger'
1415
import { Paperclip } from 'lucide-react'
1516
import { useParams } from 'next/navigation'
1617
import { Button, Tooltip } from '@/components/emcn'
@@ -58,6 +59,8 @@ import type { ChatContext } from '@/stores/panel'
5859

5960
export type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types'
6061

62+
const logger = createLogger('UserInput')
63+
6164
function getCaretAnchor(
6265
textarea: HTMLTextAreaElement,
6366
caretPos: number
@@ -148,7 +151,8 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
148151
const [value, setValue] = useState(() => {
149152
if (defaultValue) return defaultValue
150153
if (!draftScopeKey) return ''
151-
return useMothershipDraftsStore.getState().drafts[draftScopeKey]?.text ?? ''
154+
const text = useMothershipDraftsStore.getState().drafts[draftScopeKey]?.text
155+
return typeof text === 'string' ? text : ''
152156
})
153157
const overlayRef = useRef<HTMLDivElement>(null)
154158
const plusMenuRef = useRef<PlusMenuHandle>(null)
@@ -189,14 +193,17 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
189193
useEffect(() => {
190194
if (hasRestoredDraftRef.current || !draftScopeKey) return
191195
hasRestoredDraftRef.current = true
192-
const draft = useMothershipDraftsStore.getState().drafts[draftScopeKey]
193-
if (!draft) return
194-
if (draft.contexts?.length) {
195-
contextManagement.setSelectedContexts(draft.contexts)
196-
}
197-
if (draft.fileAttachments?.length) {
198-
files.restoreAttachedFiles(
199-
draft.fileAttachments.map((a) => ({
196+
let restoredContexts: ChatContext[] | null = null
197+
let restoredFiles: AttachedFile[] | null = null
198+
let caretText: string | null = null
199+
try {
200+
const draft = useMothershipDraftsStore.getState().drafts[draftScopeKey]
201+
if (!draft) return
202+
if (draft.contexts?.length) {
203+
restoredContexts = draft.contexts
204+
}
205+
if (draft.fileAttachments?.length) {
206+
restoredFiles = draft.fileAttachments.map((a) => ({
200207
id: a.id,
201208
name: a.filename,
202209
size: a.size,
@@ -205,13 +212,22 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
205212
key: a.key,
206213
uploading: false,
207214
}))
208-
)
215+
}
216+
if (typeof draft.text === 'string' && draft.text.length > 0) {
217+
caretText = draft.text
218+
}
219+
} catch (err) {
220+
logger.error('Failed to read draft, clearing', { err })
221+
useMothershipDraftsStore.getState().clearDraft(draftScopeKey)
222+
return
209223
}
210-
if (draft.text) {
224+
if (restoredContexts) contextManagement.setSelectedContexts(restoredContexts)
225+
if (restoredFiles) files.restoreAttachedFiles(restoredFiles)
226+
if (caretText !== null) {
211227
const textarea = textareaRef.current
212228
if (textarea) {
213229
textarea.focus()
214-
textarea.setSelectionRange(draft.text.length, draft.text.length)
230+
textarea.setSelectionRange(caretText.length, caretText.length)
215231
}
216232
}
217233
}, []) // eslint-disable-line react-hooks/exhaustive-deps -- intentional mount-only restore

0 commit comments

Comments
 (0)