fix: remove legacy message rewind entry

The chat surface now relies on the current-turn changes card for file rollback, so the older per-message hover rewind affordance was removed to avoid two competing rollback models.

Constraint: Current-turn undo still depends on the existing checkpoint rewind API.
Rejected: Keep both rewind entry points | duplicate rollback affordances make the product harder to explain and test.
Confidence: high
Scope-risk: moderate
Directive: Prefer adding rollback behavior through the current-turn change card instead of restoring per-message hover rewind.
Tested: bun test src/server/__tests__/sessions.test.ts
Tested: cd desktop && bun run test
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Not-tested: Browser E2E scripts were updated but not executed in this commit.
This commit is contained in:
程序员阿江(Relakkes) 2026-04-30 18:28:08 +08:00
parent bcfe042fc8
commit 6d56f3c21d
14 changed files with 49 additions and 421 deletions

View File

@ -137,10 +137,10 @@ fi
${AB} screenshot "${ARTIFACT_DIR}/03-after-edit.png" >/dev/null
${AB} eval "const button = [...document.querySelectorAll('button')].find((node) => node.getAttribute('aria-label') === 'Rewind to here'); if (!button) throw new Error('Rewind button not found'); button.click();"
${AB} find role button click --name "Undo current turn changes"
${AB} wait 1500
${AB} screenshot "${ARTIFACT_DIR}/04-rewind-modal.png" >/dev/null
${AB} find role button click --name "Rewind here"
${AB} screenshot "${ARTIFACT_DIR}/04-undo-current-turn-confirm.png" >/dev/null
${AB} find role button click --name "Undo current turn"
for _ in $(seq 1 120); do
if grep -q "before-rewind" "${TARGET_FILE}" && ! grep -q "after-rewind" "${TARGET_FILE}"; then

View File

@ -171,10 +171,10 @@ wait_for_file_contains "${README_FILE}" "Second turn touched README" 60
wait_for_file_contains "${GENERATED_FILE}" "second-turn" 60
${AB} screenshot "${ARTIFACT_DIR}/04-after-second-turn.png" >/dev/null
${AB} eval "const buttons = [...document.querySelectorAll('button')].filter((node) => node.getAttribute('aria-label') === 'Rewind to here'); if (buttons.length < 2) throw new Error('Expected at least two Rewind buttons, found ' + buttons.length); buttons[buttons.length - 1].click();"
${AB} find role button click --name "Undo current turn changes"
${AB} wait 1500
${AB} screenshot "${ARTIFACT_DIR}/05-rewind-second-turn-modal.png" >/dev/null
${AB} find role button click --name "Rewind here"
${AB} screenshot "${ARTIFACT_DIR}/05-undo-second-turn-confirm.png" >/dev/null
${AB} find role button click --name "Undo current turn"
for _ in $(seq 1 120); do
if grep -q "turn-one" "${APP_FILE}" \

View File

