diff --git a/packages/ui/src/components/chat/MarkdownRenderer.tsx b/packages/ui/src/components/chat/MarkdownRenderer.tsx index 00c0e80a..5f7f901d 100644 --- a/packages/ui/src/components/chat/MarkdownRenderer.tsx +++ b/packages/ui/src/components/chat/MarkdownRenderer.tsx @@ -47,8 +47,12 @@ export const MarkdownRenderer: React.FC ); -export const SimpleMarkdownRenderer: React.FC> = (props) => ( - }> +type SimpleMarkdownRendererProps = React.ComponentPropsWithoutRef & { + fallbackContent?: React.ReactNode; +}; + +export const SimpleMarkdownRenderer: React.FC = ({ fallbackContent, ...props }) => ( + }> ); diff --git a/packages/ui/src/components/chat/QuestionCard.tsx b/packages/ui/src/components/chat/QuestionCard.tsx index 01d5a5d0..14c0425e 100644 --- a/packages/ui/src/components/chat/QuestionCard.tsx +++ b/packages/ui/src/components/chat/QuestionCard.tsx @@ -15,6 +15,7 @@ import * as sessionActions from '@/sync/session-actions'; import { useI18n } from '@/lib/i18n'; import { serializeQuestionAsJson, serializeQuestionAsMarkdown } from './questionSerializers'; import { QUESTION_CUSTOM_TEXTAREA_MIN_HEIGHT, getQuestionCustomTextareaHeight } from './questionTextareaSizing'; +import { QuestionMarkdown } from './QuestionMarkdown'; interface QuestionCardProps { question: QuestionRequest; @@ -423,7 +424,11 @@ export const QuestionCard: React.FC = ({ question }) => { ) : activeQuestion ? ( <> -
{activeQuestion.question}
+ {isMultiple ? (
{t('chat.questionCard.selectMultiple')}
diff --git a/packages/ui/src/components/chat/QuestionMarkdown.test.tsx b/packages/ui/src/components/chat/QuestionMarkdown.test.tsx new file mode 100644 index 00000000..2c3f1ef2 --- /dev/null +++ b/packages/ui/src/components/chat/QuestionMarkdown.test.tsx @@ -0,0 +1,25 @@ +import { describe, expect, test } from 'bun:test'; + +import { SimpleMarkdownRenderer } from './MarkdownRenderer'; +import { QuestionMarkdown } from './QuestionMarkdown'; + +describe('QuestionMarkdown', () => { + test('delegates exact content to the tool markdown renderer', () => { + const content = 'Choose **one** from `mode`: [details](https://example.com)'; + const element = QuestionMarkdown({ content, size: 'meta' }); + + expect(element.type).toBe(SimpleMarkdownRenderer); + expect(element.props.content).toBe(content); + expect(element.props.variant).toBe('tool'); + expect(element.props.fallbackContent.props.children).toBe(content); + expect(element.props.fallbackContent.props.className).toContain('whitespace-pre-wrap'); + }); + + test('preserves question typography size and caller classes', () => { + const meta = QuestionMarkdown({ content: 'Meta', size: 'meta', className: 'font-medium text-foreground' }); + const micro = QuestionMarkdown({ content: 'Micro', size: 'micro', className: 'text-muted-foreground' }); + + expect(meta.props.className).toBe('question-markdown typography-meta font-medium text-foreground'); + expect(micro.props.className).toBe('question-markdown typography-micro text-muted-foreground'); + }); +}); diff --git a/packages/ui/src/components/chat/QuestionMarkdown.tsx b/packages/ui/src/components/chat/QuestionMarkdown.tsx new file mode 100644 index 00000000..3ffcdae7 --- /dev/null +++ b/packages/ui/src/components/chat/QuestionMarkdown.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { cn } from '@/lib/utils'; +import { SimpleMarkdownRenderer } from './MarkdownRenderer'; + +interface QuestionMarkdownProps { + content: string; + size: 'meta' | 'micro'; + className?: string; +} + +export function QuestionMarkdown({ content, size, className }: QuestionMarkdownProps) { + const classes = cn('question-markdown', size === 'meta' ? 'typography-meta' : 'typography-micro', className); + + return ( + {content}} + /> + ); +} diff --git a/packages/ui/src/components/chat/message/parts/ToolPart.tsx b/packages/ui/src/components/chat/message/parts/ToolPart.tsx index fa1279f9..6a57ef0f 100644 --- a/packages/ui/src/components/chat/message/parts/ToolPart.tsx +++ b/packages/ui/src/components/chat/message/parts/ToolPart.tsx @@ -4,6 +4,7 @@ import { useMobileAppActions } from '@/apps/mobileAppContext'; import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext'; import { cn } from '@/lib/utils'; import { SimpleMarkdownRenderer } from '../../MarkdownRenderer'; +import { QuestionMarkdown } from '../../QuestionMarkdown'; import { MessageFilesDisplay } from '../../FileAttachment'; import { getToolMetadata } from '@/lib/toolHelpers'; import type { ToolPart as ToolPartType, ToolState as ToolStateUnion, FilePart } from '@opencode-ai/sdk/v2'; @@ -1407,7 +1408,7 @@ const ToolExpandedContent: React.FC = React.memo(({
{parsedQA.map((qa, index) => (
-
{qa.question}
+
{qa.answer}
))} @@ -1444,7 +1445,7 @@ const ToolExpandedContent: React.FC = React.memo(({ {q.header ? (
{coerceToText(q.header)}
) : null} -
{coerceToText(q.question)}
+ {Array.isArray(q.options) && q.options.length > 0 ? (
{q.options.map((opt) => ( diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css index 265f77be..0eaf8ba3 100644 --- a/packages/ui/src/index.css +++ b/packages/ui/src/index.css @@ -1039,6 +1039,18 @@ html:not(.dark) .chat-scroll { font-size: var(--text-code) !important; } +.question-markdown > .markdown-content.markdown-tool { + font-size: inherit !important; +} + +.question-markdown > .markdown-content > [data-md-block]:first-child > :first-child { + margin-top: 0; +} + +.question-markdown > .markdown-content > [data-md-block]:last-child > :last-child { + margin-bottom: 0; +} + /* Reasoning markdown renders at meta size, dimmed. */ .markdown-content.markdown-reasoning { font-size: var(--text-markdown); diff --git a/packages/ui/src/styles/mobile.css b/packages/ui/src/styles/mobile.css index d32e5dcd..22b0dcd5 100644 --- a/packages/ui/src/styles/mobile.css +++ b/packages/ui/src/styles/mobile.css @@ -41,6 +41,10 @@ font-size: var(--text-code) !important; } + :root.mobile-pointer:not(.desktop-runtime) .question-markdown > .markdown-content.markdown-tool { + font-size: inherit !important; + } + /* Improve touch targets for mobile */ :root.mobile-pointer:not(.desktop-runtime) button:not([role="radio"]):not([role="checkbox"]):not([role="switch"]), :root.mobile-pointer:not(.desktop-runtime) .btn,