fix(chat): render question prompts as markdown

This commit is contained in:
Pascal André
2026-07-28 13:16:55 +02:00
parent 92abdefea8
commit def0b869e2
7 changed files with 79 additions and 5 deletions
@@ -43,8 +43,12 @@ export const MarkdownRenderer: React.FC<React.ComponentPropsWithoutRef<typeof Ma
</React.Suspense>
);
export const SimpleMarkdownRenderer: React.FC<React.ComponentPropsWithoutRef<typeof SimpleMarkdownRendererLazy>> = (props) => (
<React.Suspense fallback={<MobileMarkdownFallback {...props} />}>
type SimpleMarkdownRendererProps = React.ComponentPropsWithoutRef<typeof SimpleMarkdownRendererLazy> & {
fallbackContent?: React.ReactNode;
};
export const SimpleMarkdownRenderer: React.FC<SimpleMarkdownRendererProps> = ({ fallbackContent, ...props }) => (
<React.Suspense fallback={fallbackContent ?? <MobileMarkdownFallback {...props} />}>
<SimpleMarkdownRendererLazy {...props} />
</React.Suspense>
);
@@ -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<QuestionCardProps> = ({ question }) => {
</div>
) : activeQuestion ? (
<>
<div className="typography-meta font-medium text-foreground mb-1.5">{activeQuestion.question}</div>
<QuestionMarkdown
content={activeQuestion.question}
size="meta"
className="font-medium text-foreground mb-1.5"
/>
{isMultiple ? (
<div className="typography-micro text-muted-foreground mb-1.5">{t('chat.questionCard.selectMultiple')}</div>
@@ -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');
});
});
@@ -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 (
<SimpleMarkdownRenderer
content={content}
variant="tool"
className={classes}
fallbackContent={<div className={cn(classes, 'whitespace-pre-wrap')}>{content}</div>}
/>
);
}
@@ -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<ToolExpandedContentProps> = React.memo(({
<div className="space-y-2">
{parsedQA.map((qa, index) => (
<div key={index} className="space-y-0.5">
<div className="typography-micro text-muted-foreground">{qa.question}</div>
<QuestionMarkdown content={qa.question} size="micro" className="text-muted-foreground" />
<div className="typography-meta text-foreground whitespace-pre-wrap">{qa.answer}</div>
</div>
))}
@@ -1709,7 +1710,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
{q.header ? (
<div className="typography-micro text-muted-foreground">{coerceToText(q.header)}</div>
) : null}
<div className="typography-meta text-foreground">{coerceToText(q.question)}</div>
<QuestionMarkdown content={coerceToText(q.question)} size="meta" className="text-foreground" />
{Array.isArray(q.options) && q.options.length > 0 ? (
<div className="flex flex-wrap gap-1 mt-0.5">
{q.options.map((opt) => (
+12
View File
@@ -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);
+4
View File
@@ -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,