@ -1,4 +1,4 @@
import { describe, it, expect, vi } from 'vitest'
import { beforeEach, describe, it, expect, vi } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
@ -46,9 +46,14 @@ import { ToolInspection } from '../pages/ToolInspection'
import { Sidebar } from '../components/layout/Sidebar'
import { UserMessage } from '../components/chat/UserMessage'
import { useChatStore } from '../stores/chatStore'
import { useSettingsStore } from '../stores/settingsStore'
import { useSessionStore } from '../stores/sessionStore'
import { useTabStore } from '../stores/tabStore'
beforeEach(() => {
useSettingsStore.setState({ locale: 'en' })
})
/**
* Core rendering tests: content-only pages must render without crashing
* and contain key structural elements from the prototype.
@ -576,7 +581,8 @@ describe('Design system compliance', () => {
html.includes('C47A5A') ||
html.includes('8F482F') ||
html.includes('var(--color-brand)') ||
html.includes('bg-[var(--color-brand)]'),
html.includes('bg-[var(--color-brand)]') ||
html.includes('var(--gradient-btn-primary)'),
).toBe(true)
unmount()
}

View File

@ -24,6 +24,7 @@ vi.mock('../../api/sessions', () => ({
import { AskUserQuestion } from './AskUserQuestion'
import { useChatStore } from '../../stores/chatStore'
import { useSettingsStore } from '../../stores/settingsStore'
import { useTabStore } from '../../stores/tabStore'
const ACTIVE_TAB = 'active-tab'
@ -31,6 +32,7 @@ const ACTIVE_TAB = 'active-tab'
describe('AskUserQuestion', () => {
beforeEach(() => {
sendMock.mockReset()
useSettingsStore.setState({ locale: 'en' })
useTabStore.setState({
activeTabId: ACTIVE_TAB,
tabs: [{ sessionId: ACTIVE_TAB, title: 'Test', type: 'session', status: 'idle' }],

View File

@ -73,12 +73,14 @@ vi.mock('../../api/computerUse', () => ({
}))
import { useChatStore } from '../../stores/chatStore'
import { useSettingsStore } from '../../stores/settingsStore'
import { ComputerUsePermissionModal } from './ComputerUsePermissionModal'
describe('ComputerUsePermissionModal', () => {
beforeEach(() => {
sendMock.mockReset()
openSettingsMock.mockReset()
useSettingsStore.setState({ locale: 'en' })
useChatStore.setState({ sessions: {} })
})

View File

@ -4,6 +4,7 @@ import '@testing-library/jest-dom'
import { FileSearchMenu } from './FileSearchMenu'
import { ApiError } from '../../api/client'
import { filesystemApi } from '../../api/filesystem'
import { useSettingsStore } from '../../stores/settingsStore'
vi.mock('../../api/filesystem', () => ({
filesystemApi: {
@ -15,6 +16,7 @@ vi.mock('../../api/filesystem', () => ({
describe('FileSearchMenu', () => {
beforeEach(() => {
vi.clearAllMocks()
useSettingsStore.setState({ locale: 'en' })
})
it('shows an explicit error when directory browsing is denied', async () => {

View File

@ -3,22 +3,17 @@ import { CopyButton } from '../shared/CopyButton'
type Props = {
copyText?: string
copyLabel: string
onRewind?: () => void
rewindLabel?: string
align?: 'start' | 'end'
}
export function MessageActionBar({
copyText,
copyLabel,
onRewind,
rewindLabel = 'Rewind to here',
align = 'start',
}: Props) {
const hasCopy = Boolean(copyText?.trim())
const hasRewind = Boolean(onRewind)
if (!hasCopy && !hasRewind) return null
if (!hasCopy) return null
return (
<div
@ -29,27 +24,13 @@ export function MessageActionBar({
}`}
>
<div className="flex items-center gap-1.5">
{hasRewind && (
<button
type="button"
onClick={onRewind}
aria-label={rewindLabel}
title={rewindLabel}
className="inline-flex min-h-7 items-center gap-1 rounded-full border border-[var(--color-border)]/70 bg-[var(--color-surface-container-low)] px-2.5 text-[11px] font-medium text-[var(--color-text-tertiary)] transition-colors hover:border-[var(--color-brand)]/35 hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35"
>
<span className="material-symbols-outlined text-[14px]">undo</span>
<span className="hidden min-[920px]:inline">Rewind</span>
</button>
)}
{hasCopy && (
<CopyButton
text={copyText!}
label={copyLabel}
displayLabel="Copy"
displayCopiedLabel="Copied"
className="inline-flex min-h-7 items-center rounded-full border border-[var(--color-border)]/70 bg-[var(--color-surface-container-low)] px-2.5 text-[11px] font-medium text-[var(--color-text-tertiary)] transition-colors hover:border-[var(--color-brand)]/35 hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35"
/>
)}
<CopyButton
text={copyText!}
label={copyLabel}
displayLabel="Copy"
displayCopiedLabel="Copied"
className="inline-flex min-h-7 items-center rounded-full border border-[var(--color-border)]/70 bg-[var(--color-surface-container-low)] px-2.5 text-[11px] font-medium text-[var(--color-text-tertiary)] transition-colors hover:border-[var(--color-brand)]/35 hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35"
/>
</div>
</div>
)

View File

@ -563,7 +563,7 @@ describe('MessageList nested tool calls', () => {
expect(assistantShell?.className).not.toContain('ml-10')
})
it('opens a rewind preview modal for user messages', async () => {
it('does not expose the old message-level rewind action', async () => {
vi.spyOn(sessionsApi, 'rewind').mockResolvedValue({
target: {
targetUserMessageId: 'user-1',
@ -575,11 +575,19 @@ describe('MessageList nested tool calls', () => {
},
code: {
available: true,
filesChanged: ['src/example.ts'],
insertions: 6,
deletions: 2,
filesChanged: ['src/App.tsx'],
insertions: 4,
deletions: 1,
},
})
vi.spyOn(sessionsApi, 'getWorkspaceStatus').mockResolvedValue({
state: 'ok',
workDir: '/tmp/example-project',
repoName: 'example-project',
branch: null,
isGitRepo: false,
changedFiles: [],
})
useChatStore.setState({
sessions: {
@ -588,74 +596,15 @@ describe('MessageList nested tool calls', () => {
{
id: 'user-1',
type: 'user_text',
content: '回到这一步重做',
timestamp: 1,
},
],
}),
},
})
render(<MessageList />)
fireEvent.click(screen.getByRole('button', { name: 'Rewind to here' }))
const dialog = await screen.findByRole('dialog')
expect(within(dialog).getByText('Rewind Conversation')).toBeTruthy()
expect(within(dialog).getByText('回到这一步重做')).toBeTruthy()
expect(within(dialog).getByText('src/example.ts')).toBeTruthy()
expect(sessionsApi.rewind).toHaveBeenCalledWith(ACTIVE_TAB, {
targetUserMessageId: 'user-1',
userMessageIndex: 0,
expectedContent: '回到这一步重做',
dryRun: true,
})
})
it('confirms rewind with the selected message id and prompt guard', async () => {
vi.spyOn(sessionsApi, 'rewind').mockResolvedValue({
target: {
targetUserMessageId: 'user-2',
userMessageIndex: 1,
userMessageCount: 2,
},
conversation: {
messagesRemoved: 2,
},
code: {
available: false,
filesChanged: [],
insertions: 0,
deletions: 0,
},
})
const reloadHistory = vi.fn().mockResolvedValue(undefined)
const queueComposerPrefill = vi.fn()
useChatStore.setState({
reloadHistory,
queueComposerPrefill,
sessions: {
[ACTIVE_TAB]: makeSessionState({
messages: [
{
id: 'user-1',
type: 'user_text',
content: '第一段',
content: '做一个页面',
timestamp: 1,
},
{
id: 'assistant-1',
type: 'assistant_text',
content: 'ok',
content: 'done',
timestamp: 2,
},
{
id: 'user-2',
type: 'user_text',
content: '第二段',
timestamp: 3,
},
],
}),
},
@ -663,23 +612,8 @@ describe('MessageList nested tool calls', () => {
render(<MessageList />)
const buttons = screen.getAllByRole('button', { name: 'Rewind to here' })
fireEvent.click(buttons[1]!)
const dialog = await screen.findByRole('dialog')
fireEvent.click(within(dialog).getByRole('button', { name: /Rewind here/ }))
await waitFor(() => {
expect(sessionsApi.rewind).toHaveBeenLastCalledWith(ACTIVE_TAB, {
targetUserMessageId: 'user-2',
userMessageIndex: 1,
expectedContent: '第二段',
})
})
expect(reloadHistory).toHaveBeenCalledWith(ACTIVE_TAB)
expect(queueComposerPrefill).toHaveBeenCalledWith(ACTIVE_TAB, {
text: '第二段',
attachments: undefined,
})
expect(await screen.findByRole('button', { name: 'Undo current turn changes' })).toBeTruthy()
expect(screen.queryByRole('button', { name: 'Rewind to here' })).toBeNull()
})
it('shows a current-turn change card from checkpoint preview', async () => {

View File

@ -19,8 +19,6 @@ import { StreamingIndicator } from './StreamingIndicator'
import { InlineTaskSummary } from './InlineTaskSummary'
import { CurrentTurnChangeCard } from './CurrentTurnChangeCard'
import type { AgentTaskNotification, UIMessage } from '../../types/chat'
import { Modal } from '../shared/Modal'
import { Button } from '../shared/Button'
import { ConfirmDialog } from '../shared/ConfirmDialog'
type ToolCall = Extract<UIMessage, { type: 'tool_use' }>
@ -191,16 +189,6 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
const shouldAutoScrollRef = useRef(true)
const lastSessionIdRef = useRef<string | null | undefined>(resolvedSessionId)
const t = useTranslation()
const [rewindTarget, setRewindTarget] = useState<{
messageId: string
userMessageIndex: number
content: string
attachments?: Extract<UIMessage, { type: 'user_text' }>['attachments']
} | null>(null)
const [rewindPreview, setRewindPreview] = useState<SessionRewindResponse | null>(null)
const [rewindError, setRewindError] = useState<string | null>(null)
const [isLoadingPreview, setIsLoadingPreview] = useState(false)
const [isExecutingRewind, setIsExecutingRewind] = useState(false)
const [currentTurnPreview, setCurrentTurnPreview] = useState<CurrentTurnPreview | null>(null)
const [currentTurnError, setCurrentTurnError] = useState<string | null>(null)
const [isLoadingCurrentTurnPreview, setIsLoadingCurrentTurnPreview] = useState(false)
@ -224,49 +212,6 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
bottomRef.current?.scrollIntoView?.({ behavior: 'smooth' })
}, [messages.length, resolvedSessionId, streamingText])
useEffect(() => {
if (!resolvedSessionId || !rewindTarget) return
let cancelled = false
setIsLoadingPreview(true)
setRewindPreview(null)
setRewindError(null)
void sessionsApi
.rewind(resolvedSessionId, {
targetUserMessageId: rewindTarget.messageId,
userMessageIndex: rewindTarget.userMessageIndex,
expectedContent: rewindTarget.content,
dryRun: true,
})
.then((preview) => {
if (!cancelled) {
setRewindPreview(preview)
}
})
.catch((error) => {
if (cancelled) return
const message =
error instanceof ApiError
? typeof error.body === 'object' && error.body && 'message' in error.body
? String((error.body as { message: unknown }).message)
: error.message
: error instanceof Error
? error.message
: String(error)
setRewindError(message)
})
.finally(() => {
if (!cancelled) {
setIsLoadingPreview(false)
}
})
return () => {
cancelled = true
}
}, [resolvedSessionId, rewindTarget])
const { toolResultMap, childToolCallsByParent, renderItems } = useMemo(
() => buildRenderModel(messages),
[messages],
@ -335,75 +280,6 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
}
}, [chatState, isMemberSession, latestTurnTarget, resolvedSessionId])
const closeRewindModal = useCallback(() => {
if (isExecutingRewind) return
setRewindTarget(null)
setRewindPreview(null)
setRewindError(null)
setIsLoadingPreview(false)
}, [isExecutingRewind])
const handleConfirmRewind = useCallback(async () => {
if (!resolvedSessionId || !rewindTarget || isExecutingRewind) return
setIsExecutingRewind(true)
setRewindError(null)
try {
if (chatState !== 'idle') {
stopGeneration(resolvedSessionId)
}
const result = await sessionsApi.rewind(resolvedSessionId, {
targetUserMessageId: rewindTarget.messageId,
userMessageIndex: rewindTarget.userMessageIndex,
expectedContent: rewindTarget.content,
})
await reloadHistory(resolvedSessionId)
queueComposerPrefill(resolvedSessionId, {
text: rewindTarget.content,
attachments: rewindTarget.attachments,
})
addToast({
type: 'success',
message: result.code.available
? t('chat.rewindSuccessWithCode', {
count: result.conversation.messagesRemoved,
})
: t('chat.rewindSuccessConversationOnly', {
count: result.conversation.messagesRemoved,
}),
})
setRewindTarget(null)
setRewindPreview(null)
} catch (error) {
const message =
error instanceof ApiError
? typeof error.body === 'object' && error.body && 'message' in error.body
? String((error.body as { message: unknown }).message)
: error.message
: error instanceof Error
? error.message
: String(error)
setRewindError(message)
} finally {
setIsExecutingRewind(false)
}
}, [
addToast,
chatState,
isExecutingRewind,
queueComposerPrefill,
reloadHistory,
resolvedSessionId,
rewindTarget,
stopGeneration,
t,
])
const handleUndoCurrentTurn = useCallback(async () => {
if (!resolvedSessionId || !currentTurnPreview || isUndoingCurrentTurn) return
@ -467,8 +343,6 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
t,
])
let visibleUserMessageIndex = -1
return (
<div
ref={scrollContainerRef}
@ -494,10 +368,6 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
}
const msg = item.message
const rewindableUserIndex =
msg.type === 'user_text' && !msg.pending
? ++visibleUserMessageIndex
: null
return (
<MessageBlock
key={msg.id}
@ -512,19 +382,6 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
})()
: null
}
rewindableUserIndex={rewindableUserIndex}
onRequestRewind={
!isMemberSession
? (message, userMessageIndex) => {
setRewindTarget({
messageId: message.id,
userMessageIndex,
content: message.content,
attachments: message.attachments,
})
}
: undefined
}
/>
)
})}
@ -563,119 +420,6 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
<div ref={bottomRef} />
</div>
<Modal
open={Boolean(rewindTarget)}
onClose={closeRewindModal}
title={t('chat.rewindModalTitle')}
footer={
<>
<Button
variant="ghost"
onClick={closeRewindModal}
disabled={isExecutingRewind}
>
{t('common.cancel')}
</Button>
<Button
onClick={() => {
void handleConfirmRewind()
}}
loading={isExecutingRewind}
disabled={isLoadingPreview || Boolean(rewindError)}
icon={
!isExecutingRewind ? (
<span className="material-symbols-outlined text-[16px]">undo</span>
) : undefined
}
>
{t('chat.rewindConfirm')}
</Button>
</>
}
>
<div className="space-y-4">
<div className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-4 py-3">
<div className="mb-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-[var(--color-text-tertiary)]">
{t('chat.rewindPromptLabel')}
</div>
<div className="whitespace-pre-wrap break-words text-sm leading-relaxed text-[var(--color-text-primary)]">
{rewindTarget?.content || t('chat.rewindAttachmentOnly')}
</div>
</div>
{isLoadingPreview && (
<div className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-4 py-3 text-sm text-[var(--color-text-secondary)]">
{t('chat.rewindLoading')}
</div>
)}
{!isLoadingPreview && rewindPreview && (
<div className="grid gap-3 md:grid-cols-[minmax(0,1fr)_220px]">
<div className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-4 py-3">
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-[var(--color-text-primary)]">
<span className="material-symbols-outlined text-[16px] text-[var(--color-brand)]">history</span>
{t('chat.rewindConversationCardTitle')}
</div>
<p className="text-sm leading-relaxed text-[var(--color-text-secondary)]">
{t('chat.rewindConversationCardBody', {
count: rewindPreview.conversation.messagesRemoved,
})}
</p>
</div>
<div className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-4 py-3">
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-[var(--color-text-primary)]">
<span className="material-symbols-outlined text-[16px] text-[var(--color-brand)]">code</span>
{t('chat.rewindCodeCardTitle')}
</div>
{rewindPreview.code.available ? (
<div className="space-y-1 text-sm text-[var(--color-text-secondary)]">
<div>{t('chat.rewindCodeFiles', { count: rewindPreview.code.filesChanged.length })}</div>
<div>{t('chat.rewindCodeInsertions', { count: rewindPreview.code.insertions })}</div>
<div>{t('chat.rewindCodeDeletions', { count: rewindPreview.code.deletions })}</div>
</div>
) : (
<p className="text-sm leading-relaxed text-[var(--color-text-secondary)]">
{rewindPreview.code.reason || t('chat.rewindCodeUnavailable')}
</p>
)}
</div>
</div>
)}
{!isLoadingPreview && rewindPreview?.code.available && rewindPreview.code.filesChanged.length > 0 && (
<div className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-4 py-3">
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-[var(--color-text-tertiary)]">
{t('chat.rewindFilesLabel')}
</div>
<div className="flex flex-wrap gap-2">
{rewindPreview.code.filesChanged.slice(0, 8).map((filePath) => (
<span
key={filePath}
className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] px-2.5 py-1 text-[11px] text-[var(--color-text-secondary)]"
>
{filePath}
</span>
))}
{rewindPreview.code.filesChanged.length > 8 && (
<span className="rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] px-2.5 py-1 text-[11px] text-[var(--color-text-secondary)]">
{t('chat.rewindFilesMore', {
count: rewindPreview.code.filesChanged.length - 8,
})}
</span>
)}
</div>
</div>
)}
{rewindError && (
<div className="rounded-[var(--radius-lg)] border border-[var(--color-error)]/30 bg-[var(--color-error-container)]/22 px-4 py-3 text-sm text-[var(--color-error)]">
{rewindError}
</div>
)}
</div>
</Modal>
<ConfirmDialog
open={currentTurnUndoConfirmOpen}
onClose={() => {
@ -700,18 +444,11 @@ export const MessageBlock = memo(function MessageBlock({
activeThinkingId,
agentTaskNotifications,
toolResult,
rewindableUserIndex,
onRequestRewind,
}: {
message: UIMessage
activeThinkingId: string | null
agentTaskNotifications: Record<string, AgentTaskNotification>
toolResult?: { content: unknown; isError: boolean } | null
rewindableUserIndex?: number | null
onRequestRewind?: (
message: Extract<UIMessage, { type: 'user_text' }>,
userMessageIndex: number,
) => void
}) {
const t = useTranslation()
@ -721,12 +458,6 @@ export const MessageBlock = memo(function MessageBlock({
<UserMessage
content={message.content}
attachments={message.attachments}
onRewind={
typeof rewindableUserIndex === 'number' && onRequestRewind
? () => onRequestRewind(message, rewindableUserIndex)
: undefined
}
rewindLabel={t('chat.rewindAction')}
/>
)
case 'assistant_text':

View File

@ -5,11 +5,9 @@ import { MessageActionBar } from './MessageActionBar'
type Props = {
content: string
attachments?: UIAttachment[]
onRewind?: () => void
rewindLabel?: string
}
export function UserMessage({ content, attachments, onRewind, rewindLabel }: Props) {
export function UserMessage({ content, attachments }: Props) {
const hasText = content.trim().length > 0
return (
@ -35,8 +33,6 @@ export function UserMessage({ content, attachments, onRewind, rewindLabel }: Pro
<MessageActionBar
copyText={content}
copyLabel="Copy prompt"
onRewind={onRewind}
rewindLabel={rewindLabel}
align="end"
/>
)}

View File

@ -4,10 +4,12 @@ import { ThinkingBlock } from './ThinkingBlock'
import { ToolCallBlock } from './ToolCallBlock'
import { PermissionDialog } from './PermissionDialog'
import { useChatStore } from '../../stores/chatStore'
import { useSettingsStore } from '../../stores/settingsStore'
import { useTabStore } from '../../stores/tabStore'
describe('chat blocks', () => {
beforeEach(() => {
useSettingsStore.setState({ locale: 'en' })
useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', type: 'session' as const, status: 'idle' }] })
useChatStore.setState({ sessions: {} })
})

View File

@ -3,10 +3,12 @@ import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import { UpdateChecker } from './UpdateChecker'
import { useSettingsStore } from '../../stores/settingsStore'
import { useUpdateStore } from '../../stores/updateStore'
describe('UpdateChecker', () => {
beforeEach(() => {
useSettingsStore.setState({ locale: 'en' })
Object.defineProperty(window, '__TAURI__', {
value: {},
configurable: true,

View File

@ -617,21 +617,6 @@ export const en = {
'chat.select': 'select',
'chat.dismiss': 'dismiss',
'chat.stopTitle': 'Stop generation (Cmd+.)',
'chat.rewindAction': 'Rewind to here',
'chat.rewindModalTitle': 'Rewind Conversation',
'chat.rewindConfirm': 'Rewind here',
'chat.rewindPromptLabel': 'Selected prompt',
'chat.rewindAttachmentOnly': 'This message only contains attachments.',
'chat.rewindLoading': 'Inspecting file checkpoints for this turn...',
'chat.rewindConversationCardTitle': 'Conversation rollback',
'chat.rewindConversationCardBody': 'Remove this prompt and the {count} active messages that follow it, then reopen the composer at that point.',
'chat.rewindCodeCardTitle': 'Code rollback',
'chat.rewindCodeFiles': '{count} files will be restored',
'chat.rewindCodeInsertions': '{count} insertions will be removed',
'chat.rewindCodeDeletions': '{count} deletions will be restored',
'chat.rewindCodeUnavailable': 'No file checkpoint is available for this prompt. The conversation can still be rewound.',
'chat.rewindFilesLabel': 'Affected files',
'chat.rewindFilesMore': '+{count} more',
'chat.rewindSuccessWithCode': 'Rewound {count} messages and restored tracked files.',
'chat.rewindSuccessConversationOnly': 'Rewound {count} messages. No file checkpoint was available for this turn.',
'chat.turnChangesCardLabel': 'Current turn changed files',

View File

@ -619,21 +619,6 @@ export const zh: Record<TranslationKey, string> = {
'chat.select': '选择',
'chat.dismiss': '关闭',
'chat.stopTitle': '停止生成 (Cmd+.)',
'chat.rewindAction': '回滚到这里',
'chat.rewindModalTitle': '回滚对话',
'chat.rewindConfirm': '执行回滚',
'chat.rewindPromptLabel': '目标提示词',
'chat.rewindAttachmentOnly': '这条消息只包含附件。',
'chat.rewindLoading': '正在检查这一轮的文件检查点...',
'chat.rewindConversationCardTitle': '对话回滚',
'chat.rewindConversationCardBody': '移除这条提示词以及其后的 {count} 条活跃消息,并把输入框恢复到这个位置。',
'chat.rewindCodeCardTitle': '代码回滚',
'chat.rewindCodeFiles': '将恢复 {count} 个文件',
'chat.rewindCodeInsertions': '将移除 {count} 行新增',
'chat.rewindCodeDeletions': '将恢复 {count} 行删除',
'chat.rewindCodeUnavailable': '这条提示词没有可用的文件检查点,仍然可以只回滚对话。',
'chat.rewindFilesLabel': '受影响文件',
'chat.rewindFilesMore': '另有 {count} 个',
'chat.rewindSuccessWithCode': '已回滚 {count} 条消息,并恢复相关文件。',
'chat.rewindSuccessConversationOnly': '已回滚 {count} 条消息。这一轮没有可用的文件检查点。',
'chat.turnChangesCardLabel': '当前轮次已更改文件',