mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 11:53:10 +08:00
feat(desktop): preview documents in the workspace and images in chat
Documents the agent writes open in the workspace panel instead of another application, and local images the agent mentions show up in the conversation. Workspace preview - PDF (pdf.js with its own layout and text layer), Word (docx-preview inside a scripts-disabled sandboxed iframe) and Excel (SheetJS; .xlsx, .xlsm, .xls) open in the side panel with zoom and fit, per-file scroll/zoom/sheet memory, and a refresh when the agent rewrites the file. The engines load lazily. - Bytes come from a new GET /api/sessions/:id/workspace/raw route, with an extension allowlist, size caps, the workspace boundary and canonical-path checks. The file endpoint returns metadata and a version for documents. The client fetches with the bearer credential, so it works in Electron, LAN H5 and remote access alike. - Chat links, output cards and the change card open pdf/docx/xlsx in the workspace; documents outside the workdir still go to the system application. - Image viewer with fit, zoom and pan, and "open in system app". Chat images - Markdown images outside the workdir, at ~/, C:\ and file:// paths render, open in the viewer, and offer "open original" (pictures only). - Images returned by tools such as Read appear as thumbnails under the call. Hardening found in review - previewFsUrl escapes each path segment; a double-escaped %2e%2e used to leave /preview-fs/<session>/. - The CORS, API timing and remote-access header decorators set headers in place. Rebuilding the response buffered whole files in memory and dropped Content-Length. - The engine owns the pdf.js worker, so closing one document no longer fails the next open. - Office archives are inflated in steps to check their real sizes, not the sizes they declare. - A viewer that fails to load stays in its panel instead of taking the window down. Adds pdfjs-dist, docx-preview, xlsx (SheetJS 0.20.3 tarball) and fflate as renderer dev dependencies; Vite bundles them. Refs #1397
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { act, render, screen } from '@testing-library/react'
|
||||
import { StrictMode } from 'react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import '@testing-library/jest-dom'
|
||||
import { useElementSize } from './useElementSize'
|
||||
|
||||
const originalClientWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'clientWidth')
|
||||
const originalClientHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'clientHeight')
|
||||
|
||||
function Probe() {
|
||||
const [measuredRef, size] = useElementSize<HTMLDivElement>()
|
||||
return (
|
||||
<div ref={measuredRef}>
|
||||
<span data-testid="size">{size === null ? 'unmeasured' : `${size.width}x${size.height}`}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useElementSize', () => {
|
||||
const observers = new Set<() => void>()
|
||||
let box = { width: 0, height: 0 }
|
||||
|
||||
function stubLayout(width: number, height: number) {
|
||||
box = { width, height }
|
||||
Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, get: () => box.width })
|
||||
Object.defineProperty(HTMLElement.prototype, 'clientHeight', { configurable: true, get: () => box.height })
|
||||
}
|
||||
|
||||
function stubResizeObserver() {
|
||||
class StubResizeObserver {
|
||||
constructor(private readonly callback: () => void) {}
|
||||
observe() { observers.add(this.callback) }
|
||||
unobserve() { observers.delete(this.callback) }
|
||||
disconnect() { observers.delete(this.callback) }
|
||||
}
|
||||
vi.stubGlobal('ResizeObserver', StubResizeObserver)
|
||||
}
|
||||
|
||||
function resizeTo(width: number, height: number) {
|
||||
box = { width, height }
|
||||
act(() => {
|
||||
observers.forEach((notify) => notify())
|
||||
})
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
observers.clear()
|
||||
box = { width: 0, height: 0 }
|
||||
vi.unstubAllGlobals()
|
||||
for (const [property, original] of [
|
||||
['clientWidth', originalClientWidth],
|
||||
['clientHeight', originalClientHeight],
|
||||
] as const) {
|
||||
if (original) Object.defineProperty(HTMLElement.prototype, property, original)
|
||||
else Reflect.deleteProperty(HTMLElement.prototype, property)
|
||||
}
|
||||
})
|
||||
|
||||
it('measures the node as soon as it mounts', () => {
|
||||
stubLayout(640, 480)
|
||||
stubResizeObserver()
|
||||
|
||||
render(<Probe />)
|
||||
|
||||
expect(screen.getByTestId('size')).toHaveTextContent('640x480')
|
||||
})
|
||||
|
||||
it('reports null rather than a zero size before layout, so callers keep their fallback', () => {
|
||||
// jsdom lays nothing out: everything measures 0.
|
||||
render(<Probe />)
|
||||
|
||||
expect(screen.getByTestId('size')).toHaveTextContent('unmeasured')
|
||||
})
|
||||
|
||||
it('follows the element as it resizes', () => {
|
||||
stubLayout(640, 480)
|
||||
stubResizeObserver()
|
||||
render(<Probe />)
|
||||
|
||||
resizeTo(320, 200)
|
||||
|
||||
expect(screen.getByTestId('size')).toHaveTextContent('320x200')
|
||||
})
|
||||
|
||||
it('goes back to null when the element collapses to nothing', () => {
|
||||
stubLayout(640, 480)
|
||||
stubResizeObserver()
|
||||
render(<Probe />)
|
||||
|
||||
resizeTo(0, 0)
|
||||
|
||||
expect(screen.getByTestId('size')).toHaveTextContent('unmeasured')
|
||||
})
|
||||
|
||||
it('keeps observing under StrictMode, which replays effects without reattaching the ref', () => {
|
||||
stubLayout(640, 480)
|
||||
stubResizeObserver()
|
||||
render(<StrictMode><Probe /></StrictMode>)
|
||||
|
||||
resizeTo(100, 50)
|
||||
|
||||
expect(screen.getByTestId('size')).toHaveTextContent('100x50')
|
||||
})
|
||||
|
||||
it('stops observing on unmount', () => {
|
||||
stubLayout(640, 480)
|
||||
stubResizeObserver()
|
||||
const { unmount } = render(<Probe />)
|
||||
expect(observers.size).toBe(1)
|
||||
|
||||
unmount()
|
||||
|
||||
expect(observers.size).toBe(0)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user