diff --git a/packages/ui/src/components/chat/MarkdownRenderer.tsx b/packages/ui/src/components/chat/MarkdownRenderer.tsx index c294bd6f..84962c95 100644 --- a/packages/ui/src/components/chat/MarkdownRenderer.tsx +++ b/packages/ui/src/components/chat/MarkdownRenderer.tsx @@ -43,8 +43,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 9ed08f5d..009fcfc3 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 { RuntimeAPIContext } from '@/contexts/runtimeAPIContext'; import { PatchDiff } from '@pierre/diffs/react'; 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'; @@ -1672,7 +1673,7 @@ const ToolExpandedContent: React.FC = React.memo(({
{parsedQA.map((qa, index) => (
-
{qa.question}
+
{qa.answer}
))} @@ -1709,7 +1710,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 ceb0b975..9cabdc3c 100644 --- a/packages/ui/src/index.css +++ b/packages/ui/src/index.css @@ -1022,6 +1022,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 74866cc9..e605c3d3 100644 --- a/packages/ui/src/styles/mobile.css +++ b/packages/ui/src/styles/mobile.css @@ -79,6 +79,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,