Merge pull request #2488 from pascalandr/fix/856-question-markdown
fix(chat): render question prompts as markdown
This commit is contained in:
@@ -47,8 +47,12 @@ export const MarkdownRenderer: React.FC<React.ComponentPropsWithoutRef<typeof Ma
|
|||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SimpleMarkdownRenderer: React.FC<React.ComponentPropsWithoutRef<typeof SimpleMarkdownRendererLazy>> = (props) => (
|
type SimpleMarkdownRendererProps = React.ComponentPropsWithoutRef<typeof SimpleMarkdownRendererLazy> & {
|
||||||
<React.Suspense fallback={<MobileMarkdownFallback {...props} />}>
|
fallbackContent?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SimpleMarkdownRenderer: React.FC<SimpleMarkdownRendererProps> = ({ fallbackContent, ...props }) => (
|
||||||
|
<React.Suspense fallback={fallbackContent ?? <MobileMarkdownFallback {...props} />}>
|
||||||
<SimpleMarkdownRendererLazy {...props} />
|
<SimpleMarkdownRendererLazy {...props} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import * as sessionActions from '@/sync/session-actions';
|
|||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { serializeQuestionAsJson, serializeQuestionAsMarkdown } from './questionSerializers';
|
import { serializeQuestionAsJson, serializeQuestionAsMarkdown } from './questionSerializers';
|
||||||
import { QUESTION_CUSTOM_TEXTAREA_MIN_HEIGHT, getQuestionCustomTextareaHeight } from './questionTextareaSizing';
|
import { QUESTION_CUSTOM_TEXTAREA_MIN_HEIGHT, getQuestionCustomTextareaHeight } from './questionTextareaSizing';
|
||||||
|
import { QuestionMarkdown } from './QuestionMarkdown';
|
||||||
|
|
||||||
interface QuestionCardProps {
|
interface QuestionCardProps {
|
||||||
question: QuestionRequest;
|
question: QuestionRequest;
|
||||||
@@ -423,7 +424,11 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
|
|||||||
</div>
|
</div>
|
||||||
) : activeQuestion ? (
|
) : 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 ? (
|
{isMultiple ? (
|
||||||
<div className="typography-micro text-muted-foreground mb-1.5">{t('chat.questionCard.selectMultiple')}</div>
|
<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 { useMobileAppActions } from '@/apps/mobileAppContext';
|
|||||||
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
|
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { SimpleMarkdownRenderer } from '../../MarkdownRenderer';
|
import { SimpleMarkdownRenderer } from '../../MarkdownRenderer';
|
||||||
|
import { QuestionMarkdown } from '../../QuestionMarkdown';
|
||||||
import { MessageFilesDisplay } from '../../FileAttachment';
|
import { MessageFilesDisplay } from '../../FileAttachment';
|
||||||
import { getToolMetadata } from '@/lib/toolHelpers';
|
import { getToolMetadata } from '@/lib/toolHelpers';
|
||||||
import type { ToolPart as ToolPartType, ToolState as ToolStateUnion, FilePart } from '@opencode-ai/sdk/v2';
|
import type { ToolPart as ToolPartType, ToolState as ToolStateUnion, FilePart } from '@opencode-ai/sdk/v2';
|
||||||
@@ -1407,7 +1408,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{parsedQA.map((qa, index) => (
|
{parsedQA.map((qa, index) => (
|
||||||
<div key={index} className="space-y-0.5">
|
<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 className="typography-meta text-foreground whitespace-pre-wrap">{qa.answer}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -1444,7 +1445,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
|||||||
{q.header ? (
|
{q.header ? (
|
||||||
<div className="typography-micro text-muted-foreground">{coerceToText(q.header)}</div>
|
<div className="typography-micro text-muted-foreground">{coerceToText(q.header)}</div>
|
||||||
) : null}
|
) : 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 ? (
|
{Array.isArray(q.options) && q.options.length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-1 mt-0.5">
|
<div className="flex flex-wrap gap-1 mt-0.5">
|
||||||
{q.options.map((opt) => (
|
{q.options.map((opt) => (
|
||||||
|
|||||||
@@ -1039,6 +1039,18 @@ html:not(.dark) .chat-scroll {
|
|||||||
font-size: var(--text-code) !important;
|
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. */
|
/* Reasoning markdown renders at meta size, dimmed. */
|
||||||
.markdown-content.markdown-reasoning {
|
.markdown-content.markdown-reasoning {
|
||||||
font-size: var(--text-markdown);
|
font-size: var(--text-markdown);
|
||||||
|
|||||||
@@ -41,6 +41,10 @@
|
|||||||
font-size: var(--text-code) !important;
|
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 */
|
/* 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) button:not([role="radio"]):not([role="checkbox"]):not([role="switch"]),
|
||||||
:root.mobile-pointer:not(.desktop-runtime) .btn,
|
:root.mobile-pointer:not(.desktop-runtime) .btn,
|
||||||
|
|||||||
Reference in New Issue
Block a user