-
-
- {ttsEnabled && !hasPromptContent && !isSessionActive && latestPlayableAssistant && (
-
- )}
- {hasPromptContent && !isSessionActive && (
-
- )}
- {isSessionActive && (
-
- )}
-
+
+ {!isFormSheetOpen && (
+
+ {ttsEnabled && !hasPromptContent && !isSessionActive && latestPlayableAssistant && (
+
+ )}
+ {hasPromptContent && !isSessionActive && (
+
+ )}
+ {isSessionActive && (
+
+ )}
+
+ )}
{minimizedForm && (
handleMinimizeForm(currentForm)}
/>
)}
-
- {!isRemoteServer && }
-
-
+
+
+ {!isRemoteServer &&
}
+
+
+
)}
diff --git a/frontend/src/pages/__tests__/SessionDetail.form-prompt.test.tsx b/frontend/src/pages/__tests__/SessionDetail.form-prompt.test.tsx
index b42fbaff7..6501f30b5 100644
--- a/frontend/src/pages/__tests__/SessionDetail.form-prompt.test.tsx
+++ b/frontend/src/pages/__tests__/SessionDetail.form-prompt.test.tsx
@@ -84,6 +84,7 @@ vi.mock('@/hooks/useSettingsDialog', () => ({
vi.mock('@/hooks/useMobile', () => ({
useMobile: mocks.useMobile,
useSwipeBack: vi.fn(() => ({ ref: vi.fn() })),
+ useSwipeDismiss: vi.fn(() => ({ bind: vi.fn(), swipeProgress: 0, swipeStyles: {} })),
}))
vi.mock('@/hooks/useVisualViewport', () => ({
@@ -275,7 +276,7 @@ describe('SessionDetail form prompt session scoping', () => {
render(renderTree())
await waitFor(() => {
- expect(screen.getByText('Continue A')).toBeInTheDocument()
+ expect(screen.getAllByText('Continue A')[0]).toBeInTheDocument()
})
})
@@ -294,7 +295,7 @@ describe('SessionDetail form prompt session scoping', () => {
render(renderTree())
- await waitFor(() => expect(screen.getByText('Continue A')).toBeInTheDocument())
+ await waitFor(() => expect(screen.getAllByText('Continue A')[0]).toBeInTheDocument())
await userEvent.click(screen.getByRole('button', { name: 'Yes' }))
await userEvent.click(screen.getByRole('button', { name: 'Submit' }))
@@ -307,7 +308,7 @@ describe('SessionDetail form prompt session scoping', () => {
formsState.bySession[VIEWED_SESSION_ID] = viewedSessionForm
const { rerender } = render(renderTree())
- await waitFor(() => expect(screen.getByText('Continue A')).toBeInTheDocument())
+ await waitFor(() => expect(screen.getAllByText('Continue A')[0]).toBeInTheDocument())
await userEvent.click(screen.getByRole('button', { name: 'Continue A' }))
await waitFor(() => expect(screen.getByRole('button', { name: 'Dismiss form' })).toBeInTheDocument())
@@ -319,7 +320,7 @@ describe('SessionDetail form prompt session scoping', () => {
formsState.bySession[VIEWED_SESSION_ID] = otherSessionForm
rerender(renderTree())
- await waitFor(() => expect(screen.getByText('Continue B')).toBeInTheDocument())
+ await waitFor(() => expect(screen.getAllByText('Continue B')[0]).toBeInTheDocument())
})
it('dismisses a minimized form locally and then opens the next form', async () => {
@@ -329,7 +330,7 @@ describe('SessionDetail form prompt session scoping', () => {
})
const { rerender } = render(renderTree())
- await waitFor(() => expect(screen.getByText('Continue A')).toBeInTheDocument())
+ await waitFor(() => expect(screen.getAllByText('Continue A')[0]).toBeInTheDocument())
await userEvent.click(screen.getByRole('button', { name: 'Continue A' }))
await userEvent.click(screen.getByRole('button', { name: 'Dismiss form' }))
@@ -340,7 +341,7 @@ describe('SessionDetail form prompt session scoping', () => {
formsState.bySession[VIEWED_SESSION_ID] = otherSessionForm
rerender(renderTree())
- await waitFor(() => expect(screen.getByText('Continue B')).toBeInTheDocument())
+ await waitFor(() => expect(screen.getAllByText('Continue B')[0]).toBeInTheDocument())
})
it('retains a minimized form and reports an error when dismissal fails', async () => {
@@ -348,7 +349,7 @@ describe('SessionDetail form prompt session scoping', () => {
formsState.cancel.mockRejectedValue(new Error('boom'))
render(renderTree())
- await waitFor(() => expect(screen.getByText('Continue A')).toBeInTheDocument())
+ await waitFor(() => expect(screen.getAllByText('Continue A')[0]).toBeInTheDocument())
await userEvent.click(screen.getByRole('button', { name: 'Continue A' }))
await userEvent.click(screen.getByRole('button', { name: 'Dismiss form' }))
@@ -357,4 +358,32 @@ describe('SessionDetail form prompt session scoping', () => {
)
expect(screen.getByRole('button', { name: 'Dismiss form' })).toBeInTheDocument()
})
+
+ it('fills the prompt overlay from the header to the prompt while a form is open on mobile', async () => {
+ mocks.useMobile.mockReturnValue(true)
+ formsState.bySession[VIEWED_SESSION_ID] = viewedSessionForm
+
+ render(renderTree())
+
+ await waitFor(() => expect(screen.getAllByText('Continue A')[0]).toBeInTheDocument())
+ expect(screen.getByTestId('prompt-overlay')).toHaveClass('top-0')
+ })
+
+ it('keeps the prompt overlay anchored without an open form or once the form is minimized', async () => {
+ mocks.useMobile.mockReturnValue(true)
+
+ const { rerender } = render(renderTree())
+ await waitFor(() => expect(screen.getByText('MockedPromptInput')).toBeInTheDocument())
+ expect(screen.getByTestId('prompt-overlay')).not.toHaveClass('top-0')
+
+ formsState.bySession[VIEWED_SESSION_ID] = viewedSessionForm
+ rerender(renderTree())
+
+ await waitFor(() => expect(screen.getAllByText('Continue A')[0]).toBeInTheDocument())
+ expect(screen.getByTestId('prompt-overlay')).toHaveClass('top-0')
+
+ await userEvent.click(screen.getByRole('button', { name: 'Continue A' }))
+
+ await waitFor(() => expect(screen.getByTestId('prompt-overlay')).not.toHaveClass('top-0'))
+ })
})
From a525314c38f1a3c5051be68788855b6185d2b6fe Mon Sep 17 00:00:00 2001
From: Chris Scott <99081550+chriswritescode-dev@users.noreply.github.com>
Date: Mon, 19 Oct 2026 22:43:58 -0400
Subject: [PATCH 2/2] feat(sessions): add a side-question button to the mobile
question sheet
---
.../components/session/FormPrompt.test.tsx | 24 +++++++++++++++++++
.../src/components/session/FormPrompt.tsx | 21 ++++++++++++----
frontend/src/pages/SessionDetail.tsx | 1 +
3 files changed, 42 insertions(+), 4 deletions(-)
diff --git a/frontend/src/components/session/FormPrompt.test.tsx b/frontend/src/components/session/FormPrompt.test.tsx
index b47b89e0c..dbb41451a 100644
--- a/frontend/src/components/session/FormPrompt.test.tsx
+++ b/frontend/src/components/session/FormPrompt.test.tsx
@@ -564,6 +564,30 @@ describe('FormPrompt', () => {
})
})
+ describe('side question button', () => {
+ it('opens the side question from the footer button', async () => {
+ const onAskSideQuestion = vi.fn()
+ render(
+
+ )
+
+ await userEvent.click(screen.getByRole('button', { name: 'Ask a side question' }))
+
+ expect(onAskSideQuestion).toHaveBeenCalledTimes(1)
+ })
+
+ it('omits the side question button when no handler is provided', () => {
+ render(
)
+
+ expect(screen.queryByRole('button', { name: 'Ask a side question' })).not.toBeInTheDocument()
+ })
+ })
+
describe('conditional and external fields', () => {
const conditionalForm: FormInfo = {
id: 'form-conditional',
diff --git a/frontend/src/components/session/FormPrompt.tsx b/frontend/src/components/session/FormPrompt.tsx
index 6e7d01027..0cb9af6c1 100644
--- a/frontend/src/components/session/FormPrompt.tsx
+++ b/frontend/src/components/session/FormPrompt.tsx
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useId, useRef, useState, type ReactNode } from 'react'
-import { X, ChevronDown, ChevronUp, Check, Loader2 } from 'lucide-react'
+import { X, ChevronDown, ChevronUp, Check, Loader2, MessageCircleQuestion } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { ExternalFieldCard } from '@/components/ui/external-field-card'
@@ -62,9 +62,10 @@ interface FormPromptProps {
onReply: (formID: string, answer: FormAnswer) => Promise
onCancel: (formID: string) => Promise
onMinimize?: () => void
+ onAskSideQuestion?: () => void
}
-export function FormPrompt({ form, onReply, onCancel, onMinimize }: FormPromptProps) {
+export function FormPrompt({ form, onReply, onCancel, onMinimize, onAskSideQuestion }: FormPromptProps) {
const candidateFields = form.fields.filter(isAnswerableField)
const [state, setState] = useState(() => loadFormDraft(form.id) ?? buildInitialState(candidateFields))
const { answer, customValues, customDrafts } = state
@@ -397,7 +398,7 @@ export function FormPrompt({ form, onReply, onCancel, onMinimize }: FormPromptPr
)}
-