Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/app-bundle/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -950,11 +950,11 @@
"packages/core/schema.json": "79ef4e2f0dd8103ac7a4b6ff3ddb0fd10764655f1dc678c1c401dfc43be9a1aa",
"packages/session-ui/bunfig.toml": "82585987c8258caf58d07c6aaf4e49b7338e4da16f97c785af398aace9e6e722",
"packages/session-ui/happydom.ts": "d81d6b14e988f058fa502c841df34b55307c57fc94a26fb4f8a5deb6aa0d4834",
"packages/session-ui/package.json": "1c69e65b039e6262ce50defb798233e5db99aeca4b0fe66885f7e35c2433b685",
"packages/session-ui/package.json": "f1edc72de0e5aa3a1b045be1c825ca359dafc8bd9cbb89208a21c53226a37d40",
"packages/ui/package.json": "b1d168d0371e9094faae1107fc6c00be197f09bc69daa2247a3890d607f4b629",
"packages/app/public/amico.svg": "a14b9d543d895bcdf0758f7b9ef5908ee0acaac794446494af059b159247db8f",
"packages/app/public/oc-theme-preload.js": "27227e802b3494e7c545da903e679efdb30ccc754cd4eb5cdf08005a40d560b6",
"packages/app/src/app.tsx": "a72e7cef35d5de80980fbb1fc26c14d8551d1677821e72798c624842927b55fd",
"packages/app/src/app.tsx": "98f367769ed22ed33cbb8ff6067b3b5fc829acef1fad62b791cfd286ed9aec54",
"packages/app/src/design-polish.css": "42cc6efaefe9a71dedd12fcb0bf2549453d9a46097cb025cf087064b29d3ccce",
"packages/app/src/entry.tsx": "f35e1017f4c9d478d254b2a38043e5750064b6bef25169c3e07ae9f72ff1049c",
"packages/app/src/index.css": "08179e06ce2d419a2d98acc96025f91c7709062ea9f3ad245e88dc35e75ff9f7",
Expand Down Expand Up @@ -1408,7 +1408,7 @@
"packages/app/src/components/session/session-context-tab.tsx": "227243b178b517f067d9ae0ae0eec3c559beeb6681828158b0600a17e98e7f81",
"packages/app/src/components/session/session-header.tsx": "a46591ed1097d0fdcff61fb0c5529396955e8857cbef748747c51e472d5564c5",
"packages/app/src/components/session/session-new-view.tsx": "9510a4f550a3f0d4791e98e8025666f09d70a60fb66f193e48ee61feddae5a57",
"packages/app/src/components/session/session-preview-tab.tsx": "b6b4b8fe6f751499e0d3005e1f12224351df4f28f221bba10fc2f91c0f09e777",
"packages/app/src/components/session/session-preview-tab.tsx": "103d531614d0f904b7bb255d484ff681aaad5b66aafea308e2774c661f57bb1a",
"packages/app/src/components/session/session-sortable-terminal-tab-v2.tsx": "08db0e378c3e07d243121f40c77e153bafe897e5e2ececd48a3e00786793032b",
"packages/app/src/components/session/use-context-warning.ts": "af7a6d0159a5541aa02ad4d08fd694af1cc4853fc1c0a70763bc264a634d1c53",
"packages/app/src/components/settings-v2/data-storage-controller.ts": "fa5d143cc101f3a3b9d5ad445edddc981e0d02783021d52dbfed6c8d8bf62498",
Expand Down Expand Up @@ -1473,7 +1473,7 @@
"packages/session-ui/src/v2/components/basic-tool-v2.css": "55d3aa179704deddda7149342a951229ff5561127f786dd551e5df9aea8a7835",
"packages/session-ui/src/v2/components/basic-tool-v2.tsx": "ece46d761f7d51a7a0925b3df00a5c289d891e9a514ee721016eb723d87fb406",
"packages/session-ui/src/v2/components/diff-gutter-extension.ts": "aed07587c0c9a5f110f9ea91aa42679d6f9f66b2ec4de9043d6e2233a375acb8",
"packages/session-ui/src/v2/components/editable-diff-view-core.ts": "d6508b56ab48b956b8e5a66319323d17241a783bc9a73d869afbedc20159ad97",
"packages/session-ui/src/v2/components/editable-diff-view-core.ts": "0c05afeceeb92220b12ffde55b4887be97c6ff61931344bd8eaad0872ac32910",
"packages/session-ui/src/v2/components/editable-diff-view.tsx": "4c0ee2525b94b04b1e708a5cacab6d3eed29ca30644321986e429b2f912b730b",
"packages/session-ui/src/v2/components/file-name-picker.test.tsx": "1ce9741e315b549165d390c74b5885a43580aa70e24de987f9ba8388cd521b41",
"packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx": "4f1fdc944b0d422689176d04cb3580f628578915f97ea8d78594dfb9ac217146",
Expand Down
11 changes: 11 additions & 0 deletions packages/app-bundle/overlay/packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "@/index.css"
import * as Sentry from "@sentry/solid"
import { requestComputeConnect } from "@/components/amicode-defaults-capsule"
import { adoptWorkspaceProjects, workspaceProjects } from "@/utils/amicode-workspace-projects"
import { adoptPreviewFileTree } from "@/utils/amicode-preview-file-tree"
import { I18nProvider } from "@opencode-ai/ui/context"
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
Expand Down Expand Up @@ -446,6 +447,16 @@ function AmicodeThemeBridge() {
adoptWorkspaceProjects((d as { projects?: unknown[] }).projects as Parameters<typeof adoptWorkspaceProjects>[0])
return
}
// amicode#725: preview file tree push from the extension host.
if (d.kind === "preview-file-tree") {
const msg = d as { files?: string[]; projectRoot?: string; environment?: unknown }
adoptPreviewFileTree(
(msg.files ?? []) as string[],
(msg.projectRoot ?? "") as string,
msg.environment as Parameters<typeof adoptPreviewFileTree>[2],
)
return
}
if (d.kind !== "theme") return
if (d.colorScheme === "light" || d.colorScheme === "dark") theme.setColorScheme(d.colorScheme)
}
Expand Down
204 changes: 204 additions & 0 deletions packages/app-bundle/overlay/packages/app/src/components/pdf-viewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// pdf-viewer.tsx — PDF rendering for the Preview tab (#727).
// Uses pdfjs-dist to render PDF pages to canvas in a scrollable container.
// Zoom controls, scroll position preservation across reloads, and
// device-pixel-ratio-aware rendering.

