Render scheduled task summaries as markdown

Scheduled task results can contain assistant-authored markdown, but the
runs panel previously displayed that content as plain pre-wrapped text.
Reusing the desktop markdown renderer keeps task summaries aligned with
chat and settings surfaces while adding a compact prose variant for the
narrow log panel.

Constraint: Desktop task logs need dense formatting without introducing a new markdown dependency
Rejected: Keep whitespace-pre-wrap summary text | markdown syntax remains visible in completed task runs
Confidence: high
Scope-risk: narrow
Directive: Keep scheduled run summary rendering on the shared MarkdownRenderer path so links, lists, inline code, and future markdown fixes stay consistent
Tested: cd desktop && bun run test src/components/markdown/MarkdownRenderer.test.tsx src/components/tasks/TaskRunsPanel.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: bun run check:desktop
Tested: agent-browser E2E against isolated CLAUDE_CONFIG_DIR with real backend and Vite UI; verified strong/li/code/link DOM output and no raw markdown bold markers
Not-tested: Tauri native packaged app shell
This commit is contained in:
程序员阿江(Relakkes) 2026-05-09 22:42:48 +08:00
parent da477c2b4b
commit 065a80580b
4 changed files with 89 additions and 6 deletions

View File

@ -51,6 +51,22 @@ describe('MarkdownRenderer', () => {
expect(screen.getByText('Body copy.')).toBeInTheDocument()
})
it('applies compact prose classes for dense surfaces', () => {
const { container } = render(
<MarkdownRenderer
content={'**Done**\n\n- One\n- Two'}
variant="compact"
/>,
)
const root = container.firstChild as HTMLDivElement
expect(root).toBeInTheDocument()
expect(root.className).toContain('prose-p:text-xs')
expect(root.className).toContain('prose-li:text-xs')
expect(screen.getByText('Done')).toBeInTheDocument()
expect(screen.getByText('One')).toBeInTheDocument()
})
it('uses semantic code colors for inline code so both themes stay readable', () => {
const { container } = render(
<MarkdownRenderer content={'Use `claude-sonnet-4-6` for balanced speed.'} />,

View File

@ -6,7 +6,7 @@ import { MermaidRenderer } from '../chat/MermaidRenderer'
type Props = {
content: string
variant?: 'default' | 'document'
variant?: 'default' | 'document' | 'compact'
className?: string
}
@ -136,8 +136,24 @@ const DOCUMENT_PROSE_CLASSES = `
prose-li:my-1.5
prose-table:my-0`
function getProseClasses(variant: 'default' | 'document', className?: string) {
return [BASE_PROSE_CLASSES, variant === 'document' ? DOCUMENT_PROSE_CLASSES : '', className ?? '']
const COMPACT_PROSE_CLASSES = `
prose-p:my-1 prose-p:text-xs prose-p:leading-5 prose-p:text-[var(--color-text-secondary)]
prose-headings:mt-2 prose-headings:mb-1 prose-headings:leading-snug
prose-h1:text-base prose-h2:text-sm prose-h3:text-xs prose-h4:text-xs
prose-blockquote:my-2 prose-blockquote:border-l-2 prose-blockquote:border-[var(--color-outline-variant)] prose-blockquote:pl-3 prose-blockquote:text-[var(--color-text-secondary)]
prose-code:text-[12px]
prose-ul:my-1 prose-ol:my-1 prose-ul:pl-4 prose-ol:pl-4
prose-li:my-0.5 prose-li:text-xs prose-li:leading-5 prose-li:text-[var(--color-text-secondary)]
prose-table:text-xs
[&_.md-table-wrap]:my-2`
function getProseClasses(variant: 'default' | 'document' | 'compact', className?: string) {
return [
BASE_PROSE_CLASSES,
variant === 'document' ? DOCUMENT_PROSE_CLASSES : '',
variant === 'compact' ? COMPACT_PROSE_CLASSES : '',
className ?? '',
]
.filter(Boolean)
.join(' ')
}

View File

@ -0,0 +1,47 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom'
import { TaskRunsPanel } from './TaskRunsPanel'
import { useSettingsStore } from '../../stores/settingsStore'
import { useTaskStore } from '../../stores/taskStore'
import type { TaskRun } from '../../types/task'
afterEach(() => {
cleanup()
useSettingsStore.setState(useSettingsStore.getInitialState(), true)
useTaskStore.setState(useTaskStore.getInitialState(), true)
})
describe('TaskRunsPanel', () => {
it('renders scheduled task summaries as markdown', async () => {
const run: TaskRun = {
id: 'run-1',
taskId: 'task-1',
taskName: 'Daily summary',
startedAt: '2026-05-08T12:05:37.000Z',
status: 'completed',
prompt: 'Summarize recent commits',
output: '最近7天有3个commit主要改动\n\n**1. 2865d50 - UI无障碍改进**\n- 添加 theme-color meta 标签\n- 修复 select 标签问题',
durationMs: 12000,
sessionId: 'session-1',
}
useSettingsStore.setState({ locale: 'en' })
useTaskStore.setState({
fetchTaskRuns: vi.fn(async () => [run]),
} as Partial<ReturnType<typeof useTaskStore.getState>>)
const { container } = render(
<TaskRunsPanel taskId="task-1" onClose={vi.fn()} />,
)
await waitFor(() => expect(screen.getByRole('button', { name: 'Summary' })).toBeInTheDocument())
fireEvent.click(screen.getByRole('button', { name: 'Summary' }))
expect(screen.getByText('1. 2865d50 - UI无障碍改进')).toBeInTheDocument()
expect(container.querySelector('strong')).toHaveTextContent('1. 2865d50 - UI无障碍改进')
expect(screen.getByText('添加 theme-color meta 标签')).toBeInTheDocument()
expect(container.querySelector('li')).toHaveTextContent('添加 theme-color meta 标签')
expect(container.textContent).not.toContain('**1. 2865d50')
})
})

View File

@ -5,6 +5,7 @@ import { useTabStore } from '../../stores/tabStore'
import { useTranslation } from '../../i18n'
import { parseRunOutput } from '../../lib/parseRunOutput'
import type { TaskRun } from '../../types/task'
import { MarkdownRenderer } from '../markdown/MarkdownRenderer'
function RunOutput({ run }: { run: TaskRun }) {
const t = useTranslation()
@ -28,10 +29,13 @@ function RunOutput({ run }: { run: TaskRun }) {
)
}
// Render AI text response with proper formatting (not monospace <pre>)
return (
<div className="mt-2 p-2.5 rounded-[var(--radius-sm)] bg-[var(--color-surface-container)] text-xs text-[var(--color-text-secondary)] whitespace-pre-wrap break-words max-h-48 overflow-y-auto leading-relaxed">
{text}
<div className="mt-2 max-h-48 overflow-y-auto rounded-[var(--radius-sm)] bg-[var(--color-surface-container)] p-2.5">
<MarkdownRenderer
content={text}
variant="compact"
className="break-words"
/>
</div>
)
}