feat(desktop): collapse long change-file lists in the turn card (show N more)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-05-30 18:15:00 +08:00
parent 0c24a5da3e
commit f2f230914d
4 changed files with 80 additions and 2 deletions

View File

@ -251,3 +251,49 @@ describe('CurrentTurnChangeCard open-with buttons', () => {
})
})
})
describe('CurrentTurnChangeCard collapse long file lists', () => {
beforeEach(() => {
vi.clearAllMocks()
ensureTargetsMock.mockResolvedValue(undefined)
openPreviewSpy.mockResolvedValue(undefined)
})
function makeFiles(count: number): string[] {
return Array.from({ length: count }, (_, i) => `/w/proj/src/file${i + 1}.ts`)
}
it('does NOT render a show-more toggle with ≤5 files', () => {
renderCard(makeFiles(5))
expect(screen.getAllByRole('button', { name: /turnChangesShowDiffAria/ })).toHaveLength(5)
expect(screen.queryByText('chat.turnChangesShowMore')).not.toBeInTheDocument()
expect(screen.queryByText('chat.turnChangesShowLess')).not.toBeInTheDocument()
})
it('with 8 files shows only 5 rows + a "show more" toggle (remaining = 3)', () => {
renderCard(makeFiles(8))
// only the first 5 diff-toggle rows are rendered
expect(screen.getAllByRole('button', { name: /turnChangesShowDiffAria/ })).toHaveLength(5)
// the show-more toggle is present (identity-mock key). The real key carries the
// remaining count via '{count}'; with the placeholder-bearing real string this
// renders as "再显示 3 个文件" (8 - COLLAPSED_COUNT(5) = 3).
expect(screen.getByText('chat.turnChangesShowMore')).toBeInTheDocument()
// …and it is the only toggle (no "show less" while collapsed)
expect(screen.queryByText('chat.turnChangesShowLess')).not.toBeInTheDocument()
})
it('clicking "show more" reveals all 8 rows and shows "show less"; clicking again re-collapses', () => {
renderCard(makeFiles(8))
const showMore = screen.getByText('chat.turnChangesShowMore')
fireEvent.click(showMore)
expect(screen.getAllByRole('button', { name: /turnChangesShowDiffAria/ })).toHaveLength(8)
const showLess = screen.getByText('chat.turnChangesShowLess')
expect(showLess).toBeInTheDocument()
expect(screen.queryByText('chat.turnChangesShowMore')).not.toBeInTheDocument()
fireEvent.click(showLess)
expect(screen.getAllByRole('button', { name: /turnChangesShowDiffAria/ })).toHaveLength(5)
expect(screen.getByText('chat.turnChangesShowMore')).toBeInTheDocument()
})
})

View File

@ -1,6 +1,6 @@
import { useCallback, useMemo, useState } from 'react'
import type { MouseEvent as ReactMouseEvent } from 'react'
import { ChevronDown } from 'lucide-react'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { sessionsApi, type SessionTurnCheckpoint } from '../../api/sessions'
import { useTranslation, type TranslationKey } from '../../i18n'
import { WorkspaceDiffSurface } from '../workspace/WorkspaceCodeSurface'
@ -34,6 +34,8 @@ type ChangedFileEntry = {
displayPath: string
}
const COLLAPSED_COUNT = 5
export function CurrentTurnChangeCard({
sessionId,
targetUserMessageId,
@ -48,6 +50,7 @@ export function CurrentTurnChangeCard({
const [expandedPath, setExpandedPath] = useState<string | null>(null)
const [diffByPath, setDiffByPath] = useState<Record<string, DiffPreviewState>>({})
const [openWith, setOpenWith] = useState<{ items: OpenWithItem[]; anchor: DOMRect } | null>(null)
const [showAllFiles, setShowAllFiles] = useState(false)
const files = useMemo<ChangedFileEntry[]>(
() => checkpoint.code.filesChanged.map((filePath) => ({
@ -57,6 +60,11 @@ export function CurrentTurnChangeCard({
[checkpoint.code.filesChanged, workDir],
)
const canCollapse = files.length > COLLAPSED_COUNT
const visibleFiles = canCollapse && !showAllFiles
? files.slice(0, COLLAPSED_COUNT)
: files
const toggleDiff = useCallback((fileEntry: ChangedFileEntry) => {
const nextExpandedPath = expandedPath === fileEntry.apiPath ? null : fileEntry.apiPath
setExpandedPath(nextExpandedPath)
@ -171,7 +179,7 @@ export function CurrentTurnChangeCard({
</div>
<div className="divide-y divide-[var(--color-border)]">
{files.map((fileEntry) => {
{visibleFiles.map((fileEntry) => {
const isExpanded = expandedPath === fileEntry.apiPath
const diffState = diffByPath[fileEntry.apiPath]
const fileName = fileEntry.displayPath.split('/').pop() || fileEntry.displayPath
@ -238,6 +246,26 @@ export function CurrentTurnChangeCard({
})}
</div>
{canCollapse && (
<button
type="button"
onClick={() => setShowAllFiles((current) => !current)}
className="flex w-full items-center justify-center gap-1 border-t border-[var(--color-border)] px-4 py-2 text-xs text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-brand)]/35"
>
{showAllFiles ? (
<>
{t('chat.turnChangesShowLess')}
<ChevronUp size={14} strokeWidth={1.9} />
</>
) : (
<>
{t('chat.turnChangesShowMore', { count: String(files.length - COLLAPSED_COUNT) })}
<ChevronDown size={14} strokeWidth={1.9} />
</>
)}
</button>
)}
{error && (
<div className="border-t border-[var(--color-error)]/20 bg-[var(--color-error-container)]/18 px-4 py-3 text-xs text-[var(--color-error)]">
{error}

View File

@ -1213,6 +1213,8 @@ export const en = {
'chat.turnChangesHideDiffAria': 'Hide diff for {path}',
'chat.turnChangesDiffLoading': 'Loading diff...',
'chat.turnChangesDiffUnavailable': 'Diff unavailable.',
'chat.turnChangesShowMore': 'Show {count} more files',
'chat.turnChangesShowLess': 'Show less',
// ─── Streaming Indicator ──────────────────────────────────────
'streaming.thinking': 'Thinking',

View File

@ -1215,6 +1215,8 @@ export const zh: Record<TranslationKey, string> = {
'chat.turnChangesHideDiffAria': '收起 {path} 的 diff',
'chat.turnChangesDiffLoading': '正在加载 diff...',
'chat.turnChangesDiffUnavailable': 'Diff 不可用。',
'chat.turnChangesShowMore': '再显示 {count} 个文件',
'chat.turnChangesShowLess': '收起',
// ─── Streaming Indicator ──────────────────────────────────────
'streaming.thinking': '思考中',