Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions docs/en/wework/developer-guide/wework-chat-state-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ When the composer is in plan mode or goal-draft mode, its bottom mode pill must

The mode pill's cancel button appears only on hover and is absolutely positioned over the left icon while that icon fades out. Do not expand the cancel button or add spacing that changes the pill width, because that causes the label to shift horizontally.

## Composer Draft Buffering

`BufferedChatInput` preserves a pane-level draft during editing and submission, while the external `value` remains the source of truth for the confirmed draft. After a non-empty draft is submitted, the local empty state must be associated with the expected empty external value instead of the text that was just submitted. Otherwise, returning the same text from a queue or guidance row for editing is mistaken for stale draft state and the composer remains empty. Changes to this path must cover the regression sequence “submit text → external value clears → edit the queued row to restore the same text.”

## Long Output Memory Boundary

The Wework chat UI must not keep complete long-running output in React state. `WorkbenchMessage.content`, thinking/text/plan block `content`, and tool block `toolOutput` must enter `messages` through the shared preview-window path:
Expand Down
4 changes: 4 additions & 0 deletions docs/zh/wework/developer-guide/wework-chat-state-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ turn 被取消后 goal 在暂停请求到达前启动下一 turn。如果 goal

模式胶囊的取消按钮仅在悬停时显示,并绝对定位覆盖左侧图标;原图标在同一状态下淡出。不要通过展开取消按钮或额外边距改变胶囊宽度,否则标签会发生横向跳动。

## Composer 草稿缓冲

`BufferedChatInput` 在输入和提交期间保留 pane 级草稿,但外部 `value` 仍是已确认草稿的信源。提交非空草稿后,本地空状态必须绑定到预期的空外部值,不能继续绑定到刚提交的文本;否则队列或引导条把同一文本送回编辑器时,会被误判为旧草稿并显示为空。维护该逻辑时必须覆盖“提交文本 → 外部清空 → 编辑队列条目恢复相同文本”的回归场景。

## 长输出内存边界

Wework 的聊天 UI 不能把持续输出的完整正文长期保存在 React state 中。`WorkbenchMessage.content`、thinking/text/plan block 的 `content`、tool block 的 `toolOutput` 都必须通过统一的预览窗口进入 `messages`:
Expand Down
40 changes: 40 additions & 0 deletions wework/src/components/chat/ChatInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,46 @@ describe('ChatInput', () => {
expect(onCancelQueuedMessage).toHaveBeenCalledWith('queued-1')
})

test('restores queued message text into the composer when editing', async () => {
function Harness() {
const [value, setValue] = useState('')
const [queuedMessages, setQueuedMessages] = useState<QueuedWorkbenchMessage[]>([
{
id: 'queued-1',
content: '先检查引导条里的文本',
status: 'queued',
createdAt: '2026-05-25T15:08:00.000+08:00',
},
])

return (
<ChatInput
value={value}
onChange={setValue}
onSubmit={vi.fn()}
disabled={false}
variant="desktop"
queuedMessages={queuedMessages}
onEditQueuedMessage={id => {
const message = queuedMessages.find(item => item.id === id)
if (!message) return
setValue(message.content)
setQueuedMessages(current => current.filter(item => item.id !== id))
}}
/>
)
}

render(<Harness />)

await userEvent.click(screen.getByTestId('queue-more-button-queued-1'))
await userEvent.click(screen.getByTestId('queue-edit-button-queued-1'))

await waitFor(() =>
expect(screen.getByTestId('chat-message-input')).toHaveTextContent('先检查引导条里的文本')
)
})

test('shows lightweight interrupt action while guidance is sending', async () => {
const onInterruptAndSendQueuedMessage = vi.fn()

Expand Down
20 changes: 20 additions & 0 deletions wework/src/components/layout/BufferedChatInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ describe('BufferedChatInput', () => {
})
})

test('restores submitted text when it is externally returned for editing', async () => {
const onSubmit = vi.fn()
const props = {
onChange: vi.fn(),
onSubmit,
disabled: false,
}
const { rerender } = render(<BufferedChatInput {...props} value="queued message" />)

await userEvent.click(screen.getByTestId('send-message-button'))
expect(onSubmit).toHaveBeenCalledWith('queued message')

rerender(<BufferedChatInput {...props} value="" />)
rerender(<BufferedChatInput {...props} value="queued message" />)

await waitFor(() => {
expect(screen.getByTestId('chat-message-input')).toHaveValue('queued message')
})
})

test('appends an insertion without replacing the buffered draft', async () => {
const props = {
value: '',
Expand Down
4 changes: 2 additions & 2 deletions wework/src/components/layout/BufferedChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ export const BufferedChatInput = memo(function BufferedChatInput({
}
if (submittedDraft.trim()) {
draftRef.current = ''
setDraftState({ scopeKey, sourceValue: value, draft: '' })
setDraftState({ scopeKey, sourceValue: '', draft: '' })
}
},
[draft, onSubmit, scopeKey, value]
[draft, onSubmit, scopeKey]
)

return <ChatInput {...props} value={draft} onChange={setDraft} onSubmit={handleSubmit} />
Expand Down
Loading