Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification

# Conflicts:
#	apps/cli/tests/snapshots/queued-manual-compact/terminal.expected.txt
#	apps/web/tests/snapshots/queue-actions/preserved.expected.md
#	packages/client/runtime/README.i18n.yaml
#	packages/client/runtime/src/client/sessions/session.ts
#	packages/client/ui-conversation/README.md
#	packages/client/ui-conversation/README.zh.md
#	packages/ui/tui/README.i18n.yaml
#	packages/ui/tui/tests/snapshots/untrusted-controls.expected.txt
This commit is contained in:
_Kerman
2026-08-03 17:15:34 +08:00
187 changed files with 4546 additions and 590 deletions
@@ -36,12 +36,14 @@ describe('MessageItem arms', () => {
// Same-day clock: construct "today at 14:24" so the label stays `HH:mm`.
const now = new Date()
const time = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 14, 24).getTime()
const onFork = vi.fn()
render(
<MessageItem t={t} node={{
kind: 'user', seq: 1, time,
content: [{ type: 'text', text: 'hello bubble' }] as never,
source: null,
}}
onFork={onFork}
/>,
)
expect(screen.getByText('14:24')).toBeTruthy()
@@ -50,6 +52,8 @@ describe('MessageItem arms', () => {
expect(screen.queryByRole('button', { name: '编辑' })).toBeNull()
fireEvent.click(screen.getByRole('button', { name: '复制' }))
expect(writeText).toHaveBeenCalledWith('hello bubble')
fireEvent.click(screen.getByRole('button', { name: '在新对话中分支' }))
expect(onFork).toHaveBeenCalledWith(1)
})
it('user copy falls back to execCommand when clipboard.writeText is unavailable', () => {
@@ -74,6 +78,30 @@ describe('MessageItem arms', () => {
expect(exec).toHaveBeenCalledWith('copy')
})
it('keeps an unavailable branch focusable and explains why without sending a fork', () => {
const onFork = vi.fn()
render(
<MessageItem t={t} node={{
kind: 'user', seq: 1, time: 1_000,
content: [{ type: 'text', text: 'open turn' }] as never,
source: null,
}}
onFork={onFork}
forkUnavailable
/>,
)
const branch = screen.getByRole('button', { name: '在新对话中分支' }) as HTMLButtonElement
expect(branch.disabled).toBe(false)
expect(branch.getAttribute('aria-disabled')).toBe('true')
const reasonId = branch.getAttribute('aria-describedby')
expect(reasonId).not.toBeNull()
expect(document.getElementById(reasonId!)?.textContent).toBe('仅可从已完成轮次的最后一条消息分支')
fireEvent.click(branch)
expect(onFork).not.toHaveBeenCalled()
fireEvent.focus(branch)
expect(screen.getByRole('tooltip').textContent).toBe('仅可从已完成轮次的最后一条消息分支')
})
it('user copy stays quiet when execCommand throws or is absent', () => {
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
@@ -396,12 +424,15 @@ describe('small branch tails', () => {
})
const now = new Date()
const time = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 14, 24).getTime()
const onFork = vi.fn()
const settled = render(
<AssistantMarkdown
t={t}
blocks={[{ kind: 'text', text: 'answer body' }, { kind: 'reasoning', text: 'hidden' }]}
streaming={false}
time={time}
seq={3}
onFork={onFork}
/>,
)
expect(settled.getByText('14:24')).toBeTruthy()
@@ -409,6 +440,8 @@ describe('small branch tails', () => {
expect(settled.getByRole('button', { name: '在新对话中分支' })).toBeTruthy()
fireEvent.click(settled.getByRole('button', { name: '复制' }))
expect(writeText).toHaveBeenCalledWith('answer body')
fireEvent.click(settled.getByRole('button', { name: '在新对话中分支' }))
expect(onFork).toHaveBeenCalledWith(3)
settled.unmount()
const thinkOnly = render(