import { createEffect, createSignal, For, on, onCleanup, Show } from "solid-js"
import { Icon } from "@opencode-ai/ui/icon"
import * as pdfjsLib from "pdfjs-dist"

// ── Worker setup ────────────────────────────────────────────────────────────
// The pdfjs worker handles the heavy PDF parsing off the main thread.
// Vite's ?url suffix gives us the asset URL without inlining.
import pdfjsWorkerUrl from "pdfjs-dist/build/pdf.worker.min.mjs?url"

pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorkerUrl

// ── Types ───────────────────────────────────────────────────────────────────

interface PageState {
pageNum: number
rendered: boolean
}

// ── Component ───────────────────────────────────────────────────────────────

export function PDFViewer(props: {
/** Binary content as base64 string, or a file URL. */
data: string | null
/** Whether data is base64-encoded binary. */
isBase64?: boolean
/** Current zoom level (100 = 100%). */
zoom: number
}) {
const [pages, setPages] = createSignal<PageState[]>([])
const [loading, setLoading] = createSignal(true)
const [error, setError] = createSignal(false)
const [pdfDoc, setPdfDoc] = createSignal<pdfjsLib.PDFDocumentProxy | null>(null)

let containerRef: HTMLDivElement | undefined
let scrollTop = 0
let activeLoadingTask: pdfjsLib.PDFDocumentLoadingTask | null = null

// Track scroll position for preservation across reloads
const saveScroll = () => {
if (containerRef) scrollTop = containerRef.scrollTop
}

// ── Load PDF ────────────────────────────────────────────────────────────

createEffect(
on(
() => props.data,
async (data) => {
if (!data) {
setLoading(false)
setError(true)
return
}

setLoading(true)
setError(false)

try {
saveScroll()

// Clean up previous loading task
if (activeLoadingTask) {
activeLoadingTask.destroy()
activeLoadingTask = null
}
setPdfDoc(null)

// Load PDF from base64 or URL
let loadingTask: pdfjsLib.PDFDocumentLoadingTask
if (props.isBase64) {
const binaryStr = atob(data)
const bytes = new Uint8Array(binaryStr.length)
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i)
}
loadingTask = pdfjsLib.getDocument({ data: bytes })
} else {
loadingTask = pdfjsLib.getDocument({ url: data })
}
activeLoadingTask = loadingTask

const doc = await loadingTask.promise
setPdfDoc(doc)

const pageStates: PageState[] = []
for (let i = 1; i <= doc.numPages; i++) {
pageStates.push({ pageNum: i, rendered: false })
}
setPages(pageStates)

// Restore scroll position after render
requestAnimationFrame(() => {
if (containerRef) containerRef.scrollTop = scrollTop
})
} catch {
setError(true)
} finally {
setLoading(false)
}
},
),
)

