diff --git a/src/index.ts b/src/index.ts
index f5362ab..72753de 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -866,7 +866,7 @@ export function apply(ctx: Context, config?: SidebarConfig): void {
const type = mediaTypeForPath(absolute)
const body = await readFile(absolute)
res.writeHead(200, {
- 'content-type': type,
+ 'content-type': type === 'text/html' ? 'text/html; charset=utf-8' : type,
'cache-control': 'no-cache',
'x-content-type-options': 'nosniff',
'referrer-policy': 'no-referrer',
diff --git a/tests/smoke.spec.ts b/tests/smoke.spec.ts
index 5cabab8..c0806f8 100644
--- a/tests/smoke.spec.ts
+++ b/tests/smoke.spec.ts
@@ -10,6 +10,7 @@ import { tmpdir } from 'node:os'
import { join, resolve as resolvePath } from 'node:path'
import { SettingsConflictError, settingsNamespace } from '@deepseek-ai/dsh-settings'
import { apply, mediaTypeForPath } from '../src/index.ts'
+import { encodeHtmlUrl } from '../src/html-route.ts'
import * as git from '../src/git.ts'
import { listDirectory } from '../src/fs-tree.ts'
import { encodeHtmlUrl } from '../src/html-route.ts'
@@ -115,6 +116,58 @@ describe('host plugin smoke', () => {
for (const cleanup of effects) cleanup()
})
+ it('serves HTML previews as UTF-8 without changing the file bytes', async () => {
+ const directory = mkdtempSync(join(tmpdir(), 'dsh-sidebar-html-utf8-'))
+ const path = join(directory, 'fragment.html')
+ const source = Buffer.from('
排序算法可视化
', 'utf8')
+ writeFileSync(path, source)
+ const routes: SidebarWebRoute[] = []
+ const effects: Array<() => void | (() => void)> = []
+ const ctx: FakeContext = {
+ webRuntime: { trustedHosts: [] },
+ webServer: {
+ register: (route) => { routes.push(route); return () => {} },
+ registerUpgrade: () => () => {},
+ },
+ sessions: { get: () => ({ header: { cwd: directory } }) },
+ tools: { register: () => () => {} },
+ effect: (fn) => {
+ const cleanup = fn()
+ if (typeof cleanup === 'function') effects.push(cleanup)
+ },
+ inject: () => () => {},
+ get: () => undefined,
+ }
+ try {
+ apply(ctx as never)
+ const route = routes.find(candidate => candidate.path === '/sidebar/html')!
+ const req = {
+ method: 'GET',
+ url: encodeHtmlUrl('s-html', path),
+ headers: { host: '127.0.0.1:3080' },
+ } as never
+ const response: { status?: number; headers?: Record; chunks: Buffer[] } = { chunks: [] }
+ const res = {
+ writeHead: (status: number, headers?: Record) => {
+ response.status = status
+ response.headers = headers
+ },
+ end: (chunk?: string | Buffer) => {
+ if (chunk !== undefined) response.chunks.push(Buffer.from(chunk))
+ },
+ } as never
+
+ await route.handler(req, res)
+
+ expect(response.status).toBe(200)
+ expect(Buffer.concat(response.chunks)).toEqual(source)
+ expect(response.headers?.['content-type']).toBe('text/html; charset=utf-8')
+ } finally {
+ for (const cleanup of effects) cleanup()
+ rmSync(directory, { recursive: true, force: true })
+ }
+ })
+
it('runs git status/log/branches against this repository', async () => {
const cwd = process.cwd()
const status = await git.status(cwd)