Skip to content

Commit 9952888

Browse files
committed
fix(mothership): hide mode selection after the first message
1 parent 8406f9b commit 9952888

3 files changed

Lines changed: 83 additions & 53 deletions

File tree

‎apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx‎

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -608,28 +608,35 @@ it('shows Build with a chevron in the shared chip and text-only modes in its men
608608
).toBe('Preserved draft')
609609
})
610610

611-
it('allows changing the next message mode while a response is streaming', async () => {
612-
const onModeChange = vi.fn()
613-
await render(false, 'Next question', 'agent', {
614-
isSending: true,
615-
showModeSelector: true,
616-
onModeChange,
617-
})
618-
const mode = container.querySelector<HTMLButtonElement>('[aria-label="Conversation mode"]')!
619-
expect(mode.disabled).toBe(false)
620-
await act(async () =>
621-
mode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
622-
)
623-
const search = [...document.querySelectorAll<HTMLElement>('[role="menuitem"]')].find(
624-
(item) => item.textContent === 'Search'
625-
)!
626-
await act(async () => search.click())
627-
expect(onModeChange).toHaveBeenCalledExactlyOnceWith('assistant')
628-
expect(
629-
container.querySelector<HTMLInputElement | HTMLTextAreaElement>('[aria-label="Ask Sim"]')!.value
630-
).toBe('Next question')
631-
expect(mocks.submit).not.toHaveBeenCalled()
632-
})
611+
it.each(['agent', 'assistant'] as const)(
612+
'shows the compact %s controls without a mode selector after sending',
613+
async (requestMode) => {
614+
await render(false, 'Next question', requestMode, {
615+
isSending: true,
616+
showModeSelector: false,
617+
})
618+
expect(container.querySelector('[aria-label="Conversation mode"]')).toBeNull()
619+
expect(container.textContent).not.toContain('Build')
620+
if (requestMode === 'agent') {
621+
const plus = container.querySelector('[aria-label="Add resources"]')!
622+
expect(plus.previousElementSibling).toBeNull()
623+
expect(
624+
[...plus.parentElement!.querySelectorAll('button')].map((button) =>
625+
button.getAttribute('aria-label')
626+
)
627+
).toEqual(['Add resources', 'Attach file', 'Skills'])
628+
} else {
629+
const input = container.querySelector('[aria-label="Ask Sim"]')!
630+
const leading = input.closest('.grid')!.firstElementChild!
631+
expect(leading.querySelectorAll('svg')).toHaveLength(1)
632+
expect(leading.querySelector('button')).toBeNull()
633+
}
634+
expect(container.querySelector<HTMLInputElement>('[aria-label="Ask Sim"]')!.value).toBe(
635+
'Next question'
636+
)
637+
expect(mocks.submit).not.toHaveBeenCalled()
638+
}
639+
)
633640

634641
it('opens resources, attaches files, and inserts skills while streaming', async () => {
635642
await render(false, 'Next question', 'agent', { isSending: true })

‎apps/sim/app/o/[organizationId]/home/organization-home.test.tsx‎

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -603,27 +603,49 @@ it('passes explicitly selected skill workspace contexts through the ordinary Bui
603603
})
604604
})
605605

