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 })
})
})
-7
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,7 +175,6 @@ 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"
@@ -188,9 +186,7 @@ export function Titlebar() {
/> />
</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"
@@ -202,9 +198,7 @@ export function Titlebar() {
/> />
</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"
@@ -288,7 +282,6 @@ export function Titlebar() {
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
</Show>
</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) {
+6 -20
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,7 +2325,6 @@ 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={{
@@ -2352,29 +2349,22 @@ export default function Layout(props: ParentProps) {
{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,7 +2373,6 @@ 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,
@@ -2407,9 +2396,7 @@ export default function Layout(props: ParentProps) {
<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,
@@ -2423,7 +2410,6 @@ export default function Layout(props: ParentProps) {
> >
<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()
} }
+11 -1
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;
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; 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);