Compare commits

..
Author SHA1 Message Date
Shoubhit Dash e7f4d851f6 fix(ui): avoid WebKit diff viewer crashes 2026-03-16 16:53:02 +05:30
12 changed files with 192 additions and 307 deletions
@@ -349,25 +349,3 @@ test("session model restore across workspaces", async ({ page, withProject }) =>
await waitFooter(page, firstState) await waitFooter(page, firstState)
}) })
}) })
test("variant preserved when switching agent modes", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1440, height: 900 })
await withProject(async ({ directory, gotoSession }) => {
await gotoSession()
await ensureVariant(page, directory)
const updated = await chooseDifferentVariant(page)
const available = await agents(page)
const other = available.find((name) => name !== updated.agent)
test.skip(!other, "only one agent available")
if (!other) return
await choose(page, promptAgentSelector, other)
await waitFooter(page, { agent: other, variant: updated.variant })
await choose(page, promptAgentSelector, updated.agent)
await waitFooter(page, { agent: updated.agent, variant: updated.variant })
})
})
+98 -105
View File
@@ -58,7 +58,6 @@ export function Titlebar() {
}) })
const path = () => `${location.pathname}${location.search}${location.hash}` const path = () => `${location.pathname}${location.search}${location.hash}`
const home = createMemo(() => !params.dir)
const creating = createMemo(() => { const creating = createMemo(() => {
if (!params.dir) return false if (!params.dir) return false
if (params.id) return false if (params.id) return false
@@ -176,119 +175,113 @@ export function Titlebar() {
> >
<Show when={mac()}> <Show when={mac()}>
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} /> <div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
<Show when={!home()}> <div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center"> <IconButton
<IconButton icon="menu"
icon="menu" variant="ghost"
variant="ghost" class="titlebar-icon rounded-md"
class="titlebar-icon rounded-md" onClick={layout.mobileSidebar.toggle}
onClick={layout.mobileSidebar.toggle} aria-label={language.t("sidebar.menu.toggle")}
aria-label={language.t("sidebar.menu.toggle")} aria-expanded={layout.mobileSidebar.opened()}
aria-expanded={layout.mobileSidebar.opened()} />
/> </div>
</div>
</Show>
</Show> </Show>
<Show when={!mac()}> <Show when={!mac()}>
<Show when={!home()}> <div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center"> <IconButton
<IconButton icon="menu"
icon="menu" variant="ghost"
variant="ghost" class="titlebar-icon rounded-md"
class="titlebar-icon rounded-md" onClick={layout.mobileSidebar.toggle}
onClick={layout.mobileSidebar.toggle} aria-label={language.t("sidebar.menu.toggle")}
aria-label={language.t("sidebar.menu.toggle")} aria-expanded={layout.mobileSidebar.opened()}
aria-expanded={layout.mobileSidebar.opened()} />
/> </div>
</div>
</Show>
</Show> </Show>
<div class="flex items-center gap-1 shrink-0"> <div class="flex items-center gap-1 shrink-0">
<Show when={!home()}> <TooltipKeybind
<TooltipKeybind class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"} placement="bottom"
placement="bottom" title={language.t("command.sidebar.toggle")}
title={language.t("command.sidebar.toggle")} keybind={command.keybind("sidebar.toggle")}
keybind={command.keybind("sidebar.toggle")} >
<Button
variant="ghost"
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
onClick={layout.sidebar.toggle}
aria-label={language.t("command.sidebar.toggle")}
aria-expanded={layout.sidebar.opened()}
> >
<Button <Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
variant="ghost" </Button>
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border" </TooltipKeybind>
onClick={layout.sidebar.toggle} <div class="hidden xl:flex items-center shrink-0">
aria-label={language.t("command.sidebar.toggle")} <Show when={params.dir}>
aria-expanded={layout.sidebar.opened()}
>
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
</Button>
</TooltipKeybind>
<div class="hidden xl:flex items-center shrink-0">
<Show when={params.dir}>
<div
class="flex items-center shrink-0 w-8 mr-1"
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
>
<div
class="transition-opacity"
classList={{
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
}}
>
<TooltipKeybind
placement="bottom"
title={language.t("command.session.new")}
keybind={command.keybind("session.new")}
openDelay={2000}
>
<Button
variant="ghost"
icon={creating() ? "new-session-active" : "new-session"}
class="titlebar-icon w-8 h-6 p-0 box-border"
disabled={layout.sidebar.opened()}
tabIndex={layout.sidebar.opened() ? -1 : undefined}
onClick={() => {
if (!params.dir) return
navigate(`/${params.dir}/session`)
}}
aria-label={language.t("command.session.new")}
aria-current={creating() ? "page" : undefined}
/>
</TooltipKeybind>
</div>
</div>
</Show>
<div <div
class="flex items-center gap-0 transition-transform" class="flex items-center shrink-0 w-8 mr-1"
classList={{ aria-hidden={layout.sidebar.opened() ? "true" : undefined}
"translate-x-0": !layout.sidebar.opened(),
"-translate-x-[36px]": layout.sidebar.opened(),
"duration-180 ease-out": !layout.sidebar.opened(),
"duration-180 ease-in": layout.sidebar.opened(),
}}
> >
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}> <div
<Button class="transition-opacity"
variant="ghost" classList={{
icon="chevron-left" "opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
class="titlebar-icon w-6 h-6 p-0 box-border" "opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
disabled={!canBack()} }}
onClick={back} >
aria-label={language.t("common.goBack")} <TooltipKeybind
/> placement="bottom"
</Tooltip> title={language.t("command.session.new")}
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}> keybind={command.keybind("session.new")}
<Button openDelay={2000}
variant="ghost" >
icon="chevron-right" <Button
class="titlebar-icon w-6 h-6 p-0 box-border" variant="ghost"
disabled={!canForward()} icon={creating() ? "new-session-active" : "new-session"}
onClick={forward} class="titlebar-icon w-8 h-6 p-0 box-border"
aria-label={language.t("common.goForward")} disabled={layout.sidebar.opened()}
/> tabIndex={layout.sidebar.opened() ? -1 : undefined}
</Tooltip> onClick={() => {
if (!params.dir) return
navigate(`/${params.dir}/session`)
}}
aria-label={language.t("command.session.new")}
aria-current={creating() ? "page" : undefined}
/>
</TooltipKeybind>
</div>
</div> </div>
</Show>
<div
class="flex items-center gap-0 transition-transform"
classList={{
"translate-x-0": !layout.sidebar.opened(),
"-translate-x-[36px]": layout.sidebar.opened(),
"duration-180 ease-out": !layout.sidebar.opened(),
"duration-180 ease-in": layout.sidebar.opened(),
}}
>
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
<Button
variant="ghost"
icon="chevron-left"
class="titlebar-icon w-6 h-6 p-0 box-border"
disabled={!canBack()}
onClick={back}
aria-label={language.t("common.goBack")}
/>
</Tooltip>
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
<Button
variant="ghost"
icon="chevron-right"
class="titlebar-icon w-6 h-6 p-0 box-border"
disabled={!canForward()}
onClick={forward}
aria-label={language.t("common.goForward")}
/>
</Tooltip>
</div> </div>
</Show> </div>
</div> </div>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" /> <div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
</div> </div>
+2 -3
View File
@@ -192,11 +192,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
model: item.model, model: item.model,
variant: item.variant ?? null, variant: item.variant ?? null,
}) })
const prev = scope()
const next = { const next = {
agent: item.name, agent: item.name,
model: item.model ?? prev?.model, model: item.model,
variant: item.variant ?? prev?.variant, variant: item.variant,
} satisfies State } satisfies State
const session = id() const session = id()
if (session) { if (session) {
+66 -80
View File
@@ -111,7 +111,6 @@ export default function Layout(props: ParentProps) {
let scrollContainerRef: HTMLDivElement | undefined let scrollContainerRef: HTMLDivElement | undefined
const params = useParams() const params = useParams()
const home = createMemo(() => !params.dir)
const globalSDK = useGlobalSDK() const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync() const globalSync = useGlobalSync()
const layout = useLayout() const layout = useLayout()
@@ -2242,7 +2241,7 @@ export default function Layout(props: ParentProps) {
const sidebarContent = (mobile?: boolean) => ( const sidebarContent = (mobile?: boolean) => (
<SidebarContent <SidebarContent
mobile={mobile} mobile={mobile}
opened={() => (home() ? false : layout.sidebar.opened())} opened={() => layout.sidebar.opened()}
aimMove={aim.move} aimMove={aim.move}
projects={projects} projects={projects}
renderProject={(project) => ( renderProject={(project) => (
@@ -2286,7 +2285,7 @@ export default function Layout(props: ParentProps) {
"absolute inset-y-0 left-0": true, "absolute inset-y-0 left-0": true,
"z-10": true, "z-10": true,
}} }}
style={{ width: home() ? "4rem" : `${Math.max(layout.sidebar.width(), 244)}px` }} style={{ width: `${Math.max(layout.sidebar.width(), 244)}px` }}
ref={(el) => { ref={(el) => {
setState("nav", el) setState("nav", el)
}} }}
@@ -2295,14 +2294,13 @@ export default function Layout(props: ParentProps) {
}} }}
onMouseLeave={() => { onMouseLeave={() => {
aim.reset() aim.reset()
if (home()) return
if (!sidebarHovering()) return if (!sidebarHovering()) return
arm() arm()
}} }}
> >
<div class="@container w-full h-full contain-strict">{sidebarContent()}</div> <div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
<Show when={!home() && layout.sidebar.opened()}> <Show when={layout.sidebar.opened()}>
<div onPointerDown={() => setState("sizing", true)}> <div onPointerDown={() => setState("sizing", true)}>
<ResizeHandle <ResizeHandle
direction="horizontal" direction="horizontal"
@@ -2327,54 +2325,46 @@ export default function Layout(props: ParentProps) {
style={{ left: "calc(4rem + 12px)" }} style={{ left: "calc(4rem + 12px)" }}
/> />
<Show when={!home()}> <div class="xl:hidden">
<div class="xl:hidden"> <div
<div classList={{
classList={{ "fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
"fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true, "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(), "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(), }}
}} onClick={(e) => {
onClick={(e) => { if (e.target === e.currentTarget) layout.mobileSidebar.hide()
if (e.target === e.currentTarget) layout.mobileSidebar.hide() }}
}} />
/> <nav
<nav aria-label={language.t("sidebar.nav.projectsAndSessions")}
aria-label={language.t("sidebar.nav.projectsAndSessions")} data-component="sidebar-nav-mobile"
data-component="sidebar-nav-mobile" classList={{
classList={{ "@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
"@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true, "translate-x-0": layout.mobileSidebar.opened(),
"translate-x-0": layout.mobileSidebar.opened(), "-translate-x-full": !layout.mobileSidebar.opened(),
"-translate-x-full": !layout.mobileSidebar.opened(), }}
}} onClick={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()} >
> {sidebarContent(true)}
{sidebarContent(true)} </nav>
</nav> </div>
</div>
</Show>
<div <div
classList={{ classList={{
"absolute inset-0": true, "absolute inset-0": true,
"xl:inset-y-0 xl:right-0": true, "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
"xl:left-[var(--main-left)]": true,
"z-20": true, "z-20": true,
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none": "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
!state.sizing, !state.sizing,
}} }}
style={{ style={{
"--main-left": home() "--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem",
? "4rem"
: layout.sidebar.opened()
? `${Math.max(layout.sidebar.width(), 244)}px`
: "4rem",
}} }}
> >
<main <main
classList={{ classList={{
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base": true, "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
"xl:border-l xl:rounded-tl-[12px]": true,
}} }}
> >
<Show when={!autoselecting()} fallback={<div class="size-full" />}> <Show when={!autoselecting()} fallback={<div class="size-full" />}>
@@ -2383,47 +2373,43 @@ export default function Layout(props: ParentProps) {
</main> </main>
</div> </div>
<Show when={!home()}> <div
<div classList={{
classList={{ "hidden xl:flex absolute inset-y-0 left-16 z-30": true,
"hidden xl:flex absolute inset-y-0 left-16 z-30": true, "opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
"opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(), "opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
"opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(), "transition-[opacity,transform] motion-reduce:transition-none": true,
"transition-[opacity,transform] motion-reduce:transition-none": true, "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(), "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(), }}
}} onMouseMove={disarm}
onMouseMove={disarm} onMouseEnter={() => {
onMouseEnter={() => { disarm()
disarm() aim.reset()
aim.reset() }}
}} onPointerDown={disarm}
onPointerDown={disarm} onMouseLeave={() => {
onMouseLeave={() => { arm()
arm() }}
}} >
> <Show when={peekProject()}>
<Show when={peekProject()}> <SidebarPanel project={peekProject} merged={false} />
<SidebarPanel project={peekProject} merged={false} /> </Show>
</Show> </div>
</div>
</Show>
<Show when={!home()}> <div
<div classList={{
classList={{ "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
"hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true, "opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
"opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(), "opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
"opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(), "transition-[opacity,transform] motion-reduce:transition-none": true,
"transition-[opacity,transform] motion-reduce:transition-none": true, "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(), "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(), }}
}} style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }}
style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }} >
> <div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
<div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} /> </div>
</div>
</Show>
</div> </div>
</div> </div>
{import.meta.env.DEV && <DebugBar />} {import.meta.env.DEV && <DebugBar />}
+4 -7
View File
@@ -11,11 +11,6 @@ const openBrowser = (url: string) => Effect.promise(() => open(url).catch(() =>
const println = (msg: string) => Effect.sync(() => UI.println(msg)) const println = (msg: string) => Effect.sync(() => UI.println(msg))
const isActiveOrgChoice = (
active: Option.Option<{ id: AccountID; active_org_id: OrgID | null }>,
choice: { accountID: AccountID; orgID: OrgID },
) => Option.isSome(active) && active.value.id === choice.accountID && active.value.active_org_id === choice.orgID
const loginEffect = Effect.fn("login")(function* (url: string) { const loginEffect = Effect.fn("login")(function* (url: string) {
const service = yield* AccountService const service = yield* AccountService
@@ -104,10 +99,11 @@ const switchEffect = Effect.fn("switch")(function* () {
if (groups.length === 0) return yield* println("Not logged in") if (groups.length === 0) return yield* println("Not logged in")
const active = yield* service.active() const active = yield* service.active()
const activeOrgID = Option.flatMap(active, (a) => Option.fromNullishOr(a.active_org_id))
const opts = groups.flatMap((group) => const opts = groups.flatMap((group) =>
group.orgs.map((org) => { group.orgs.map((org) => {
const isActive = isActiveOrgChoice(active, { accountID: group.account.id, orgID: org.id }) const isActive = Option.isSome(activeOrgID) && activeOrgID.value === org.id
return { return {
value: { orgID: org.id, accountID: group.account.id, label: org.name }, value: { orgID: org.id, accountID: group.account.id, label: org.name },
label: isActive label: isActive
@@ -136,10 +132,11 @@ const orgsEffect = Effect.fn("orgs")(function* () {
if (!groups.some((group) => group.orgs.length > 0)) return yield* println("No orgs found") if (!groups.some((group) => group.orgs.length > 0)) return yield* println("No orgs found")
const active = yield* service.active() const active = yield* service.active()
const activeOrgID = Option.flatMap(active, (a) => Option.fromNullishOr(a.active_org_id))
for (const group of groups) { for (const group of groups) {
for (const org of group.orgs) { for (const org of group.orgs) {
const isActive = isActiveOrgChoice(active, { accountID: group.account.id, orgID: org.id }) const isActive = Option.isSome(activeOrgID) && activeOrgID.value === org.id
const dot = isActive ? UI.Style.TEXT_SUCCESS + "●" + UI.Style.TEXT_NORMAL : " " const dot = isActive ? UI.Style.TEXT_SUCCESS + "●" + UI.Style.TEXT_NORMAL : " "
const name = isActive ? UI.Style.TEXT_HIGHLIGHT_BOLD + org.name + UI.Style.TEXT_NORMAL : org.name const name = isActive ? UI.Style.TEXT_HIGHLIGHT_BOLD + org.name + UI.Style.TEXT_NORMAL : org.name
const email = UI.Style.TEXT_DIM + group.account.email + UI.Style.TEXT_NORMAL const email = UI.Style.TEXT_DIM + group.account.email + UI.Style.TEXT_NORMAL
+1 -2
View File
@@ -167,8 +167,7 @@ export namespace ProviderError {
export function parseAPICallError(input: { providerID: ProviderID; error: APICallError }): ParsedAPICallError { export function parseAPICallError(input: { providerID: ProviderID; error: APICallError }): ParsedAPICallError {
const m = message(input.providerID, input.error) const m = message(input.providerID, input.error)
const body = json(input.error.responseBody) if (isOverflow(m) || input.error.statusCode === 413) {
if (isOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") {
return { return {
type: "context_overflow", type: "context_overflow",
message: m, message: m,
@@ -869,26 +869,6 @@ describe("session.message-v2.fromError", () => {
}) })
}) })
test("detects context overflow from context_length_exceeded code in response body", () => {
const error = new APICallError({
message: "Request failed",
url: "https://example.com",
requestBodyValues: {},
statusCode: 422,
responseHeaders: { "content-type": "application/json" },
responseBody: JSON.stringify({
error: {
message: "Some message",
type: "invalid_request_error",
code: "context_length_exceeded",
},
}),
isRetryable: false,
})
const result = MessageV2.fromError(error, { providerID })
expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(true)
})
test("does not classify 429 no body as context overflow", () => { test("does not classify 429 no body as context overflow", () => {
const result = MessageV2.fromError( const result = MessageV2.fromError(
new APICallError({ new APICallError({
+1 -11
View File
@@ -6,13 +6,7 @@ import { useWorkerPool } from "../context/worker-pool"
import { createDefaultOptions, styleVariables } from "../pierre" import { createDefaultOptions, styleVariables } from "../pierre"
import { markCommentedDiffLines } from "../pierre/commented-lines" import { markCommentedDiffLines } from "../pierre/commented-lines"
import { fixDiffSelection } from "../pierre/diff-selection" import { fixDiffSelection } from "../pierre/diff-selection"
import { import { clearReadyWatcher, createReadyWatcher, notifyShadowReady } from "../pierre/file-runtime"
applyViewerScheme,
clearReadyWatcher,
createReadyWatcher,
notifyShadowReady,
observeViewerScheme,
} from "../pierre/file-runtime"
import { acquireVirtualizer, virtualMetrics } from "../pierre/virtualizer" import { acquireVirtualizer, virtualMetrics } from "../pierre/virtualizer"
import { File, type DiffFileProps, type FileProps } from "./file" import { File, type DiffFileProps, type FileProps } from "./file"
@@ -87,8 +81,6 @@ function DiffSSRViewer<T>(props: SSRDiffFileProps<T>) {
onMount(() => { onMount(() => {
if (isServer) return if (isServer) return
onCleanup(observeViewerScheme(() => fileDiffRef))
const virtualizer = getVirtualizer() const virtualizer = getVirtualizer()
fileDiffInstance = virtualizer fileDiffInstance = virtualizer
? new VirtualizedFileDiff<T>( ? new VirtualizedFileDiff<T>(
@@ -110,8 +102,6 @@ function DiffSSRViewer<T>(props: SSRDiffFileProps<T>) {
workerPool, workerPool,
) )
applyViewerScheme(fileDiffRef)
// @ts-expect-error private field required for hydration // @ts-expect-error private field required for hydration
fileDiffInstance.fileContainer = fileDiffRef fileDiffInstance.fileContainer = fileDiffRef
fileDiffInstance.hydrate({ fileDiffInstance.hydrate({
+2 -17
View File
@@ -16,20 +16,12 @@ import {
} from "@pierre/diffs" } from "@pierre/diffs"
import { type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { createMediaQuery } from "@solid-primitives/media" import { createMediaQuery } from "@solid-primitives/media"
import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js" import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, Show, splitProps } from "solid-js"
import { createDefaultOptions, styleVariables } from "../pierre" import { createDefaultOptions, styleVariables } from "../pierre"
import { markCommentedDiffLines, markCommentedFileLines } from "../pierre/commented-lines" import { markCommentedDiffLines, markCommentedFileLines } from "../pierre/commented-lines"
import { fixDiffSelection, findDiffSide, type DiffSelectionSide } from "../pierre/diff-selection" import { fixDiffSelection, findDiffSide, type DiffSelectionSide } from "../pierre/diff-selection"
import { createFileFind } from "../pierre/file-find" import { createFileFind } from "../pierre/file-find"
import { import { clearReadyWatcher, createReadyWatcher, getViewerRoot, notifyShadowReady } from "../pierre/file-runtime"
applyViewerScheme,
clearReadyWatcher,
createReadyWatcher,
getViewerHost,
getViewerRoot,
notifyShadowReady,
observeViewerScheme,
} from "../pierre/file-runtime"
import { import {
findCodeSelectionSide, findCodeSelectionSide,
findDiffLineNumber, findDiffLineNumber,
@@ -154,7 +146,6 @@ function useFileViewer(config: ViewerConfig) {
const [rendered, setRendered] = createSignal(0) const [rendered, setRendered] = createSignal(0)
const getRoot = () => getViewerRoot(container) const getRoot = () => getViewerRoot(container)
const getHost = () => getViewerHost(container)
const find = createFileFind({ const find = createFileFind({
wrapper: () => wrapper, wrapper: () => wrapper,
@@ -265,10 +256,6 @@ function useFileViewer(config: ViewerConfig) {
// -- shared effects -- // -- shared effects --
onMount(() => {
onCleanup(observeViewerScheme(getHost))
})
createEffect(() => { createEffect(() => {
rendered() rendered()
const ranges = config.commentedLines() const ranges = config.commentedLines()
@@ -351,7 +338,6 @@ function useFileViewer(config: ViewerConfig) {
rendered, rendered,
setRendered, setRendered,
getRoot, getRoot,
getHost,
find, find,
scheduleSelectionUpdate, scheduleSelectionUpdate,
} }
@@ -486,7 +472,6 @@ function renderViewer<I extends RenderTarget>(opts: {
opts.viewer.container.innerHTML = "" opts.viewer.container.innerHTML = ""
opts.draw(next) opts.draw(next)
applyViewerScheme(opts.viewer.getHost())
opts.viewer.setRendered((value) => value + 1) opts.viewer.setRendered((value) => value + 1)
opts.onReady() opts.onReady()
} }
+12 -2
View File
@@ -1050,8 +1050,18 @@
line-height: var(--line-height-large); line-height: var(--line-height-large);
color: var(--text-base); color: var(--text-base);
min-width: 0; min-width: 0;
overflow-wrap: anywhere; overflow: hidden;
white-space: normal; text-overflow: ellipsis;
white-space: nowrap;
}
[data-slot="question-option"][data-custom="true"] {
[data-slot="option-description"] {
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
}
} }
[data-slot="question-custom"] { [data-slot="question-custom"] {
-25
View File
@@ -23,31 +23,6 @@ export function getViewerRoot(container: HTMLElement | undefined) {
return getViewerHost(container)?.shadowRoot ?? undefined return getViewerHost(container)?.shadowRoot ?? undefined
} }
export function applyViewerScheme(host: HTMLElement | undefined) {
if (!host) return
if (typeof document === "undefined") return
const scheme = document.documentElement.dataset.colorScheme
if (scheme === "dark" || scheme === "light") {
host.dataset.colorScheme = scheme
return
}
host.removeAttribute("data-color-scheme")
}
export function observeViewerScheme(getHost: () => HTMLElement | undefined) {
if (typeof document === "undefined") return () => {}
applyViewerScheme(getHost())
if (typeof MutationObserver === "undefined") return () => {}
const root = document.documentElement
const monitor = new MutationObserver(() => applyViewerScheme(getHost()))
monitor.observe(root, { attributes: true, attributeFilter: ["data-color-scheme"] })
return () => monitor.disconnect()
}
export function notifyShadowReady(opts: { export function notifyShadowReady(opts: {
state: ReadyWatcher state: ReadyWatcher
container: HTMLElement container: HTMLElement
+6 -13
View File
@@ -37,26 +37,19 @@ const unsafeCSS = `
--diffs-bg-addition-emphasis: var(--diffs-bg-addition-emphasis-override, light-dark(rgb(from var(--diffs-addition-base) r g b / 0.07), rgb(from var(--diffs-addition-base) r g b / 0.1))); --diffs-bg-addition-emphasis: var(--diffs-bg-addition-emphasis-override, light-dark(rgb(from var(--diffs-addition-base) r g b / 0.07), rgb(from var(--diffs-addition-base) r g b / 0.1)));
--diffs-selection-base: var(--surface-warning-strong); --diffs-selection-base: var(--surface-warning-strong);
--diffs-selection-border: var(--border-warning-base); --diffs-selection-border: var(--border-warning-base);
--diffs-selection-number-fg: #1c1917; --diffs-selection-number-fg: light-dark(#1c1917, #fdfbfb);
/* Use explicit alpha instead of color-mix(..., transparent) to avoid Safari's non-premultiplied interpolation bugs. */ /* Use explicit alpha instead of color-mix(..., transparent) to avoid Safari's non-premultiplied interpolation bugs. */
--diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--surface-warning-base) r g b / 0.65)); --diffs-bg-selection: var(
--diffs-bg-selection-override,
light-dark(rgb(from var(--surface-warning-base) r g b / 0.65), rgb(from var(--solaris-dark-6) r g b / 0.65))
);
--diffs-bg-selection-number: var( --diffs-bg-selection-number: var(
--diffs-bg-selection-number-override, --diffs-bg-selection-number-override,
rgb(from var(--surface-warning-base) r g b / 0.85) light-dark(rgb(from var(--surface-warning-base) r g b / 0.85), rgb(from var(--solaris-dark-6) r g b / 0.85))
); );
--diffs-bg-selection-text: rgb(from var(--surface-warning-strong) r g b / 0.2); --diffs-bg-selection-text: rgb(from var(--surface-warning-strong) r g b / 0.2);
} }
:host([data-color-scheme='dark']) [data-diff],
:host([data-color-scheme='dark']) [data-file] {
--diffs-selection-number-fg: #fdfbfb;
--diffs-bg-selection: var(--diffs-bg-selection-override, rgb(from var(--solaris-dark-6) r g b / 0.65));
--diffs-bg-selection-number: var(
--diffs-bg-selection-number-override,
rgb(from var(--solaris-dark-6) r g b / 0.85)
);
}
[data-diff] ::selection, [data-diff] ::selection,
[data-file] ::selection { [data-file] ::selection {
background-color: var(--diffs-bg-selection-text); background-color: var(--diffs-bg-selection-text);