606-
describe('same-chat mode selection', () => {
607-
it('preserves an existing chat and draft and applies the selected harness to its next turn', async () => {
606+
describe('conversation mode selection', () => {
607+
it.each(['agent', 'assistant'] as const)(
608+
'keeps an existing %s chat in its saved mode without a mode control',
609+
async (mode) => {
610+
mocks.renderer.mockImplementation(({ composer }: { composer: ReactNode }) => composer)
611+
await act(async () => renderHome(<OrganizationHome chatId='chat-a' requestMode={mode} />))
612+
await act(async () => composerProps().onChange('Keep this draft'))
613+
expect(composerProps().showModeSelector).toBe(false)
614+
expect(composerProps().onModeChange).toBeUndefined()
615+
expect(composerProps().requestMode).toBe(mode)
616+
expect(composerProps().value).toBe('Keep this draft')
617+
expect(mocks.push).not.toHaveBeenCalled()
618+
expect(mocks.send).not.toHaveBeenCalled()
619+
await act(async () => composerProps().onSubmit('Keep this draft'))
620+
expect(mocks.send).toHaveBeenLastCalledWith(
621+
'Keep this draft',
622+
undefined,
623+
undefined,
624+
expect.objectContaining({ requestMode: mode })
625+
)
626+
}
627+
)
628+
629+
it('offers both modes before sending and removes mode selection with the first message', async () => {
608630
mocks.renderer.mockImplementation(({ composer }: { composer: ReactNode }) => composer)
609-
await act(async () => renderHome(<OrganizationHome chatId='chat-a' requestMode='agent' />))
610-
await act(async () => composerProps().onChange('Keep this draft'))
631+
await act(async () => renderHome(<OrganizationHome />))
632+
expect(composerProps().showModeSelector).toBe(true)
611633
await act(async () => composerProps().onModeChange?.('assistant'))
612-
expect(composerProps().value).toBe('Keep this draft')
613634
expect(composerProps().requestMode).toBe('assistant')
614-
expect(mocks.push).not.toHaveBeenCalled()
615-
expect(mocks.send).not.toHaveBeenCalled()
616-
expect(mocks.chat).toHaveBeenLastCalledWith(
617-
{ organizationId: 'organization-a' },
618-
'chat-a',
619-
expect.objectContaining({ requestMode: 'assistant' })
620-
)
621-
await act(async () => composerProps().onSubmit('Keep this draft'))
622-
expect(mocks.send).toHaveBeenLastCalledWith('Keep this draft', undefined, undefined, {
623-
requestMode: 'assistant',
624-
assistantSearchLevel: 'adaptive',
635+
await act(async () => composerProps().onModeChange?.('agent'))
636+
expect(composerProps().requestMode).toBe('agent')
637+
mocks.send.mockImplementationOnce(() => {
638+
mocks.chat.mockReturnValue({
639+
...mocks.chat(),
640+
messages: [{ id: 'user-1', role: 'user', content: 'First message' }],
641+
isSending: true,
642+
})
625643
})
626-
expect(useOrganizationChatModeStore.getState().modes['reader:organization-a']).toBe('assistant')
644+
await act(async () => composerProps().onSubmit('First message'))
645+
expect(composerProps().isInitialView).toBe(false)
646+
expect(composerProps().showModeSelector).toBe(false)
647+
expect(composerProps().onModeChange).toBeUndefined()
648+
expect(composerProps().requestMode).toBe('agent')
627649
})
628650
it('uses only this user and organization preference for new chats without changing saved chats', async () => {
629651
const store = useOrganizationChatModeStore.getState()
@@ -639,18 +661,18 @@ describe('same-chat mode selection', () => {
639661
expect(useOrganizationChatModeStore.getState().modes['reader:organization-a']).toBe('assistant')
640662
})
641663
it.each(['isSending', 'isReconnecting', 'messageQueue'] as const)(
642-
'changes the next message mode while %s is active without sending or navigating',
664+
'keeps mode selection unavailable while %s is active in a conversation',
643665
async (field) => {
644666
mocks.chat.mockReturnValue({
645667
...mocks.chat(),
668+
messages: [{ id: 'user-1', role: 'user', content: 'First message' }],
646669
[field]: field === 'messageQueue' ? [{ id: 'queued' }] : true,
647670
})
671+
mocks.renderer.mockImplementation(({ composer }: { composer: ReactNode }) => composer)
648672
await act(async () => renderHome(<OrganizationHome />))
649-
await act(async () => composerProps().onModeChange?.('assistant'))
650-
expect(composerProps().requestMode).toBe('assistant')
651-
expect(useOrganizationChatModeStore.getState().modes['reader:organization-a']).toBe(
652-
'assistant'
653-
)
673+
expect(composerProps().showModeSelector).toBe(false)
674+
expect(composerProps().onModeChange).toBeUndefined()
675+
expect(composerProps().requestMode).toBe('agent')
654676
expect(mocks.send).not.toHaveBeenCalled()
655677
expect(mocks.push).not.toHaveBeenCalled()
656678
}
@@ -700,21 +722,21 @@ describe('same-chat mode selection', () => {
700722
)
701723
})
702724

703-
it('does not seed results for a Build turn just because Search is selected next', async () => {
725+
it('does not seed Search results for an existing Build conversation', async () => {
704726
mocks.chat.mockReturnValue({
705727
...mocks.chat(),
706728
messages: [{ id: 'user-1', role: 'user', content: 'Build a table', requestMode: 'agent' }],
707729
})
708730
mocks.renderer.mockImplementation(({ composer }: { composer: ReactNode }) => composer)
709731
await act(async () => renderHome(<OrganizationHome chatId='chat-a' requestMode='agent' />))
710-
await act(async () => composerProps().onModeChange?.('assistant'))
732+
expect(composerProps().onModeChange).toBeUndefined()
711733
expect(mocks.resourcePanel).toHaveBeenLastCalledWith(
712734
expect.not.objectContaining({ searchRequest: expect.anything() }),
713735
undefined
714736
)
715737
})
716738

717-
it('does not start a new panel search when the next turn is switched to Build', async () => {
739+
it('does not start a new panel search when an existing Search conversation loads', async () => {
718740
mocks.chat.mockReturnValue({
719741
...mocks.chat(),
720742
messages: [
@@ -723,7 +745,7 @@ it('does not start a new panel search when the next turn is switched to Build',
723745
})
724746
mocks.renderer.mockImplementation(({ composer }: { composer: ReactNode }) => composer)
725747
await act(async () => renderHome(<OrganizationHome chatId='chat-a' requestMode='assistant' />))
726-
await act(async () => composerProps().onModeChange?.('agent'))
748+
expect(composerProps().onModeChange).toBeUndefined()
727749
expect(mocks.resourcePanel).toHaveBeenLastCalledWith(
728750
expect.not.objectContaining({ searchRequest: expect.anything() }),
729751
undefined

‎apps/sim/app/o/[organizationId]/home/organization-home.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const subscribeToClient = () => () => {}
5454
const clientSnapshot = () => true
5555
const serverSnapshot = () => false
5656

57-
/** Home chooses the next turn harness while keeping the current conversation intact. */
57+
/** Home chooses the conversation mode before its first message. */
5858
export function OrganizationHome(props: OrganizationHomeProps) {
5959
const { organization, searchAccess, canBuild, mothershipAvailable } = useOrganizationContext()
6060
const { data: session } = useSession()
@@ -108,6 +108,7 @@ function OrganizationHomeContent({
108108
activeResourceState: controller.activeResourceState,
109109
})
110110
const hasChat = Boolean(chatId || chat.messages.length)
111+
const canSelectMode = !hasChat && mothershipAvailable && canBuild && searchAccess.memberScoped
111112
const assistantSearchLevel = urlSearchLevel ?? rememberedSearchLevel
112113
const panel = useChatResourcePanel(chat, controller)
113114
const addResource = panel.addResourceFromUser
@@ -229,7 +230,7 @@ function OrganizationHomeContent({
229230
})
230231
}
231232
const changeMode = (mode: ChatRequestMode) => {
232-
if (!canBuild || !searchAccess.memberScoped || mode === requestMode) return
233+
if (!canSelectMode || mode === requestMode) return
233234
setSelectedMode(mode)
234235
void setSearchParams({ searchLevel: null })
235236
if (userId) rememberMode(userId, organization.id, mode)
@@ -270,8 +271,8 @@ function OrganizationHomeContent({
270271
requestMode={requestMode}
271272
assistantSearchLevel={assistantSearchLevel}
272273
onAssistantSearchLevelChange={changeAssistantSearchLevel}
273-
showModeSelector={mothershipAvailable && canBuild && searchAccess.memberScoped}
274-
onModeChange={changeMode}
274+
showModeSelector={canSelectMode}
275+
onModeChange={canSelectMode ? changeMode : undefined}
275276
value={draft}
276277
restoredContexts={restoredContexts}
277278
files={files}

0 commit comments

Comments
 (0)