fix(web): show file metadata in preview header (#1450)

This commit is contained in:
Ananovo
2026-08-10 10:47:58 +08:00
committed by GitHub
parent ad7f2a4084
commit 044d72fdc0
5 changed files with 69 additions and 3 deletions
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { mkdir, rm, stat, writeFile } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
import { RpcHandlerManager } from '../../../api/rpc/RpcHandlerManager'
import { registerFileHandlers } from './files'
async function createTempDir(prefix: string): Promise<string> {
const path = join(tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`)
await mkdir(path, { recursive: true })
return path
}
describe('file RPC handlers', () => {
let rootDir: string
let rpc: RpcHandlerManager
beforeEach(async () => {
rootDir = await createTempDir('hapi-file-handler')
rpc = new RpcHandlerManager({ scopePrefix: 'session-test' })
registerFileHandlers(rpc, rootDir)
})
afterEach(async () => {
await rm(rootDir, { recursive: true, force: true })
})
it('returns file metadata alongside content', async () => {
const filePath = join(rootDir, 'README.md')
await writeFile(filePath, '# test')
const expectedStats = await stat(filePath)
const response = await rpc.handleRequest({
method: 'session-test:readFile',
params: JSON.stringify({ path: 'README.md' })
})
const parsed = JSON.parse(response) as {
success: boolean
content?: string
size?: number
modified?: number
}
expect(parsed.success).toBe(true)
expect(parsed.content).toBe(Buffer.from('# test').toString('base64'))
expect(parsed.size).toBe(expectedStats.size)
expect(parsed.modified).toBe(expectedStats.mtime.getTime())
})
})
+7 -1
View File
@@ -44,9 +44,15 @@ export function registerFileHandlers(rpcHandlerManager: RpcHandlerManager, worki
try {
const resolvedPath = resolve(workingDirectory, data.path)
const stats = await stat(resolvedPath)
const buffer = await readFile(resolvedPath)
const content = buffer.toString('base64')
return { success: true, content }
return {
success: true,
content,
size: stats.size,
modified: stats.mtime.getTime()
}
} catch (error) {
logger.debug('Failed to read file:', error)
return rpcError(getErrorMessage(error, 'Failed to read file'))
+2
View File
@@ -627,6 +627,8 @@ export type GitCommandResponse = CommandResponse
export type FileReadResponse = {
success: boolean
content?: string
size?: number
modified?: number
error?: string
}
+7
View File
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { I18nProvider } from '@/lib/i18n-context'
import { formatFileMetadata } from '@/lib/file-metadata'
import { encodeBase64 } from '@/lib/utils'
import FilePage from './file'
@@ -12,6 +13,8 @@ const sampleMarkdown = '# Heading\n\n| Col A | Col B |\n| --- | --- |\n| one | t
const filePath = 'docs/README.md'
const encodedPath = encodeBase64(filePath)
const encodedContent = encodeBase64(sampleMarkdown)
const fileSize = 1024
const fileModified = 1_784_175_060_000
vi.mock('@tanstack/react-router', () => ({
useParams: () => ({ sessionId: 'session-1' }),
@@ -28,6 +31,8 @@ vi.mock('@/lib/app-context', () => ({
readSessionFile: vi.fn(async () => ({
success: true,
content: encodedContent,
size: fileSize,
modified: fileModified,
})),
},
}),
@@ -82,6 +87,8 @@ describe('FilePage markdown preview', () => {
await waitFor(() => {
expect(screen.getByTestId('markdown-preview')).toHaveTextContent('# Heading')
})
expect(screen.getByText(formatFileMetadata(fileSize, fileModified, 'en')!)).toBeInTheDocument()
expect(screen.getAllByText(filePath)).toHaveLength(1)
const previewCopyButton = screen.getByRole('button', { name: 'Copy file content' })
expect(previewCopyButton.closest('[data-hapi-file-content-header="true"]')).not.toBeNull()
expect(previewCopyButton).not.toHaveClass('absolute')
+4 -2
View File
@@ -14,6 +14,7 @@ import { useTranslation } from '@/lib/use-translation'
import { decodeBase64 } from '@/lib/utils'
import { ImagePreview } from '@/components/ImagePreview'
import { MarkdownRenderer } from '@/components/MarkdownRenderer'
import { formatFileMetadata } from '@/lib/file-metadata'
import {
getInitialMarkdownPreviewMode,
isMarkdownFile,
@@ -199,7 +200,7 @@ function extractCommandError(result: GitCommandResponse | undefined): string | n
export default function FilePage() {
const { api } = useAppContext()
const { t } = useTranslation()
const { t, locale } = useTranslation()
const { copied: pathCopied, copy: copyPath } = useCopyToClipboard()
const { copied: contentCopied, copy: copyContent } = useCopyToClipboard()
const goBack = useAppGoBack()
@@ -298,6 +299,7 @@ export default function FilePage() {
const missingPath = !filePath
const diffErrorMessage = diffError ? formatDiffError(diffError, t) : null
const fileErrorMessage = fileError ? formatReadFileError(fileError, t) : null
const fileMetadata = formatFileMetadata(fileContentResult?.size, fileContentResult?.modified, locale)
return (
<div className="flex h-full min-h-0 flex-col">
@@ -312,7 +314,7 @@ export default function FilePage() {
</button>
<div className="min-w-0 flex-1">
<div className="truncate font-semibold">{fileName}</div>
<div className="truncate text-xs text-[var(--app-hint)]">{filePath || t('file.page.unknownPath')}</div>
<div className="truncate text-xs text-[var(--app-hint)]">{fileMetadata ?? (filePath || t('file.page.unknownPath'))}</div>
</div>
</div>
</div>