Adam 1 месяц назад
Родитель
Сommit
ee18c9976e
2 измененных файлов с 621 добавлено и 177 удалено
  1. 432 0
      packages/app/src/components/debug-bar.tsx
  2. 189 177
      packages/app/src/pages/layout.tsx

+ 432 - 0
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>
+  )
+}

+ 189 - 177
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>