mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 00:02:56 +00:00
De-emphasize reasoning headlines with observed timing labels
This commit is contained in:
@@ -153,6 +153,8 @@ export const MessageRow = memo(function MessageRow({
|
||||
return (
|
||||
<div ref={rowRef}>
|
||||
<NativeChatReasoningRow
|
||||
blockId={message.id}
|
||||
isStreaming={activeTurnIsWorking}
|
||||
markdown={markdown}
|
||||
onLinkClick={onLinkClick}
|
||||
allowFileUriLinks={allowFileUriLinks}
|
||||
|
||||
@@ -10,16 +10,19 @@ vi.mock('@/components/sidebar/CommentMarkdown', () => ({
|
||||
default: ({ content }: { content: string }) => <div data-testid="markdown">{content}</div>
|
||||
}))
|
||||
|
||||
afterEach(cleanup)
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('reasoning disclosure', () => {
|
||||
it('starts collapsed with the first meaningful line and does not mount markdown', () => {
|
||||
it('shows the unobserved-history fallback collapsed without mounting markdown', () => {
|
||||
render(<NativeChatReasoningRow markdown={'\n\nInspecting the request\nFull reasoning'} />)
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Reasoning: Inspecting the request' })
|
||||
screen.getByRole('button', { name: 'Reasoning: Thought for a few seconds' })
|
||||
).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.queryByTestId('markdown')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/Thought for/)).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button')).not.toHaveTextContent(/\d/)
|
||||
})
|
||||
|
||||
it('expands through a native button and preserves disclosure state through revisions', () => {
|
||||
@@ -44,10 +47,46 @@ describe('reasoning disclosure', () => {
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('bounds a growing summary without changing the collapsed layout', () => {
|
||||
render(<NativeChatReasoningRow markdown={'a'.repeat(200)} />)
|
||||
expect(screen.getByRole('button')).toHaveTextContent(`${'a'.repeat(120)}…`)
|
||||
it('shows Thinking while streaming and freezes the locally observed duration on settlement', () => {
|
||||
const now = vi.spyOn(Date, 'now').mockReturnValue(100_000)
|
||||
const { rerender } = render(
|
||||
<NativeChatReasoningRow blockId="a" markdown="Inspecting" isStreaming />
|
||||
)
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Thinking...')
|
||||
expect(screen.getByText('Thinking...')).toHaveClass('animate-pulse')
|
||||
now.mockReturnValue(105_000)
|
||||
rerender(<NativeChatReasoningRow blockId="a" markdown="Inspecting more" isStreaming />)
|
||||
now.mockReturnValue(112_000)
|
||||
rerender(<NativeChatReasoningRow blockId="a" markdown="Done" isStreaming={false} />)
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Thought for 12s')
|
||||
now.mockReturnValue(180_000)
|
||||
rerender(<NativeChatReasoningRow blockId="a" markdown="Done again" />)
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Thought for 12s')
|
||||
expect(screen.queryByTestId('markdown')).not.toBeInTheDocument()
|
||||
rerender(<NativeChatReasoningRow blockId="history" markdown="Replayed" />)
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Thought for a few seconds')
|
||||
expect(screen.getByRole('button')).not.toHaveTextContent(/\d/)
|
||||
rerender(<NativeChatReasoningRow blockId="b" markdown="New block" isStreaming />)
|
||||
now.mockReturnValue(245_000)
|
||||
rerender(<NativeChatReasoningRow blockId="b" markdown="New block done" />)
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Thought for 1m 5s')
|
||||
})
|
||||
|
||||
it('resets the clock when block identity changes without a streaming-state transition', () => {
|
||||
const now = vi.spyOn(Date, 'now').mockReturnValue(10_000)
|
||||
const { rerender } = render(<NativeChatReasoningRow blockId="a" markdown="First" isStreaming />)
|
||||
now.mockReturnValue(30_000)
|
||||
rerender(<NativeChatReasoningRow blockId="b" markdown="Second" isStreaming />)
|
||||
now.mockReturnValue(33_000)
|
||||
rerender(<NativeChatReasoningRow blockId="b" markdown="Second done" />)
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Thought for 3s')
|
||||
})
|
||||
|
||||
it('never displays zero seconds for a briefly observed stream', () => {
|
||||
vi.spyOn(Date, 'now').mockReturnValue(100_000)
|
||||
const { rerender } = render(<NativeChatReasoningRow markdown="Brief" isStreaming />)
|
||||
rerender(<NativeChatReasoningRow markdown="Brief" />)
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Thought for 1s')
|
||||
})
|
||||
|
||||
it('uses the existing message prose pipeline for reasoning-role messages', () => {
|
||||
@@ -58,7 +97,24 @@ describe('reasoning disclosure', () => {
|
||||
timestamp: 1,
|
||||
blocks: [{ type: 'text', text: 'Inspecting the request\nFull reasoning' }]
|
||||
}
|
||||
render(<MessageRow message={message} expandSignal={false} onScrollMessageToTop={vi.fn()} />)
|
||||
const { rerender } = render(
|
||||
<MessageRow
|
||||
message={message}
|
||||
activeTurnIsWorking
|
||||
expandSignal={false}
|
||||
onScrollMessageToTop={vi.fn()}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Thinking...')
|
||||
rerender(
|
||||
<MessageRow
|
||||
message={{ ...message, id: 'history', timestamp: 1 }}
|
||||
activeTurnIsWorking={false}
|
||||
expandSignal={false}
|
||||
onScrollMessageToTop={vi.fn()}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Thought for a few seconds')
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(screen.getByTestId('markdown')).toHaveTextContent('Full reasoning')
|
||||
})
|
||||
|
||||
@@ -1,32 +1,81 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import CommentMarkdown, {
|
||||
type CommentMarkdownLinkClickHandler
|
||||
} from '@/components/sidebar/CommentMarkdown'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { formatNativeChatDuration } from '../../../../shared/native-chat-turn-status'
|
||||
|
||||
export function NativeChatReasoningRow({
|
||||
markdown,
|
||||
isStreaming = false,
|
||||
blockId = 'default',
|
||||
onLinkClick,
|
||||
allowFileUriLinks
|
||||
}: {
|
||||
markdown: string
|
||||
isStreaming?: boolean
|
||||
blockId?: string
|
||||
onLinkClick?: CommentMarkdownLinkClickHandler
|
||||
allowFileUriLinks?: boolean
|
||||
}): React.JSX.Element | null {
|
||||
const firstLine = markdown.trim().split('\n', 1)[0]?.trim()
|
||||
if (!firstLine) {
|
||||
const [timing, setTiming] = useState(() => ({
|
||||
blockId,
|
||||
streaming: isStreaming,
|
||||
startedAt: isStreaming ? Date.now() : null,
|
||||
elapsedMs: null as number | null
|
||||
}))
|
||||
useEffect(() => {
|
||||
const now = Date.now()
|
||||
setTiming((previous) => {
|
||||
if (previous.blockId !== blockId) {
|
||||
return {
|
||||
blockId,
|
||||
streaming: isStreaming,
|
||||
startedAt: isStreaming ? now : null,
|
||||
elapsedMs: null
|
||||
}
|
||||
}
|
||||
if (previous.streaming === isStreaming) {
|
||||
return previous
|
||||
}
|
||||
// Only an observed stream supplies a duration; journal history has no local start.
|
||||
return {
|
||||
...previous,
|
||||
streaming: isStreaming,
|
||||
startedAt: isStreaming ? now : previous.startedAt,
|
||||
elapsedMs: isStreaming || previous.startedAt === null ? null : now - previous.startedAt
|
||||
}
|
||||
})
|
||||
}, [blockId, isStreaming])
|
||||
if (!markdown.trim()) {
|
||||
return null
|
||||
}
|
||||
const summary = firstLine.length > 120 ? `${firstLine.slice(0, 120)}…` : firstLine
|
||||
const label = translate('components.native-chat.reasoning', 'Reasoning')
|
||||
const duration = timing.blockId === blockId ? timing.elapsedMs : null
|
||||
const headline = isStreaming
|
||||
? translate('components.native-chat.thinking', 'Thinking...')
|
||||
: duration === null
|
||||
? translate('components.native-chat.thoughtForFewSeconds', 'Thought for a few seconds')
|
||||
: translate('components.native-chat.thoughtForDuration', 'Thought for {{duration}}', {
|
||||
duration: formatNativeChatDuration(Math.max(1, duration / 1000))
|
||||
})
|
||||
|
||||
return (
|
||||
<Collapsible className="min-w-0 text-sm text-muted-foreground">
|
||||
<CollapsibleTrigger className="group flex w-full min-w-0 items-center gap-1.5 rounded-sm py-1 text-left hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
||||
<CollapsibleTrigger className="group flex w-full min-w-0 items-center gap-1.5 rounded-sm py-1 text-left text-[13px] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
||||
<span className="sr-only">{label}: </span>
|
||||
<span className="min-w-0 flex-1 truncate">{summary}</span>
|
||||
<ChevronRight className="size-3.5 shrink-0 transition-transform group-data-[state=open]:rotate-90 motion-reduce:transition-none" />
|
||||
<span
|
||||
className={
|
||||
isStreaming
|
||||
? 'min-w-0 truncate animate-pulse motion-reduce:animate-none'
|
||||
: 'min-w-0 truncate'
|
||||
}
|
||||
>
|
||||
{headline}
|
||||
</span>
|
||||
<ChevronRight className="ml-auto size-3.5 shrink-0 transition-transform group-data-[state=open]:rotate-90 motion-reduce:transition-none" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-1 pl-4 italic">
|
||||
<CommentMarkdown
|
||||
|
||||
@@ -17192,7 +17192,10 @@
|
||||
"conversationCommand": {
|
||||
"pendingWork": "Wait for pending work and messages to finish before using this command.",
|
||||
"unconfirmed": "Conversation operation was not confirmed."
|
||||
}
|
||||
},
|
||||
"thinking": "Thinking...",
|
||||
"thoughtForFewSeconds": "Thought for a few seconds",
|
||||
"thoughtForDuration": "Thought for {{duration}}"
|
||||
},
|
||||
"tab": {
|
||||
"bar": {
|
||||
|
||||
Reference in New Issue
Block a user