summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/src/components/debug-bar.tsx432
-rw-r--r--packages/app/src/pages/layout.tsx366
2 files changed, 621 insertions, 177 deletions
diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx
new file mode 100644
index 000000000..93a4654f8
--- /dev/null
+++ b/packages/app/src/components/debug-bar.tsx
@@ -0,0 +1,432 @@
+import { useIsRouting, useLocation } from "@solidjs/router"
+import { batch, createEffect, onCleanup, onMount } from "solid-js"
+import { createStore } from "solid-js/store"
+import { Tooltip } from "@opencode-ai/ui/tooltip"
+
+type Mem = Performance & {
+ memory?: {
+ usedJSHeapSize: number
+ jsHeapSizeLimit: number
+ }
+}
+
+type Evt = PerformanceEntry & {
+ interactionId?: number
+ processingStart?: number
+}
+
+type Shift = PerformanceEntry & {
+ hadRecentInput: boolean
+ value: number
+}
+
+type Obs = PerformanceObserverInit & {
+ durationThreshold?: number
+}
+
+const span = 5000
+
+const ms = (n?: number, d = 0) => {
+ if (n === undefined || Number.isNaN(n)) return "n/a"
+ return `${n.toFixed(d)}ms`
+}
+
+const time = (n?: number) => {
+ if (n === undefined || Number.isNaN(n)) return "n/a"
+ return `${Math.round(n)}`
+}
+
+const mb = (n?: number) => {
+ if (n === undefined || Number.isNaN(n)) return "n/a"
+ const v = n / 1024 / 1024
+ return `${v >= 1024 ? v.toFixed(0) : v.toFixed(1)}MB`
+}
+
+const bad = (n: number | undefined, limit: number, low = false) => {
+ if (n === undefined || Number.isNaN(n)) return false
+ return low ? n < limit : n > limit
+}
+
+const session = (path: string) => path.includes("/session")
+
+function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string; value: string }) {
+ return (
+ <Tooltip value={props.tip} placement="left">
+ <div class="flex w-full flex-col items-center px-0.5 py-1 text-center">
+ <div class="text-[7px] font-black uppercase tracking-[0.04em] opacity-70 leading-none">{props.label}</div>
+ <div
+ classList={{
+ "text-[9px] font-semibold leading-none tabular-nums": true,
+ "text-text-on-critical-base": !!props.bad,
+ "opacity-70": !!props.dim,
+ }}
+ >
+ {props.value}
+ </div>
+ </div>
+ </Tooltip>
+ )
+}
+
+export function DebugBar() {
+ const location = useLocation()
+ const routing = useIsRouting()
+ const [state, setState] = createStore({
+ cls: undefined as number | undefined,
+ delay: undefined as number | undefined,
+ fps: undefined as number | undefined,
+ gap: undefined as number | undefined,
+ heap: {
+ limit: undefined as number | undefined,
+ used: undefined as number | undefined,
+ },
+ inp: undefined as number | undefined,
+ jank: undefined as number | undefined,
+ long: {
+ block: undefined as number | undefined,
+ count: undefined as number | undefined,
+ max: undefined as number | undefined,
+ },
+ nav: {
+ dur: undefined as number | undefined,
+ pending: false,
+ },
+ })
+
+ const heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined)
+ const heapv = () => {
+ const value = heap()
+ if (value === undefined) return "n/a"
+ return `${Math.round(value * 100)}%`
+ }
+ const longv = () => (state.long.count === undefined ? "n/a" : `${time(state.long.block)}/${state.long.count}`)
+ const navv = () => (state.nav.pending ? "..." : time(state.nav.dur))
+
+ let prev = ""
+ let start = 0
+ let init = false
+ let one = 0
+ let two = 0
+
+ createEffect(() => {
+ const busy = routing()
+ const next = `${location.pathname}${location.search}`
+
+ if (!init) {
+ init = true
+ prev = next
+ return
+ }
+
+ if (busy) {
+ if (one !== 0) cancelAnimationFrame(one)
+ if (two !== 0) cancelAnimationFrame(two)
+ one = 0
+ two = 0
+ if (start !== 0) return
+ start = performance.now()
+ if (session(prev)) setState("nav", { dur: undefined, pending: true })
+ return
+ }
+
+ if (start === 0) {
+ prev = next
+ return
+ }
+
+ const at = start
+ const from = prev
+ start = 0
+ prev = next
+
+ if (!(session(from) || session(next))) return
+
+ if (one !== 0) cancelAnimationFrame(one)
+ if (two !== 0) cancelAnimationFrame(two)
+ one = requestAnimationFrame(() => {
+ one = 0
+ two = requestAnimationFrame(() => {
+ two = 0
+ setState("nav", { dur: performance.now() - at, pending: false })
+ })
+ })
+ })
+
+ onMount(() => {
+ const obs: PerformanceObserver[] = []
+ const fps: Array<{ at: number; dur: number }> = []
+ const long: Array<{ at: number; dur: number }> = []
+ const seen = new Map<number | string, { at: number; delay: number; dur: number }>()
+ let hasLong = false
+ let poll: number | undefined
+ let raf = 0
+ let last = 0
+ let snap = 0
+
+ const trim = (list: Array<{ at: number; dur: number }>, span: number, at: number) => {
+ while (list[0] && at - list[0].at > span) list.shift()
+ }
+
+ const syncFrame = (at: number) => {
+ trim(fps, span, at)
+ const total = fps.reduce((sum, entry) => sum + entry.dur, 0)
+ const gap = fps.reduce((max, entry) => Math.max(max, entry.dur), 0)
+ const jank = fps.filter((entry) => entry.dur > 32).length
+ batch(() => {
+ setState("fps", total > 0 ? (fps.length * 1000) / total : undefined)
+ setState("gap", gap > 0 ? gap : undefined)
+ setState("jank", jank)
+ })
+ }
+
+ const syncLong = (at = performance.now()) => {
+ if (!hasLong) return
+ trim(long, span, at)
+ const block = long.reduce((sum, entry) => sum + Math.max(0, entry.dur - 50), 0)
+ const max = long.reduce((hi, entry) => Math.max(hi, entry.dur), 0)
+ setState("long", { block, count: long.length, max })
+ }
+
+ const syncInp = (at = performance.now()) => {
+ for (const [key, entry] of seen) {
+ if (at - entry.at > span) seen.delete(key)
+ }
+ let delay = 0
+ let inp = 0
+ for (const entry of seen.values()) {
+ delay = Math.max(delay, entry.delay)
+ inp = Math.max(inp, entry.dur)
+ }
+ batch(() => {
+ setState("delay", delay > 0 ? delay : undefined)
+ setState("inp", inp > 0 ? inp : undefined)
+ })
+ }
+
+ const syncHeap = () => {
+ const mem = (performance as Mem).memory
+ if (!mem) return
+ setState("heap", { limit: mem.jsHeapSizeLimit, used: mem.usedJSHeapSize })
+ }
+
+ const reset = () => {
+ fps.length = 0
+ long.length = 0
+ seen.clear()
+ last = 0
+ snap = 0
+ batch(() => {
+ setState("fps", undefined)
+ setState("gap", undefined)
+ setState("jank", undefined)
+ setState("delay", undefined)
+ setState("inp", undefined)
+ if (hasLong) setState("long", { block: 0, count: 0, max: 0 })
+ })
+ }
+
+ const watch = (type: string, init: Obs, fn: (entries: PerformanceEntry[]) => void) => {
+ if (typeof PerformanceObserver === "undefined") return false
+ if (!(PerformanceObserver.supportedEntryTypes ?? []).includes(type)) return false
+ const ob = new PerformanceObserver((list) => fn(list.getEntries()))
+ try {
+ ob.observe(init)
+ obs.push(ob)
+ return true
+ } catch {
+ ob.disconnect()
+ return false
+ }
+ }
+
+ if (
+ watch("layout-shift", { buffered: true, type: "layout-shift" }, (entries) => {
+ const add = entries.reduce((sum, entry) => {
+ const item = entry as Shift
+ if (item.hadRecentInput) return sum
+ return sum + item.value
+ }, 0)
+ if (add === 0) return
+ setState("cls", (value) => (value ?? 0) + add)
+ })
+ ) {
+ setState("cls", 0)
+ }
+
+ if (
+ watch("longtask", { buffered: true, type: "longtask" }, (entries) => {
+ const at = performance.now()
+ long.push(...entries.map((entry) => ({ at: entry.startTime, dur: entry.duration })))
+ syncLong(at)
+ })
+ ) {
+ hasLong = true
+ setState("long", { block: 0, count: 0, max: 0 })
+ }
+
+ watch("event", { buffered: true, durationThreshold: 16, type: "event" }, (entries) => {
+ for (const raw of entries) {
+ const entry = raw as Evt
+ if (entry.duration < 16) continue
+ const key =
+ entry.interactionId && entry.interactionId > 0
+ ? entry.interactionId
+ : `${entry.name}:${Math.round(entry.startTime)}`
+ const prev = seen.get(key)
+ const delay = Math.max(0, (entry.processingStart ?? entry.startTime) - entry.startTime)
+ seen.set(key, {
+ at: entry.startTime,
+ delay: Math.max(prev?.delay ?? 0, delay),
+ dur: Math.max(prev?.dur ?? 0, entry.duration),
+ })
+ if (seen.size <= 200) continue
+ const first = seen.keys().next().value
+ if (first !== undefined) seen.delete(first)
+ }
+ syncInp()
+ })
+
+ const loop = (at: number) => {
+ if (document.visibilityState !== "visible") {
+ raf = 0
+ return
+ }
+
+ if (last === 0) {
+ last = at
+ raf = requestAnimationFrame(loop)
+ return
+ }
+
+ fps.push({ at, dur: at - last })
+ last = at
+
+ if (at - snap >= 250) {
+ snap = at
+ syncFrame(at)
+ }
+
+ raf = requestAnimationFrame(loop)
+ }
+
+ const stop = () => {
+ if (raf !== 0) cancelAnimationFrame(raf)
+ raf = 0
+ if (poll === undefined) return
+ clearInterval(poll)
+ poll = undefined
+ }
+
+ const start = () => {
+ if (document.visibilityState !== "visible") return
+ if (poll === undefined) {
+ poll = window.setInterval(() => {
+ syncLong()
+ syncInp()
+ syncHeap()
+ }, 1000)
+ }
+ if (raf !== 0) return
+ raf = requestAnimationFrame(loop)
+ }
+
+ const vis = () => {
+ if (document.visibilityState !== "visible") {
+ stop()
+ return
+ }
+ reset()
+ start()
+ }
+
+ syncHeap()
+ start()
+ document.addEventListener("visibilitychange", vis)
+
+ onCleanup(() => {
+ if (one !== 0) cancelAnimationFrame(one)
+ if (two !== 0) cancelAnimationFrame(two)
+ stop()
+ document.removeEventListener("visibilitychange", vis)
+ for (const ob of obs) ob.disconnect()
+ })
+ })
+
+ return (
+ <aside
+ aria-label="Development performance diagnostics"
+ class="pointer-events-auto h-full min-h-0 w-[36px] shrink-0 overflow-y-auto text-text-on-interactive-base no-scrollbar sm:w-[38px]"
+ style={{ "background-color": "color-mix(in srgb, var(--icon-interactive-base) 42%, black)" }}
+ >
+ <div class="flex min-h-full flex-col gap-0.5 py-2 font-mono">
+ <Cell
+ label="NAV"
+ tip="Last completed route transition touching a session page, measured from router start until the first paint after it settles."
+ value={navv()}
+ bad={bad(state.nav.dur, 400)}
+ dim={state.nav.dur === undefined && !state.nav.pending}
+ />
+ <Cell
+ label="FPS"
+ tip="Rolling frames per second over the last 5 seconds."
+ value={state.fps === undefined ? "n/a" : `${Math.round(state.fps)}`}
+ bad={bad(state.fps, 50, true)}
+ dim={state.fps === undefined}
+ />
+ <Cell
+ label="FRM"
+ tip="Worst frame time over the last 5 seconds."
+ value={time(state.gap)}
+ bad={bad(state.gap, 50)}
+ dim={state.gap === undefined}
+ />
+ <Cell
+ label="JNK"
+ tip="Frames over 32ms in the last 5 seconds."
+ value={state.jank === undefined ? "n/a" : `${state.jank}`}
+ bad={bad(state.jank, 8)}
+ dim={state.jank === undefined}
+ />
+ <Cell
+ label="LNG"
+ tip={`Blocked time and long-task count in the last 5 seconds. Max task: ${ms(state.long.max)}.`}
+ value={longv()}
+ bad={bad(state.long.block, 200)}
+ dim={state.long.count === undefined}
+ />
+ <Cell
+ label="DLY"
+ tip="Worst observed input delay in the last 5 seconds."
+ value={time(state.delay)}
+ bad={bad(state.delay, 100)}
+ dim={state.delay === undefined}
+ />
+ <Cell
+ label="INP"
+ tip="Approximate interaction duration over the last 5 seconds. This is INP-like, not the official Web Vitals INP."
+ value={time(state.inp)}
+ bad={bad(state.inp, 200)}
+ dim={state.inp === undefined}
+ />
+ <Cell
+ label="CLS"
+ tip="Cumulative layout shift for the current app lifetime."
+ value={state.cls === undefined ? "n/a" : state.cls.toFixed(2)}
+ bad={bad(state.cls, 0.1)}
+ dim={state.cls === undefined}
+ />
+ <Cell
+ label="MEM"
+ tip={
+ state.heap.used === undefined
+ ? "Used JS heap vs heap limit. Chromium only."
+ : `Used JS heap vs heap limit. ${mb(state.heap.used)} of ${mb(state.heap.limit)}.`
+ }
+ value={heapv()}
+ bad={bad(heap(), 0.8)}
+ dim={state.heap.used === undefined}
+ />
+ </div>
+ </aside>
+ )
+}
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 40015db1b..052a03c54 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -54,6 +54,7 @@ import { useCommand, type CommandOption } from "@/context/command"
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
import { DialogEditProject } from "@/components/dialog-edit-project"
+import { DebugBar } from "@/components/debug-bar"
import { Titlebar } from "@/components/titlebar"
import { useServer } from "@/context/server"
import { useLanguage, type Locale } from "@/context/language"
@@ -2135,193 +2136,204 @@ export default function Layout(props: ParentProps) {
}
return (
- <div class="relative bg-background-base flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
+ <div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
<Titlebar />
- <div class="flex-1 min-h-0 relative overflow-x-hidden">
- <nav
- aria-label={language.t("sidebar.nav.projectsAndSessions")}
- data-component="sidebar-nav-desktop"
- classList={{
- "hidden xl:block": true,
- "absolute inset-y-0 left-0": true,
- "z-10": true,
- }}
- style={{ width: `${Math.max(layout.sidebar.width(), 244)}px` }}
- ref={(el) => {
- setState("nav", el)
- }}
- onMouseEnter={() => {
- disarm()
- }}
- onMouseLeave={() => {
- aim.reset()
- if (!sidebarHovering()) return
+ <div class="flex-1 min-h-0 min-w-0 flex">
+ <div class="flex-1 min-h-0 relative">
+ <div class="size-full relative overflow-x-hidden">
+ <nav
+ aria-label={language.t("sidebar.nav.projectsAndSessions")}
+ data-component="sidebar-nav-desktop"
+ classList={{
+ "hidden xl:block": true,
+ "absolute inset-y-0 left-0": true,
+ "z-10": true,
+ }}
+ style={{ width: `${Math.max(layout.sidebar.width(), 244)}px` }}
+ ref={(el) => {
+ setState("nav", el)
+ }}
+ onMouseEnter={() => {
+ disarm()
+ }}
+ onMouseLeave={() => {
+ aim.reset()
+ if (!sidebarHovering()) return
+
+ arm()
+ }}
+ >
+ <div class="@container w-full h-full contain-strict">
+ <SidebarContent
+ opened={() => layout.sidebar.opened()}
+ aimMove={aim.move}
+ projects={() => layout.projects.list()}
+ renderProject={(project) => (
+ <SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} />
+ )}
+ handleDragStart={handleDragStart}
+ handleDragEnd={handleDragEnd}
+ handleDragOver={handleDragOver}
+ openProjectLabel={language.t("command.project.open")}
+ openProjectKeybind={() => command.keybind("project.open")}
+ onOpenProject={chooseProject}
+ renderProjectOverlay={() => (
+ <ProjectDragOverlay
+ projects={() => layout.projects.list()}
+ activeProject={() => store.activeProject}
+ />
+ )}
+ settingsLabel={() => language.t("sidebar.settings")}
+ settingsKeybind={() => command.keybind("settings.open")}
+ onOpenSettings={openSettings}
+ helpLabel={() => language.t("sidebar.help")}
+ onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
+ renderPanel={() => (
+ <Show when={currentProject()} keyed>
+ {(project) => <SidebarPanel project={project} merged />}
+ </Show>
+ )}
+ />
+ </div>
+ <Show when={layout.sidebar.opened()}>
+ <div onPointerDown={() => setSizing(true)}>
+ <ResizeHandle
+ direction="horizontal"
+ size={layout.sidebar.width()}
+ min={244}
+ max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
+ collapseThreshold={244}
+ onResize={(w) => {
+ setSizing(true)
+ if (sizet !== undefined) clearTimeout(sizet)
+ sizet = window.setTimeout(() => setSizing(false), 120)
+ layout.sidebar.resize(w)
+ }}
+ onCollapse={layout.sidebar.close}
+ />
+ </div>
+ </Show>
+ </nav>
- arm()
- }}
- >
- <div class="@container w-full h-full contain-strict">
- <SidebarContent
- opened={() => layout.sidebar.opened()}
- aimMove={aim.move}
- projects={() => layout.projects.list()}
- renderProject={(project) => (
- <SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} />
- )}
- handleDragStart={handleDragStart}
- handleDragEnd={handleDragEnd}
- handleDragOver={handleDragOver}
- openProjectLabel={language.t("command.project.open")}
- openProjectKeybind={() => command.keybind("project.open")}
- onOpenProject={chooseProject}
- renderProjectOverlay={() => (
- <ProjectDragOverlay projects={() => layout.projects.list()} activeProject={() => store.activeProject} />
- )}
- settingsLabel={() => language.t("sidebar.settings")}
- settingsKeybind={() => command.keybind("settings.open")}
- onOpenSettings={openSettings}
- helpLabel={() => language.t("sidebar.help")}
- onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
- renderPanel={() => (
- <Show when={currentProject()} keyed>
- {(project) => <SidebarPanel project={project} merged />}
- </Show>
- )}
+ <div
+ class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
+ style={{ left: "calc(4rem + 12px)" }}
/>
- </div>
- <Show when={layout.sidebar.opened()}>
- <div onPointerDown={() => setSizing(true)}>
- <ResizeHandle
- direction="horizontal"
- size={layout.sidebar.width()}
- min={244}
- max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
- collapseThreshold={244}
- onResize={(w) => {
- setSizing(true)
- if (sizet !== undefined) clearTimeout(sizet)
- sizet = window.setTimeout(() => setSizing(false), 120)
- layout.sidebar.resize(w)
+
+ <div class="xl:hidden">
+ <div
+ classList={{
+ "fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
+ "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
+ "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
+ }}
+ onClick={(e) => {
+ if (e.target === e.currentTarget) layout.mobileSidebar.hide()
}}
- onCollapse={layout.sidebar.close}
/>
+ <nav
+ aria-label={language.t("sidebar.nav.projectsAndSessions")}
+ data-component="sidebar-nav-mobile"
+ 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,
+ "translate-x-0": layout.mobileSidebar.opened(),
+ "-translate-x-full": !layout.mobileSidebar.opened(),
+ }}
+ onClick={(e) => e.stopPropagation()}
+ >
+ <SidebarContent
+ mobile
+ opened={() => layout.sidebar.opened()}
+ aimMove={aim.move}
+ projects={() => layout.projects.list()}
+ renderProject={(project) => (
+ <SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} mobile />
+ )}
+ handleDragStart={handleDragStart}
+ handleDragEnd={handleDragEnd}
+ handleDragOver={handleDragOver}
+ openProjectLabel={language.t("command.project.open")}
+ openProjectKeybind={() => command.keybind("project.open")}
+ onOpenProject={chooseProject}
+ renderProjectOverlay={() => (
+ <ProjectDragOverlay
+ projects={() => layout.projects.list()}
+ activeProject={() => store.activeProject}
+ />
+ )}
+ settingsLabel={() => language.t("sidebar.settings")}
+ settingsKeybind={() => command.keybind("settings.open")}
+ onOpenSettings={openSettings}
+ helpLabel={() => language.t("sidebar.help")}
+ onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
+ renderPanel={() => <SidebarPanel project={currentProject()} mobile />}
+ />
+ </nav>
</div>
- </Show>
- </nav>
-
- <div
- class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
- style={{ left: "calc(4rem + 12px)" }}
- />
-
- <div class="xl:hidden">
- <div
- classList={{
- "fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
- "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
- "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
- }}
- onClick={(e) => {
- if (e.target === e.currentTarget) layout.mobileSidebar.hide()
- }}
- />
- <nav
- aria-label={language.t("sidebar.nav.projectsAndSessions")}
- data-component="sidebar-nav-mobile"
- 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,
- "translate-x-0": layout.mobileSidebar.opened(),
- "-translate-x-full": !layout.mobileSidebar.opened(),
- }}
- onClick={(e) => e.stopPropagation()}
- >
- <SidebarContent
- mobile
- opened={() => layout.sidebar.opened()}
- aimMove={aim.move}
- projects={() => layout.projects.list()}
- renderProject={(project) => (
- <SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} mobile />
- )}
- handleDragStart={handleDragStart}
- handleDragEnd={handleDragEnd}
- handleDragOver={handleDragOver}
- openProjectLabel={language.t("command.project.open")}
- openProjectKeybind={() => command.keybind("project.open")}
- onOpenProject={chooseProject}
- renderProjectOverlay={() => (
- <ProjectDragOverlay projects={() => layout.projects.list()} activeProject={() => store.activeProject} />
- )}
- settingsLabel={() => language.t("sidebar.settings")}
- settingsKeybind={() => command.keybind("settings.open")}
- onOpenSettings={openSettings}
- helpLabel={() => language.t("sidebar.help")}
- onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
- renderPanel={() => <SidebarPanel project={currentProject()} mobile />}
- />
- </nav>
- </div>
- <div
- classList={{
- "absolute inset-0": true,
- "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
- "z-20": true,
- "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
- !sizing(),
- }}
- style={{
- "--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem",
- }}
- >
- <main
- classList={{
- "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,
- }}
- >
- <Show when={!autoselecting()} fallback={<div class="size-full" />}>
- {props.children}
- </Show>
- </main>
- </div>
+ <div
+ classList={{
+ "absolute inset-0": true,
+ "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
+ "z-20": true,
+ "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
+ !sizing(),
+ }}
+ style={{
+ "--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem",
+ }}
+ >
+ <main
+ classList={{
+ "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,
+ }}
+ >
+ <Show when={!autoselecting()} fallback={<div class="size-full" />}>
+ {props.children}
+ </Show>
+ </main>
+ </div>
- <div
- classList={{
- "hidden xl:flex absolute inset-y-0 left-16 z-30": true,
- "opacity-100 translate-x-0 pointer-events-auto": peeked() && !layout.sidebar.opened(),
- "opacity-0 -translate-x-2 pointer-events-none": !peeked() || layout.sidebar.opened(),
- "transition-[opacity,transform] motion-reduce:transition-none": true,
- "duration-180 ease-out": peeked() && !layout.sidebar.opened(),
- "duration-120 ease-in": !peeked() || layout.sidebar.opened(),
- }}
- onMouseMove={disarm}
- onMouseEnter={() => {
- disarm()
- aim.reset()
- }}
- onPointerDown={disarm}
- onMouseLeave={() => {
- arm()
- }}
- >
- <Show when={peek()} keyed>
- {(project) => <SidebarPanel project={project} merged={false} />}
- </Show>
- </div>
+ <div
+ classList={{
+ "hidden xl:flex absolute inset-y-0 left-16 z-30": true,
+ "opacity-100 translate-x-0 pointer-events-auto": peeked() && !layout.sidebar.opened(),
+ "opacity-0 -translate-x-2 pointer-events-none": !peeked() || layout.sidebar.opened(),
+ "transition-[opacity,transform] motion-reduce:transition-none": true,
+ "duration-180 ease-out": peeked() && !layout.sidebar.opened(),
+ "duration-120 ease-in": !peeked() || layout.sidebar.opened(),
+ }}
+ onMouseMove={disarm}
+ onMouseEnter={() => {
+ disarm()
+ aim.reset()
+ }}
+ onPointerDown={disarm}
+ onMouseLeave={() => {
+ arm()
+ }}
+ >
+ <Show when={peek()} keyed>
+ {(project) => <SidebarPanel project={project} merged={false} />}
+ </Show>
+ </div>
- <div
- classList={{
- "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
- "opacity-100 translate-x-0": peeked() && !layout.sidebar.opened(),
- "opacity-0 -translate-x-2": !peeked() || layout.sidebar.opened(),
- "transition-[opacity,transform] motion-reduce:transition-none": true,
- "duration-180 ease-out": peeked() && !layout.sidebar.opened(),
- "duration-120 ease-in": !peeked() || layout.sidebar.opened(),
- }}
- 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
+ classList={{
+ "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
+ "opacity-100 translate-x-0": peeked() && !layout.sidebar.opened(),
+ "opacity-0 -translate-x-2": !peeked() || layout.sidebar.opened(),
+ "transition-[opacity,transform] motion-reduce:transition-none": true,
+ "duration-180 ease-out": peeked() && !layout.sidebar.opened(),
+ "duration-120 ease-in": !peeked() || layout.sidebar.opened(),
+ }}
+ 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>
+ </div>
</div>
+ {import.meta.env.DEV && <DebugBar />}
</div>
<Toast.Region />
</div>