fix(chat): float approvals above composer

This commit is contained in:
xiaohan.yuan committed 2026-10-20 16:31:03 +08:00
1 parent efb44f9340
commit 1b52df0603
5 files changed
+321 -127

No files matched your search

@@ -1,5 +1,7 @@
import assert from 'node:assert/strict'
import { renderToStaticMarkup } from 'react-dom/server'
import MessageBubble from '../src/renderer/src/components/MessageBubble'
import type { ChatMessage } from '../src/shared/types'
import {
AskUserQuestion,
PlanCreateBlock,
@@ -210,6 +212,36 @@ const cancelledPermission = renderToStaticMarkup(
)
assert.match(cancelledPermission, /Cancelled/)
const terminalApprovalMessage: ChatMessage = {
id: 'message-terminal-approval',
role: 'assistant',
content: 'Browser opened.',
parts: [{ type: 'text', text: 'Browser opened.' }],
status: 'done',
createdAt: 1,
interactions: [{
id: 'approval-terminal',
runId: 'run-terminal',
toolCallId: 'browser-tool',
title: 'Allow browser access?',
status: 'answered',
kind: 'approval',
requestId: 'request-terminal',
options: permissionOptions
}]
}
const terminalApprovalMarkup = renderToStaticMarkup(<MessageBubble message={terminalApprovalMessage} />)
assert.doesNotMatch(terminalApprovalMarkup, /conversation-high-credit-approval/,
'a completed non-inline approval must not leave a terminal card in the conversation')
const activeApprovalMarkup = renderToStaticMarkup(<MessageBubble message={{
...terminalApprovalMessage,
status: 'streaming',
interactions: terminalApprovalMessage.interactions?.map(interaction => ({ ...interaction, status: 'pending' as const }))
}} />)
assert.doesNotMatch(activeApprovalMarkup, /conversation-high-credit-approval/,
'an active approval belongs to the composer floating slot, not the conversation stream')
const questionConfig = {
title: 'Choose a runtime',
questions: [
@@ -229,6 +261,34 @@ const questionConfig = {
}
]
}
const activeQuestionMessage: ChatMessage = {
id: 'message-active-question',
role: 'assistant',
content: '',
tools: [{
id: 'question-tool',
name: 'AskUserQuestion',
args: { questions: questionConfig.questions },
argsRaw: '{}',
status: 'running'
}],
parts: [{ type: 'tool', toolId: 'question-tool' }],
status: 'streaming',
createdAt: 2,
interactions: [{
id: 'question-active',
runId: 'run-question',
toolCallId: 'question-tool',
title: 'Need input',
status: 'pending',
kind: 'question',
questions: questionConfig.questions.map(question => ({ id: question.id, question: question.question, options: question.options }))
}]
}
const activeQuestionMarkup = renderToStaticMarkup(<MessageBubble message={activeQuestionMessage} />)
assert.doesNotMatch(activeQuestionMarkup, /ask-user-question/,
'an active user question must render only in the composer floating slot')
const questionMarkup = renderToStaticMarkup(<QuestionFloating config={questionConfig} />)
assert.match(questionMarkup, /Which runtime\?/)
assert.match(questionMarkup, /Node\.js/)