Skip to content
Open

Dev #5410

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions apps/docs/components/workflow-preview/format-references.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ export function formatReferences(text: string): ReactNode[] {
const isReference =
(part.startsWith('<') && part.endsWith('>')) || (part.startsWith('{{') && part.endsWith('}}'))
return isReference ? (
// biome-ignore lint/suspicious/noArrayIndexKey: static, never reordered
<span key={index} className='text-[var(--brand-secondary)]'>
{part}
</span>
) : (
// biome-ignore lint/suspicious/noArrayIndexKey: static, never reordered
<span key={index}>{part}</span>
)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { useSubmitCopilotFeedback } from '@/hooks/queries/copilot-feedback'
import { useForkMothershipChat } from '@/hooks/queries/mothership-chats'
import { useFolderStore } from '@/stores/folders/store'

const SPECIAL_TAGS = 'thinking|options|usage_upgrade|credential|mothership-error|file'
const SPECIAL_TAGS = 'thinking|options|usage_upgrade|credential|mothership-error|file|question'

function toPlainText(raw: string): string {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ const MARKDOWN_COMPONENTS = {
interface ChatContentProps {
content: string
isStreaming?: boolean
/** Transcript-derived answers for this message's question card (renders the recap). */
questionAnswers?: string[]
onOptionSelect?: (id: string) => void
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
onRevealStateChange?: (isRevealing: boolean) => void
Expand All @@ -287,6 +289,7 @@ interface ChatContentProps {
function ChatContentInner({
content,
isStreaming = false,
questionAnswers,
onOptionSelect,
onWorkspaceResourceSelect,
onRevealStateChange,
Expand Down Expand Up @@ -406,6 +409,7 @@ function ChatContentInner({
<SpecialTags
key={`special-${group.index}`}
segment={group.segment}
questionAnswers={questionAnswers}
onOptionSelect={onOptionSelect}
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export type { AgentGroupItem, NestedAgentGroup } from './agent-group'
export { AgentGroup, CircleStop, isAgentGroupResolved } from './agent-group'
export { ChatContent } from './chat-content'
export { Options } from './options'
export { QuestionDisplay } from './question'
export { PendingTagIndicator, parseSpecialTags, SpecialTags } from './special-tags'
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {
formatQuestionAnswerMessage,
parseQuestionAnswerMessage,
QuestionDisplay,
} from './question'
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import {
formatQuestionAnswerMessage,
parseQuestionAnswerMessage,
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/question/question'
import type { QuestionItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags'

const QUESTIONS: QuestionItem[] = [
{
type: 'single_select',
prompt: 'How should I handle the duplicates?',
options: [{ id: 'keep_newest', label: 'Keep the newest entry' }],
},
{
type: 'single_select',
prompt: 'Delete 4 archived workflows?',
options: [
{ id: 'yes', label: 'Delete them' },
{ id: 'no', label: 'Cancel' },
],
},
{
type: 'multi_select',
prompt: 'What time zone should the daily report run in?',
options: [
{ id: 'est', label: 'EST' },
{ id: 'pst', label: 'PST' },
],
},
]

describe('formatQuestionAnswerMessage', () => {
it('sends a prompt-answer line for a single question', () => {
expect(formatQuestionAnswerMessage([QUESTIONS[0]], ['Keep the newest entry'])).toBe(
'How should I handle the duplicates? — Keep the newest entry'
)
})

it('sends one prompt-answer line per question for multi-step batches', () => {
expect(formatQuestionAnswerMessage(QUESTIONS, ['Keep the newest entry', 'Cancel', 'EST'])).toBe(
'How should I handle the duplicates? — Keep the newest entry\n' +
'Delete 4 archived workflows? — Cancel\n' +
'What time zone should the daily report run in? — EST'
)
})
})

describe('parseQuestionAnswerMessage', () => {
it('round-trips what formatQuestionAnswerMessage produces', () => {
const answers = ['Keep the newest entry', 'Cancel', 'EST, PST']
const message = formatQuestionAnswerMessage(QUESTIONS, answers)
expect(parseQuestionAnswerMessage(QUESTIONS, message)).toEqual(answers)
})

it('round-trips a single question', () => {
const message = formatQuestionAnswerMessage([QUESTIONS[0]], ['Merge them'])
expect(parseQuestionAnswerMessage([QUESTIONS[0]], message)).toEqual(['Merge them'])
})

it('rejects an unrelated user message (dismissed card, typed something else)', () => {
expect(parseQuestionAnswerMessage([QUESTIONS[0]], 'actually, show me the logs')).toBeNull()
})

it('rejects when the line count does not match the question count', () => {
const partial = formatQuestionAnswerMessage(QUESTIONS.slice(0, 2), ['A', 'B'])
expect(parseQuestionAnswerMessage(QUESTIONS, partial)).toBeNull()
})

it('rejects when a line pairs with the wrong prompt', () => {
const swapped =
'Delete 4 archived workflows? — Cancel\n' +
'How should I handle the duplicates? — Keep the newest entry\n' +
'What time zone should the daily report run in? — EST'
expect(parseQuestionAnswerMessage(QUESTIONS, swapped)).toBeNull()
})

it('preserves em-dashes inside the answer text', () => {
const message = formatQuestionAnswerMessage([QUESTIONS[0]], ['newest — but keep backups'])
expect(parseQuestionAnswerMessage([QUESTIONS[0]], message)).toEqual([
'newest — but keep backups',
])
})
})
Loading
Loading