Skip to content

Commit 4f08a0a

Browse files
committed
fix(mothership): restore saved Plan chat history
1 parent 7f82c65 commit 4f08a0a

4 files changed

Lines changed: 142 additions & 8 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.mount-send.test.tsx‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,66 @@ async function waitFor(predicate: () => boolean, budgetMs = 2000): Promise<void>
477477
}
478478

479479
describe('useChat remount send recovery', () => {
480+
it('hydrates a saved Plan conversation on a cold mount without posting', async () => {
481+
const messages = [
482+
{
483+
id: 'user-plan',
484+
role: 'user',
485+
content: 'Investigate incidents',
486+
requestMode: 'plan',
487+
timestamp: '2026-09-23T00:19:00Z',
488+
},
489+
{
490+
id: 'assistant-plan',
491+
role: 'assistant',
492+
content: 'Saved investigation',
493+
requestMode: 'plan',
494+
timestamp: '2026-09-23T00:36:00Z',
495+
},
496+
]
497+
mockRequestJson.mockResolvedValue({
498+
success: true,
499+
chat: {
500+
id: 'chat-plan',
501+
mode: 'plan',
502+
title: 'Incident triage',
503+
messages,
504+
activeStreamId: null,
505+
resources: [],
506+
},
507+
})
508+
const { getResult } = renderUseChatInChat('chat-plan')
509+
await waitFor(() => !getResult().isChatHistoryPending)
510+
expect(getResult().messages.map(({ id, content }) => ({ id, content }))).toEqual(
511+
messages.map(({ id, content }) => ({ id, content }))
512+
)
513+
expect(getResult().error).toBeNull()
514+
expect(state.postBodies).toHaveLength(0)
515+
})
516+
517+
it('exposes a history load failure and clears it after a successful retry', async () => {
518+
mockRequestJson.mockRejectedValue(new Error('History request failed'))
519+
const { getResult } = renderUseChatInChat('chat-unavailable')
520+
await waitFor(() => !getResult().isChatHistoryPending)
521+
expect(getResult().error).toBe('Failed to load chat history. Refresh to try again.')
522+
mockRequestJson.mockResolvedValue({
523+
success: true,
524+
chat: {
525+
id: 'chat-unavailable',
526+
mode: 'plan',
527+
title: null,
528+
messages: [],
529+
activeStreamId: null,
530+
resources: [],
531+
},
532+
})
533+
await act(async () => {
534+
await queryClient.refetchQueries({ queryKey: mothershipChatKeys.detail('chat-unavailable') })
535+
})
536+
await waitFor(() => getResult().error === null)
537+
expect(state.postBodies).toHaveLength(0)
538+
})
539+
480540
it.each([
481541
['agent', 'assistant'],
482542
['assistant', 'agent'],

‎apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,8 +1238,11 @@ export function useChat(
12381238
[flushPendingResources, queryClient, workspaceId, organizationId, scopeKey]
12391239
)
12401240

1241-
const { data: chatHistory, isPending: isChatHistoryPending } =
1242-
useMothershipChatHistory(resolvedChatId)
1241+
const {
1242+
data: chatHistory,
1243+
isPending: isChatHistoryPending,
1244+
error: chatHistoryError,
1245+
} = useMothershipChatHistory(resolvedChatId)
12431246
const requestModeRef = useRef<ChatRequestMode>(
12441247
options?.requestMode ?? (organizationId ? 'assistant' : 'agent')
12451248
)
@@ -4912,7 +4915,8 @@ export function useChat(
49124915
isChatHistoryPending,
49134916
isSending,
49144917
isReconnecting,
4915-
error,
4918+
error:
4919+
error ?? (chatHistoryError ? 'Failed to load chat history. Refresh to try again.' : null),
49164920
resolvedChatId,
49174921
desktopScopeId,
49184922
sendMessage,

‎apps/sim/hooks/queries/mothership-chats.test.ts‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { sleep } from '@sim/utils/helpers'
66
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
import { getMothershipChatResponseSchema } from '@/lib/api/contracts/mothership-chats'
78
import type { MothershipResource } from '@/lib/mothership/resources/types'
89

910
const { queryClient, suspendBrowserScope, suspendTerminalScope, clearChat } = vi.hoisted(() => ({
@@ -165,6 +166,77 @@ describe('tasks query boundary parsing', () => {
165166
})
166167
})
167168

169+
describe.each(['primary', 'fallback'] as const)('%s history endpoint', (endpoint) => {
170+
it.each(getMothershipChatResponseSchema.shape.chat.shape.mode.options)(
171+
'reopens a saved %s transcript without sending a new message',
172+
async (mode) => {
173+
const messages = [
174+
{
175+
id: 'user-1',
176+
role: 'user',
177+
content: 'Investigate incidents',
178+
timestamp: '2026-09-23T00:19:00Z',
179+
requestMode: mode,
180+
},
181+
{
182+
id: 'assistant-1',
183+
role: 'assistant',
184+
content: 'Saved investigation',
185+
timestamp: '2026-09-23T00:36:00Z',
186+
requestMode: mode,
187+
},
188+
]
189+
if (endpoint === 'fallback') {
190+
vi.mocked(fetch).mockResolvedValueOnce(new Response('Not found', { status: 404 }))
191+
}
192+
vi.mocked(fetch).mockResolvedValueOnce(
193+
jsonResponse({
194+
success: true,
195+
chat: {
196+
id: 'chat-plan',
197+
mode,
198+
title: 'Incident triage',
199+
messages,
200+
activeStreamId: null,
201+
resources: [],
202+
},
203+
})
204+
)
205+
206+
const history = await fetchMothershipChatHistory('chat-plan')
207+
208+
expect(history.mode).toBe(mode)
209+
expect(history.messages).toEqual(
210+
messages.map((message) => expect.objectContaining(message))
211+
)
212+
expect(fetch).toHaveBeenCalledTimes(endpoint === 'fallback' ? 2 : 1)
213+
expect(
214+
vi.mocked(fetch).mock.calls.every(([, init]) => !init?.method || init.method === 'GET')
215+
).toBe(true)
216+
}
217+
)
218+
219+
it('rejects an unknown conversation mode', async () => {
220+
if (endpoint === 'fallback') {
221+
vi.mocked(fetch).mockResolvedValueOnce(new Response('Not found', { status: 404 }))
222+
}
223+
vi.mocked(fetch).mockResolvedValueOnce(
224+
jsonResponse({
225+
success: true,
226+
chat: {
227+
id: 'chat-invalid',
228+
mode: 'unknown',
229+
title: null,
230+
messages: [],
231+
activeStreamId: null,
232+
resources: [],
233+
},
234+
})
235+
)
236+
await expect(fetchMothershipChatHistory('chat-invalid')).rejects.toThrow()
237+
})
238+
})
239+
168240
it('retains saved table views, file paths and execution identities when reading chat resources', async () => {
169241
const resources: MothershipResource[] = [
170242
{ type: 'table', id: 'table-1', title: 'Invoices', viewId: 'overdue' },

‎apps/sim/hooks/queries/mothership-chats.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
deleteMothershipChatContract,
1515
forkMothershipChatContract,
1616
getMothershipChatContract,
17+
getMothershipChatResponseSchema,
1718
listMothershipChatsContract,
1819
type MothershipChat,
1920
type MothershipChatScope,
@@ -181,14 +182,11 @@ function parseChatHistory(value: unknown): MothershipChatHistory {
181182
`${chatContext}.activeStreamId must be a string or null`
182183
)
183184

184-
assertValid(
185-
chat.mode === 'agent' || chat.mode === 'assistant',
186-
`${chatContext}.mode must be agent or assistant`
187-
)
185+
const mode = getMothershipChatResponseSchema.shape.chat.shape.mode.parse(chat.mode)
188186

189187
return {
190188
id: chat.id,
191-
mode: chat.mode,
189+
mode,
192190
title: chat.title,
193191
messages: normalizeMessages(chat.messages),
194192
activeStreamId: chat.activeStreamId,

0 commit comments

Comments
 (0)