onCleanup(() => {
if (activeLoadingTask) {
activeLoadingTask.destroy()
activeLoadingTask = null
}
})

// ── Render ──────────────────────────────────────────────────────────────

return (
<div class="h-full flex flex-col overflow-hidden">
<Show when={loading()}>
<div class="p-4 text-12-regular text-text-weak">Loading PDF...</div>
</Show>
<Show when={error()}>
<div class="h-full flex items-center justify-center text-12-regular text-text-weak p-4">
<div class="text-center">
<Icon name="circle-x" size="large" class="mx-auto mb-2 text-text-faint" />
<p>Could not load PDF</p>
</div>
</div>
</Show>
<Show when={!loading() && !error()}>
<div ref={(el) => (containerRef = el)} class="flex-1 min-h-0 overflow-auto" onScroll={saveScroll}>
<div class="flex flex-col items-center gap-2 py-4">
<For each={pages()}>
{(page) => (
<PDFPage
doc={pdfDoc()!}
pageNum={page.pageNum}
zoom={props.zoom}
/>
)}
</For>
</div>
</div>
</Show>
</div>
)
}

// ── Single page renderer ────────────────────────────────────────────────────

function PDFPage(props: {
doc: pdfjsLib.PDFDocumentProxy
pageNum: number
zoom: number
}) {
let canvasRef: HTMLCanvasElement | undefined
const [pageError, setPageError] = createSignal(false)

createEffect(
on(
() => [props.doc, props.pageNum, props.zoom] as const,
async ([doc, num, zoom]) => {
if (!canvasRef || !doc) return
setPageError(false)

try {
const page = await doc.getPage(num)
const scale = (zoom / 100) * (window.devicePixelRatio || 1)
const viewport = page.getViewport({ scale })

canvasRef.width = viewport.width
canvasRef.height = viewport.height
canvasRef.style.width = `${viewport.width / (window.devicePixelRatio || 1)}px`
canvasRef.style.height = `${viewport.height / (window.devicePixelRatio || 1)}px`

const ctx = canvasRef.getContext("2d")
if (!ctx) return

await page.render({ canvasContext: ctx, canvas: canvasRef, viewport }).promise
} catch {
setPageError(true)
}
},
),
)

return (
<Show
when={!pageError()}
fallback={
<div class="p-4 text-12-regular text-text-weak">
Could not render page {props.pageNum}
</div>
}
>
<canvas
ref={(el) => (canvasRef = el)}
class="shadow-sm border border-border-weaker-base"
data-page={props.pageNum}
/>
</Show>
)
}
Loading
Loading