Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions wework/e2e/desktop/scenarios/streaming-text.scenario.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const ORDER_COMPLETION_PREFIX = 'WEWORK_DESKTOP_E2E_ORDER_COMPLETION'
const ORDER_FOLLOW_UP_COUNT = 26
const HIDDEN_REASONING = 'WEWORK_DESKTOP_E2E_HIDDEN_REASONING_CONTENT'
const REASONING_SUMMARY = 'WEWORK_DESKTOP_E2E_REASONING_SUMMARY'
const REASONING_PREVIEW = REASONING_SUMMARY.replaceAll('_', ' ')
const INITIAL_PROMPT = 'WEWORK_DESKTOP_E2E_STREAMING_TEXT_INITIAL'
const HISTORY_PROMPT_PREFIX = 'WEWORK_DESKTOP_E2E_STREAMING_TEXT_HISTORY'
const PROMPT = 'WEWORK_DESKTOP_E2E_STREAMING_TEXT: keep the partial response active until released.'
Expand Down Expand Up @@ -457,7 +458,9 @@ export function createDesktopScenario({
let releaseAppend
let releaseResponse
let releaseStart
let releaseToolCompletion
let resolveRequest
let resolveToolFollowUp
let targetRequest
const appendRelease = new Promise(resolve => {
releaseAppend = resolve
Expand All @@ -471,6 +474,12 @@ export function createDesktopScenario({
const requestReceived = new Promise(resolve => {
resolveRequest = resolve
})
const toolCompletionRelease = new Promise(resolve => {
releaseToolCompletion = resolve
})
const toolFollowUpReceived = new Promise(resolve => {
resolveToolFollowUp = resolve
})

const verifyStoppedTurnOrder = async control => {
await control.command('click', '[data-testid="new-chat-button"]')
Expand Down Expand Up @@ -571,6 +580,9 @@ export function createDesktopScenario({
return true
}
if (toolRegressionStage === 'awaiting-tool-output' && requestContainsToolOutput(body)) {
toolRegressionStage = 'awaiting-completion-release'
resolveToolFollowUp()
await toolCompletionRelease
toolRegressionStage = 'complete'
response.writeHead(200, { 'Content-Type': 'text/event-stream; charset=utf-8' })
response.end(
Expand Down Expand Up @@ -687,6 +699,13 @@ export function createDesktopScenario({
}
await control.command('fill', COMPOSER_SELECTOR, { value: TOOL_REGRESSION_PROMPT })
await control.command('press', COMPOSER_SELECTOR, { key: 'Enter' })
await toolFollowUpReceived
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
await control.command('waitFor', THINKING_INDICATOR_SELECTOR, {
text: `正在思考 · ${REASONING_PREVIEW}`,
timeoutMs: uiTimeoutMs,
})
await capture(control, 'streaming-text-00-live-reasoning-summary.png')
releaseToolCompletion()
await control.command('waitFor', '[data-testid="message-assistant"]', {
text: TOOL_COMPLETION,
timeoutMs: uiTimeoutMs,
Expand All @@ -703,7 +722,7 @@ export function createDesktopScenario({
false,
'The collapsed reasoning disclosure exposed its full summary'
)
await capture(control, 'streaming-text-00-processing-collapsed.png')
await capture(control, 'streaming-text-01-processing-collapsed.png')
await control.command('click', '[data-testid="final-processing-toggle"]')
const expandedProcessingSnapshot = JSON.parse(
await control.command('snapshot', ACTIVE_WORKBENCH_SELECTOR)
Expand All @@ -718,7 +737,7 @@ export function createDesktopScenario({
false,
'The completed response retained its reasoning summary'
)
await capture(control, 'streaming-text-01-reasoning-removed.png')
await capture(control, 'streaming-text-02-reasoning-removed.png')

await control.command('click', '[data-testid="new-chat-button"]')
await control.command('waitFor', COMPOSER_SELECTOR, { timeoutMs: uiTimeoutMs })
Expand Down
37 changes: 34 additions & 3 deletions wework/src/components/chat/AssistantThinkingIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
import { useTranslation } from '@/hooks/useTranslation'

export function AssistantThinkingIndicator() {
const THINKING_PREVIEW_MAX_LENGTH = 96

export function AssistantThinkingIndicator({
content = '',
testId = 'thinking-indicator',
}: {
content?: string
testId?: string
}) {
const { t } = useTranslation('chat')
const preview = buildThinkingPreview(content)
const text = preview ? `${t('thinking.running')} · ${preview}` : t('thinking.running')

return (
<div className="inline-flex items-center text-sm" data-testid="thinking-indicator">
<span className="waiting-thinking-text">{t('thinking.running')}</span>
<div className="inline-flex min-w-0 max-w-full items-center text-sm" data-testid={testId}>
<span className="waiting-thinking-text min-w-0 truncate">{text}</span>
</div>
)
}

function buildThinkingPreview(content: string): string {
const normalized = content
.replace(/```[\s\S]*?```/g, ' ')
.replace(/`([^`]*)`/g, '$1')
.replace(/[#>*_[\]()-]+/g, ' ')
.replace(/\s+/g, ' ')
.trim()

if (!normalized) return ''

const segments = normalized
.split(/[。!?!?]+|\.(?=\s|$)/)
.map(segment => segment.trim())
.filter(Boolean)
const preview = segments[segments.length - 1] ?? normalized

return preview.length <= THINKING_PREVIEW_MAX_LENGTH
? preview
: `${preview.substring(0, THINKING_PREVIEW_MAX_LENGTH)}...`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
49 changes: 46 additions & 3 deletions wework/src/components/chat/MessageList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1704,14 +1704,14 @@ describe('MessageList', () => {
expect(screen.queryByText('正在执行 pwd')).not.toBeInTheDocument()
})

test('shows the live reasoning summary while reasoning is streaming', () => {
test('shows the latest reasoning summary while the assistant turn is streaming', () => {
const blocks: ProcessingBlock[] = [
{
id: 'thinking-1',
subtaskId: 11,
type: 'thinking',
content: '检查运行日志并定位事件边界',
status: 'streaming',
status: 'done',
createdAt: 1770000000000,
},
]
Expand All @@ -1731,9 +1731,52 @@ describe('MessageList', () => {
/>
)

const preview = screen.getByTestId('thinking-live-preview')
const preview = screen.getByTestId('thinking-indicator')
expect(preview).toHaveTextContent('正在思考')
expect(preview).toHaveTextContent('·')
expect(preview).toHaveTextContent('检查运行日志并定位事件边界')
expect(screen.queryByTestId('thinking-live-preview')).not.toBeInTheDocument()
})

test('keeps the latest reasoning summary on the thinking row after a tool completes', () => {
const blocks: ProcessingBlock[] = [
{
id: 'thinking-1',
subtaskId: 11,
type: 'thinking',
content: '先检查监督线程与后续请求的事件衔接。',
status: 'done',
createdAt: 1770000000000,
},
{
id: 'tool-1',
subtaskId: 11,
type: 'tool',
toolName: 'search_query',
toolInput: { q: 'runtime supervisor events' },
status: 'done',
createdAt: 1770000001000,
},
]

render(
<MessageList
messages={[
{
id: 'assistant-blocks',
role: 'assistant',
content: '',
status: 'streaming',
blocks,
createdAt: '2026-06-24T08:00:01.000Z',
},
]}
/>
)

expect(screen.getByTestId('tool-block-thinking')).toHaveTextContent(
'正在思考 · 先检查监督线程与后续请求的事件衔接'
)
expect(screen.queryByTestId('thinking-indicator')).not.toBeInTheDocument()
})

Expand Down
25 changes: 20 additions & 5 deletions wework/src/components/chat/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1847,15 +1847,24 @@ function getDisplayProcessingBlocks(
: block
)
.filter(block => {
if (block.type === 'thinking') {
return block.status !== 'done' && block.status !== 'error' && Boolean(block.content.trim())
}
if (block.type === 'thinking') return false
if (block.type !== 'text') return true

return Boolean(block.content.trim())
})
}

function getLatestThinkingContent(blocks: ProcessingBlock[] | undefined): string {
if (!blocks?.length) return ''

for (let index = blocks.length - 1; index >= 0; index -= 1) {
const block = blocks[index]
if (block?.type === 'thinking' && block.content.trim()) return block.content
}

return ''
}

function getWebSearchToolBlocks(blocks: ProcessingBlock[]) {
return blocks.filter(
(block): block is Extract<ProcessingBlock, { type: 'tool' }> =>
Expand Down Expand Up @@ -1933,6 +1942,7 @@ function AssistantMessage({
const hasBlocks = displayBlocks.length > 0
const hasVisibleContent = Boolean(visibleContent.trim())
const isStreaming = !isCancelled && message.status === 'streaming'
const activeThinkingContent = isStreaming ? getLatestThinkingContent(message.blocks) : ''
const hasRunningBlocks = hasRunningProcessingBlocks(displayBlocks)
const isAssistantRunning = isStreaming || hasRunningBlocks
const canShowFinalArtifacts = !isAssistantRunning
Expand Down Expand Up @@ -1995,6 +2005,7 @@ function AssistantMessage({
segment.kind === 'tool' &&
!processingSegments.slice(index + 1).some(candidate => candidate.kind === 'tool')
}
thinkingContent={activeThinkingContent}
showSummary={segment.kind === 'tool'}
stateKey={`${processingStateKey}:${index}`}
onOpenWorkspaceFile={onOpenWorkspaceFile}
Expand Down Expand Up @@ -2061,7 +2072,9 @@ function AssistantMessage({
) : (
processingTimeline
)}
{shouldShowThinking && !hasVisibleContent && <AssistantThinkingIndicator />}
{shouldShowThinking && !hasVisibleContent && (
<AssistantThinkingIndicator content={activeThinkingContent} />
)}
{generatedImages.length > 0 ? <GeneratedImageGallery images={generatedImages} /> : null}
{message.contentTruncated ? (
<ContentTruncatedNotice
Expand All @@ -2080,7 +2093,9 @@ function AssistantMessage({
/>
</div>
) : null}
{shouldShowThinking && hasVisibleContent && <AssistantThinkingIndicator />}
{shouldShowThinking && hasVisibleContent && (
<AssistantThinkingIndicator content={activeThinkingContent} />
)}
{canShowFinalArtifacts && hasVisibleContent && webSearchSources.length > 0 && (
<WebSearchSourcesChip sources={webSearchSources} />
)}
Expand Down
40 changes: 0 additions & 40 deletions wework/src/components/chat/blocks/ToolBlockItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,6 @@ import { describe, expect, test, vi } from 'vitest'
import { ToolBlockItem } from './ToolBlockItem'
import type { ProcessingBlock } from '@/types/workbench'

const streamingThinkingBlock: ProcessingBlock = {
id: 'thinking-1',
subtaskId: 1,
type: 'thinking',
content: 'First step. Latest visible thought',
status: 'streaming',
createdAt: 1770000000000,
}

const streamingTextBlock: ProcessingBlock = {
id: 'text-1',
subtaskId: 1,
Expand Down Expand Up @@ -79,37 +70,6 @@ describe('ToolBlockItem', () => {
expect(screen.getByRole('button')).toHaveClass('min-h-8')
})

test('renders streaming thinking as a single live preview row', () => {
render(<ToolBlockItem block={streamingThinkingBlock} />)

const preview = screen.getByTestId('thinking-live-preview')

expect(preview).toHaveTextContent('正在思考')
expect(preview.firstElementChild).toHaveTextContent('正在思考')
expect(preview.firstElementChild?.tagName).toBe('SPAN')
expect(preview).toHaveTextContent('Latest visible thought')
expect(screen.queryByText('First step. Latest visible thought')).not.toBeInTheDocument()
expect(screen.queryByTestId('thinking-toggle-button')).not.toBeInTheDocument()
})

test('does not render completed thinking placeholders', () => {
render(
<ToolBlockItem
block={{
...streamingThinkingBlock,
status: 'done',
content: 'I will inspect the repository before answering.',
}}
/>
)

expect(screen.queryByTestId('thinking-toggle-button')).not.toBeInTheDocument()
expect(screen.queryByTestId('thinking-detail')).not.toBeInTheDocument()
expect(
screen.queryByText('I will inspect the repository before answering.')
).not.toBeInTheDocument()
})

test('renders streaming process text directly in the timeline', () => {
render(<ToolBlockItem block={streamingTextBlock} />)

Expand Down
53 changes: 1 addition & 52 deletions wework/src/components/chat/blocks/ToolBlockItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { WebSearchActivityRows } from './WebSearchSources'
import { getWebSearchActivityItems } from './webSearchActivity'
import { usePersistentProcessingExpansion } from './processingExpansionState'

const THINKING_PREVIEW_MAX_LENGTH = 96
const INLINE_DIFF_MAX_LINES = 96

interface ToolBlockItemProps {
Expand Down Expand Up @@ -81,7 +80,7 @@ export function ToolBlockItem({
}, [block.type, expanded, onExpandedChange])

if (block.type === 'thinking') {
return <ThinkingBlockItem block={block} isRunning={isRunning} />
return null
}
if (block.type === 'text') {
return (
Expand Down Expand Up @@ -852,37 +851,6 @@ function basename(path: string): string {
return path.split(/[\\/]/).filter(Boolean).pop() || path
}

function ThinkingBlockItem({
block,
isRunning,
}: {
block: Extract<ProcessingBlock, { type: 'thinking' }>
isRunning: boolean
}) {
const { t } = useTranslation('chat')

if (!block.content || !isRunning) return null

const preview = buildBlockPreview(block.content)

return (
<div className="min-w-0 overflow-x-hidden text-sm" data-processing-block-id={block.id}>
<div
className="flex max-w-full items-center gap-1.5 text-text-secondary"
role="status"
aria-live="polite"
data-testid="thinking-live-preview"
>
<span className="shrink-0">{t('thinking.running')}</span>
<span className="shrink-0 text-text-muted">·</span>
<span className="min-w-0 truncate text-text-muted">
{preview || t('thinking.updating')}
</span>
</div>
</div>
)
}

function ProcessTextBlockItem({
block,
isRunning,
Expand Down Expand Up @@ -1586,22 +1554,3 @@ function truncate(str: string, maxLen: number): string {
if (str.length <= maxLen) return str
return str.substring(0, maxLen) + '...'
}

function buildBlockPreview(content: string): string {
const normalized = content
.replace(/```[\s\S]*?```/g, ' ')
.replace(/`([^`]*)`/g, '$1')
.replace(/[#>*_[\]()-]+/g, ' ')
.replace(/\s+/g, ' ')
.trim()

if (!normalized) return ''

const segments = normalized
.split(/[。!?!?]+|\.(?=\s|$)/)
.map(segment => segment.trim())
.filter(Boolean)
const preview = segments[segments.length - 1] ?? normalized

return truncate(preview, THINKING_PREVIEW_MAX_LENGTH)
}
Loading
Loading