Просмотр исходного кода

feat: loading session header in a portal, show in toolbar

Aaron Iker 3 месяцев назад
Родитель
Сommit
f552eea391

+ 0 - 1
packages/app/src/components/session/index.ts

@@ -1,4 +1,3 @@
-export { SessionHeader } from "./session-header"
 export { SessionContextTab } from "./session-context-tab"
 export { SortableTab, FileVisual } from "./session-sortable-tab"
 export { SortableTerminalTab } from "./session-sortable-terminal-tab"

+ 66 - 0
packages/app/src/components/toolbar/index.tsx

@@ -0,0 +1,66 @@
+import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
+import { Button } from "@opencode-ai/ui/button"
+import { Icon } from "@opencode-ai/ui/icon"
+import type { Component, ComponentProps } from "solid-js"
+import { useLayout } from "@/context/layout"
+import { useCommand } from "@/context/command"
+
+const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
+
+// ID for the portal mount target
+export const TOOLBAR_PORTAL_ID = "toolbar-content-portal"
+
+export const Toolbar: Component<ComponentProps<"div">> = ({ class: className, ...props }) => {
+  const command = useCommand()
+  const layout = useLayout()
+
+  return (
+    <div
+      classList={{
+        "pl-[80px]": IS_MAC,
+        "pl-2": !IS_MAC,
+        "py-2 mx-px bg-background-base border-b border-border-weak-base flex items-center justify-between w-full border-box relative": true,
+        ...(className ? { [className]: true } : {}),
+      }}
+      data-tauri-drag-region
+      {...props}
+    >
+      <TooltipKeybind
+        class="shrink-0 relative z-10"
+        placement="bottom"
+        title="Toggle sidebar"
+        keybind={command.keybind("sidebar.toggle")}
+      >
+        <Button
+          variant="ghost"
+          size="normal"
+          class="group/sidebar-toggle shrink-0 text-left justify-center align-middle rounded-lg px-1.5"
+          onClick={layout.sidebar.toggle}
+        >
+          <div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
+            <Icon
+              name={layout.sidebar.opened() ? "layout-left" : "layout-right"}
+              size="small"
+              class="group-hover/sidebar-toggle:hidden"
+            />
+            <Icon
+              name={layout.sidebar.opened() ? "layout-left-partial" : "layout-right-partial"}
+              size="small"
+              class="hidden group-hover/sidebar-toggle:inline-block"
+            />
+            <Icon
+              name={layout.sidebar.opened() ? "layout-left-full" : "layout-right-full"}
+              size="small"
+              class="hidden group-active/sidebar-toggle:inline-block"
+            />
+          </div>
+        </Button>
+      </TooltipKeybind>
+      {/* Portal mount target - content rendered here from DirectoryLayout */}
+      <div id={TOOLBAR_PORTAL_ID} class="contents" />
+    </div>
+  )
+}
+
+// Re-export for use in DirectoryLayout
+export { ToolbarSession } from "./session"

+ 103 - 101
packages/app/src/components/session/session-header.tsx → packages/app/src/components/toolbar/session.tsx

@@ -1,4 +1,4 @@
-import { createMemo, createResource, Show } from "solid-js"
+import { ComponentProps, createMemo, createResource, Show, Component } from "solid-js"
 import { A, useNavigate, useParams } from "@solidjs/router"
 import { useLayout } from "@/context/layout"
 import { useCommand } from "@/context/command"
@@ -22,7 +22,7 @@ import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
 import type { Session } from "@opencode-ai/sdk/v2/client"
 import { same } from "@/utils/same"
 
-export function SessionHeader() {
+export const ToolbarSession: Component<ComponentProps<"header">> = ({ class: className, ...props }) => {
   const globalSDK = useGlobalSDK()
   const layout = useLayout()
   const params = useParams()
@@ -56,7 +56,7 @@ export function SessionHeader() {
   }
 
   return (
-    <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex">
+    <header class={`flex absolute inset-0 ${className}`} {...props}>
       <button
         type="button"
         class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
@@ -64,8 +64,8 @@ export function SessionHeader() {
       >
         <Icon name="menu" size="small" />
       </button>
-      <div class="px-4 flex items-center justify-between gap-4 w-full">
-        <div class="flex items-center gap-3 min-w-0">
+      <div class="flex items-center justify-between gap-4 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
+        <div class="flex items-center gap-2 min-w-0">
           <div class="flex items-center gap-2 min-w-0">
             <div class="hidden xl:flex items-center gap-2">
               <Select
@@ -137,124 +137,126 @@ export function SessionHeader() {
           </div>
           <Show when={currentSession() && !parentSession()}>
             <TooltipKeybind class="hidden xl:block" title="New session" keybind={command.keybind("session.new")}>
-              <IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
+              <IconButton as={A} href={`/${params.dir}/session`} icon="plus-small" variant="ghost" />
             </TooltipKeybind>
           </Show>
         </div>
-        <div class="flex items-center gap-3">
-          <div class="hidden md:flex items-center gap-1">
-            <Button
-              size="small"
-              variant="ghost"
-              onClick={() => {
-                dialog.show(() => <DialogSelectServer />)
+      </div>
+
+      <div class="flex items-center gap-3 absolute right-6 top-1/2 -translate-y-1/2">
+        <div class="hidden md:flex items-center gap-1">
+          <Button
+            size="small"
+            variant="ghost"
+            class="flex gap-2 items-center justify-center"
+            onClick={() => {
+              dialog.show(() => <DialogSelectServer />)
+            }}
+          >
+            <div
+              classList={{
+                "size-1.5 rounded-full": true,
+                "bg-icon-success-base": server.healthy() === true,
+                "bg-icon-critical-base": server.healthy() === false,
+                "bg-border-weak-base": server.healthy() === undefined,
               }}
-            >
-              <div
-                classList={{
-                  "size-1.5 rounded-full": true,
-                  "bg-icon-success-base": server.healthy() === true,
-                  "bg-icon-critical-base": server.healthy() === false,
-                  "bg-border-weak-base": server.healthy() === undefined,
-                }}
-              />
-              <Icon name="server" size="small" class="text-icon-weak" />
-              <span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span>
-            </Button>
-            <SessionLspIndicator />
-            <SessionMcpIndicator />
-          </div>
-          <div class="flex items-center gap-1">
-            <Show when={currentSession()?.summary?.files}>
-              <TooltipKeybind
-                class="hidden md:block shrink-0"
-                title="Toggle review"
-                keybind={command.keybind("review.toggle")}
-              >
-                <Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
-                  <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
-                    <Icon
-                      name={layout.review.opened() ? "layout-right" : "layout-left"}
-                      size="small"
-                      class="group-hover/review-toggle:hidden"
-                    />
-                    <Icon
-                      name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
-                      size="small"
-                      class="hidden group-hover/review-toggle:inline-block"
-                    />
-                    <Icon
-                      name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
-                      size="small"
-                      class="hidden group-active/review-toggle:inline-block"
-                    />
-                  </div>
-                </Button>
-              </TooltipKeybind>
-            </Show>
+            />
+            <Icon name="server" size="small" class="text-icon-weak" />
+            <span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span>
+          </Button>
+          <SessionLspIndicator />
+          <SessionMcpIndicator />
+        </div>
+        <div class="flex items-center gap-1">
+          <Show when={currentSession()?.summary?.files}>
             <TooltipKeybind
               class="hidden md:block shrink-0"
-              title="Toggle terminal"
-              keybind={command.keybind("terminal.toggle")}
+              title="Toggle review"
+              keybind={command.keybind("review.toggle")}
             >
-              <Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
+              <Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
                 <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
                   <Icon
+                    name={layout.review.opened() ? "layout-right" : "layout-left"}
                     size="small"
-                    name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
-                    class="group-hover/terminal-toggle:hidden"
+                    class="group-hover/review-toggle:hidden"
                   />
                   <Icon
+                    name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
                     size="small"
-                    name="layout-bottom-partial"
-                    class="hidden group-hover/terminal-toggle:inline-block"
+                    class="hidden group-hover/review-toggle:inline-block"
                   />
                   <Icon
+                    name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
                     size="small"
-                    name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
-                    class="hidden group-active/terminal-toggle:inline-block"
+                    class="hidden group-active/review-toggle:inline-block"
                   />
                 </div>
               </Button>
             </TooltipKeybind>
-          </div>
-          <Show when={shareEnabled() && currentSession()}>
-            <Popover
-              title="Share session"
-              trigger={
-                <Tooltip class="shrink-0" value="Share session">
-                  <IconButton icon="share" variant="ghost" class="" />
-                </Tooltip>
-              }
-            >
-              {iife(() => {
-                const [url] = createResource(
-                  () => currentSession(),
-                  async (session) => {
-                    if (!session) return
-                    let shareURL = session.share?.url
-                    if (!shareURL) {
-                      shareURL = await globalSDK.client.session
-                        .share({ sessionID: session.id, directory: projectDirectory() })
-                        .then((r) => r.data?.share?.url)
-                        .catch((e) => {
-                          console.error("Failed to share session", e)
-                          return undefined
-                        })
-                    }
-                    return shareURL
-                  },
-                  { initialValue: "" },
-                )
-                return (
-                  <Show when={url.latest}>
-                    {(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
-                  </Show>
-                )
-              })}
-            </Popover>
           </Show>
+          <TooltipKeybind
+            class="hidden md:block shrink-0"
+            title="Toggle terminal"
+            keybind={command.keybind("terminal.toggle")}
+          >
+            <Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
+              <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
+                <Icon
+                  size="small"
+                  name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
+                  class="group-hover/terminal-toggle:hidden"
+                />
+                <Icon
+                  size="small"
+                  name="layout-bottom-partial"
+                  class="hidden group-hover/terminal-toggle:inline-block"
+                />
+                <Icon
+                  size="small"
+                  name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
+                  class="hidden group-active/terminal-toggle:inline-block"
+                />
+              </div>
+            </Button>
+          </TooltipKeybind>
         </div>
+        <Show when={shareEnabled() && currentSession()}>
+          <Popover
+            title="Share session"
+            trigger={
+              <Tooltip class="shrink-0" value="Share session">
+                <IconButton icon="share" variant="ghost" class="" />
+              </Tooltip>
+            }
+          >
+            {iife(() => {
+              const [url] = createResource(
+                () => currentSession(),
+                async (session) => {
+                  if (!session) return
+                  let shareURL = session.share?.url
+                  if (!shareURL) {
+                    shareURL = await globalSDK.client.session
+                      .share({ sessionID: session.id, directory: projectDirectory() })
+                      .then((r) => r.data?.share?.url)
+                      .catch((e) => {
+                        console.error("Failed to share session", e)
+                        return undefined
+                      })
+                  }
+                  return shareURL
+                },
+                { initialValue: "" },
+              )
+              return (
+                <Show when={url.latest}>
+                  {(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
+                </Show>
+              )
+            })}
+          </Popover>
+        </Show>
       </div>
     </header>
   )

+ 20 - 8
packages/app/src/pages/directory-layout.tsx

@@ -1,8 +1,10 @@
 import { createMemo, Show, type ParentProps } from "solid-js"
+import { Portal } from "solid-js/web"
 import { useNavigate, useParams } from "@solidjs/router"
 import { SDKProvider, useSDK } from "@/context/sdk"
 import { SyncProvider, useSync } from "@/context/sync"
 import { LocalProvider } from "@/context/local"
+import { ToolbarSession, TOOLBAR_PORTAL_ID } from "@/components/toolbar"
 
 import { base64Decode } from "@opencode-ai/util/encode"
 import { DataProvider } from "@opencode-ai/ui/context"
@@ -14,6 +16,7 @@ export default function Layout(props: ParentProps) {
   const directory = createMemo(() => {
     return base64Decode(params.dir!)
   })
+
   return (
     <Show when={params.dir} keyed>
       <SDKProvider directory={directory()}>
@@ -31,15 +34,24 @@ export default function Layout(props: ParentProps) {
               navigate(`/${params.dir}/session/${sessionID}`)
             }
 
+            const portalMount = document.getElementById(TOOLBAR_PORTAL_ID)
+
             return (
-              <DataProvider
-                data={sync.data}
-                directory={directory()}
-                onPermissionRespond={respond}
-                onNavigateToSession={navigateToSession}
-              >
-                <LocalProvider>{props.children}</LocalProvider>
-              </DataProvider>
+              <>
+                <Show when={portalMount}>
+                  <Portal mount={portalMount!}>
+                    <ToolbarSession />
+                  </Portal>
+                </Show>
+                <DataProvider
+                  data={sync.data}
+                  directory={directory()}
+                  onPermissionRespond={respond}
+                  onNavigateToSession={navigateToSession}
+                >
+                  <LocalProvider>{props.children}</LocalProvider>
+                </DataProvider>
+              </>
             )
           })}
         </SyncProvider>

+ 147 - 195
packages/app/src/pages/layout.tsx

@@ -6,32 +6,31 @@ import {
   Match,
   onCleanup,
   onMount,
-  ParentProps,
   Show,
   Switch,
   untrack,
   type JSX,
+  type ParentProps,
 } from "solid-js"
+import { createStore, produce } from "solid-js/store"
 import { DateTime } from "luxon"
 import { A, useNavigate, useParams } from "@solidjs/router"
-import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
-import { useGlobalSync } from "@/context/global-sync"
-import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
-import { Avatar } from "@opencode-ai/ui/avatar"
-import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
+import type { Session } from "@opencode-ai/sdk/v2/client"
 import { Button } from "@opencode-ai/ui/button"
-import { Icon } from "@opencode-ai/ui/icon"
-import { IconButton } from "@opencode-ai/ui/icon-button"
-import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
 import { Collapsible } from "@opencode-ai/ui/collapsible"
 import { DiffChanges } from "@opencode-ai/ui/diff-changes"
-import { Spinner } from "@opencode-ai/ui/spinner"
+import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
+import { Icon } from "@opencode-ai/ui/icon"
+import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Mark } from "@opencode-ai/ui/logo"
+import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
+import { Spinner } from "@opencode-ai/ui/spinner"
+import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
+import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
 import { getFilename } from "@opencode-ai/util/path"
-import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
-import { Session } from "@opencode-ai/sdk/v2/client"
+import { getAvatarColors, useLayout, type LocalProject } from "@/context/layout"
+import { useGlobalSync } from "@/context/global-sync"
 import { usePlatform } from "@/context/platform"
-import { createStore, produce } from "solid-js/store"
 import {
   DragDropProvider,
   DragDropSensors,
@@ -57,6 +56,8 @@ import { useCommand, type CommandOption } from "@/context/command"
 import { ConstrainDragXAxis } from "@/utils/solid-dnd"
 import { DialogSelectDirectory } from "@/components/dialog-select-directory"
 import { useServer } from "@/context/server"
+import { Toolbar } from "@/components/toolbar"
+import { ProjectIcon } from "@/components/project-icon"
 
 export default function Layout(props: ParentProps) {
   const [store, setStore] = createStore({
@@ -575,14 +576,15 @@ export default function Layout(props: ParentProps) {
     const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
     const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
     const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
-    const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
 
     return (
       <div class="relative size-5 shrink-0 rounded-sm">
-        <Avatar
-          fallback={name()}
-          src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url}
-          {...getAvatarColors(props.project.icon?.color)}
+        <ProjectIcon
+          name={name()}
+          projectId={props.project.id}
+          iconUrl={props.project.icon?.url}
+          iconColor={props.project.icon?.color}
+          size="small"
           class={`size-full ${props.class ?? ""}`}
           style={
             notifications().length > 0 && props.notify ? { "-webkit-mask-image": mask, "mask-image": mask } : undefined
@@ -669,77 +671,75 @@ export default function Layout(props: ParentProps) {
       return status?.type === "busy" || status?.type === "retry"
     })
     return (
-      <>
-        <div
-          data-session-id={props.session.id}
-          class="group/session relative w-full rounded-md cursor-default transition-colors
-                 hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
-        >
-          <Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
-            <A
-              href={`${props.slug}/session/${props.session.id}`}
-              class="flex flex-col min-w-0 text-left w-full focus:outline-none pl-4 pr-2 py-1"
-            >
-              <div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
-                <span
-                  classList={{
-                    "text-14-regular text-text-strong overflow-hidden text-ellipsis truncate": true,
-                    "animate-pulse": isWorking(),
-                  }}
-                >
-                  {props.session.title}
-                </span>
-                <div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
-                  <Switch>
-                    <Match when={isWorking()}>
-                      <Spinner class="size-2.5 mr-0.5" />
-                    </Match>
-                    <Match when={hasPermissions()}>
-                      <div class="size-1.5 mr-1.5 rounded-full bg-surface-warning-strong" />
-                    </Match>
-                    <Match when={hasError()}>
-                      <div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
-                    </Match>
-                    <Match when={notifications().length > 0}>
-                      <div class="size-1.5 mr-1.5 rounded-full bg-text-interactive-base" />
-                    </Match>
-                    <Match when={true}>
-                      <span class="text-12-regular text-text-weak text-right whitespace-nowrap">
-                        {Math.abs(updated().diffNow().as("seconds")) < 60
-                          ? "Now"
-                          : updated()
-                              .toRelative({
-                                style: "short",
-                                unit: ["days", "hours", "minutes"],
-                              })
-                              ?.replace(" ago", "")
-                              ?.replace(/ days?/, "d")
-                              ?.replace(" min.", "m")
-                              ?.replace(" hr.", "h")}
-                      </span>
-                    </Match>
-                  </Switch>
-                </div>
+      <div
+        data-session-id={props.session.id}
+        class="group/session relative w-full rounded-md cursor-default transition-colors
+               hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
+      >
+        <Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
+          <A
+            href={`${props.slug}/session/${props.session.id}`}
+            class="flex flex-col min-w-0 text-left w-full focus:outline-none pl-4 pr-2 py-1"
+          >
+            <div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
+              <span
+                classList={{
+                  "text-14-regular text-text-strong overflow-hidden text-ellipsis truncate": true,
+                  "animate-pulse": isWorking(),
+                }}
+              >
+                {props.session.title}
+              </span>
+              <div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
+                <Switch>
+                  <Match when={isWorking()}>
+                    <Spinner class="size-2.5 mr-0.5" />
+                  </Match>
+                  <Match when={hasPermissions()}>
+                    <div class="size-1.5 mr-1.5 rounded-full bg-surface-warning-strong" />
+                  </Match>
+                  <Match when={hasError()}>
+                    <div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
+                  </Match>
+                  <Match when={notifications().length > 0}>
+                    <div class="size-1.5 mr-1.5 rounded-full bg-text-interactive-base" />
+                  </Match>
+                  <Match when={true}>
+                    <span class="text-12-regular text-text-weak text-right whitespace-nowrap">
+                      {Math.abs(updated().diffNow().as("seconds")) < 60
+                        ? "Now"
+                        : updated()
+                            .toRelative({
+                              style: "short",
+                              unit: ["days", "hours", "minutes"],
+                            })
+                            ?.replace(" ago", "")
+                            ?.replace(/ days?/, "d")
+                            ?.replace(" min.", "m")
+                            ?.replace(" hr.", "h")}
+                    </span>
+                  </Match>
+                </Switch>
               </div>
-              <Show when={props.session.summary?.files}>
-                <div class="flex justify-between items-center self-stretch">
-                  <span class="text-12-regular text-text-weak">{`${props.session.summary?.files || "No"} file${props.session.summary?.files !== 1 ? "s" : ""} changed`}</span>
-                  <Show when={props.session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
-                </div>
-              </Show>
-            </A>
-          </Tooltip>
-          <div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
-            <TooltipKeybind
-              placement={props.mobile ? "bottom" : "right"}
-              title="Archive session"
-              keybind={command.keybind("session.archive")}
-            >
-              <IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
-            </TooltipKeybind>
-          </div>
+            </div>
+            <Show when={props.session.summary?.files}>
+              <div class="flex justify-between items-center self-stretch">
+                <span class="text-12-regular text-text-weak">{`${props.session.summary?.files || "No"} file${props.session.summary?.files !== 1 ? "s" : ""} changed`}</span>
+                <Show when={props.session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
+              </div>
+            </Show>
+          </A>
+        </Tooltip>
+        <div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
+          <TooltipKeybind
+            placement={props.mobile ? "bottom" : "right"}
+            title="Archive session"
+            keybind={command.keybind("session.archive")}
+          >
+            <IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
+          </TooltipKeybind>
         </div>
-      </>
+      </div>
     )
   }
 
@@ -780,7 +780,7 @@ export default function Layout(props: ParentProps) {
       }
     }
     return (
-      // @ts-ignore
+      // @ts-expect-error - SolidJS directive
       <div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
         <Switch>
           <Match when={showExpanded()}>
@@ -902,58 +902,7 @@ export default function Layout(props: ParentProps) {
     return (
       <div class="flex flex-col self-stretch h-full items-center justify-between overflow-hidden min-h-0">
         <div class="flex flex-col items-start self-stretch gap-4 min-h-0">
-          <Show when={!sidebarProps.mobile}>
-            <div
-              classList={{
-                "border-b border-border-weak-base w-full h-12 ml-px flex items-center pl-1.75 shrink-0": true,
-                "justify-start": expanded(),
-              }}
-            >
-              <A href="/" class="shrink-0 h-8 flex items-center justify-start px-2 w-full" data-tauri-drag-region>
-                <Mark class="shrink-0" />
-              </A>
-            </div>
-          </Show>
           <div class="flex flex-col items-start self-stretch gap-4 px-2 overflow-hidden min-h-0">
-            <Show when={!sidebarProps.mobile}>
-              <TooltipKeybind
-                class="shrink-0"
-                placement="right"
-                title="Toggle sidebar"
-                keybind={command.keybind("sidebar.toggle")}
-                inactive={expanded()}
-              >
-                <Button
-                  variant="ghost"
-                  size="large"
-                  class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
-                  onClick={layout.sidebar.toggle}
-                >
-                  <div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
-                    <Icon
-                      name={layout.sidebar.opened() ? "layout-left" : "layout-right"}
-                      size="small"
-                      class="group-hover/sidebar-toggle:hidden"
-                    />
-                    <Icon
-                      name={layout.sidebar.opened() ? "layout-left-partial" : "layout-right-partial"}
-                      size="small"
-                      class="hidden group-hover/sidebar-toggle:inline-block"
-                    />
-                    <Icon
-                      name={layout.sidebar.opened() ? "layout-left-full" : "layout-right-full"}
-                      size="small"
-                      class="hidden group-active/sidebar-toggle:inline-block"
-                    />
-                  </div>
-                  <Show when={layout.sidebar.opened()}>
-                    <div class="hidden group-hover/sidebar-toggle:block group-active/sidebar-toggle:block text-text-base">
-                      Toggle sidebar
-                    </div>
-                  </Show>
-                </Button>
-              </TooltipKeybind>
-            </Show>
             <DragDropProvider
               onDragStart={handleDragStart}
               onDragEnd={handleDragEnd}
@@ -1056,71 +1005,74 @@ export default function Layout(props: ParentProps) {
   }
 
   return (
-    <div class="relative flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
-      <div class="flex-1 min-h-0 flex">
-        <div
-          classList={{
-            "hidden xl:block": true,
-            "relative shrink-0": true,
-          }}
-          style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : "48px" }}
-        >
+    <>
+      <Toolbar />
+      <div class="relative flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
+        <div class="flex-1 min-h-0 flex">
           <div
             classList={{
-              "@container w-full h-full pb-5 bg-background-base": true,
-              "flex flex-col gap-5.5 items-start self-stretch justify-between": true,
-              "border-r border-border-weak-base contain-strict": true,
+              "hidden xl:block": true,
+              "relative shrink-0": true,
             }}
+            style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : "48px" }}
           >
-            <SidebarContent />
+            <div
+              classList={{
+                "@container w-full h-full py-3 bg-background-base": true,
+                "flex flex-col gap-5.5 items-start self-stretch justify-between": true,
+                "border-r border-border-weak-base contain-strict": true,
+              }}
+            >
+              <SidebarContent />
+            </div>
+            <Show when={layout.sidebar.opened()}>
+              <ResizeHandle
+                direction="horizontal"
+                size={layout.sidebar.width()}
+                min={150}
+                max={window.innerWidth * 0.3}
+                collapseThreshold={80}
+                onResize={layout.sidebar.resize}
+                onCollapse={layout.sidebar.close}
+              />
+            </Show>
           </div>
-          <Show when={layout.sidebar.opened()}>
-            <ResizeHandle
-              direction="horizontal"
-              size={layout.sidebar.width()}
-              min={150}
-              max={window.innerWidth * 0.3}
-              collapseThreshold={80}
-              onResize={layout.sidebar.resize}
-              onCollapse={layout.sidebar.close}
+          <div class="xl:hidden">
+            <div
+              classList={{
+                "fixed inset-0 bg-black/50 z-40 transition-opacity duration-200": true,
+                "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
+                "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
+              }}
+              onPointerDown={(e) => {
+                if (e.target === e.currentTarget) layout.mobileSidebar.hide()
+              }}
             />
-          </Show>
-        </div>
-        <div class="xl:hidden">
-          <div
-            classList={{
-              "fixed inset-0 bg-black/50 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()
-            }}
-          />
-          <div
-            classList={{
-              "@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base border-r border-border-weak-base flex flex-col gap-5.5 items-start self-stretch justify-between pb-5 transition-transform duration-200 ease-out": true,
-              "translate-x-0": layout.mobileSidebar.opened(),
-              "-translate-x-full": !layout.mobileSidebar.opened(),
-            }}
-            onClick={(e) => e.stopPropagation()}
-          >
-            <div class="border-b border-border-weak-base w-full h-12 ml-px flex items-center pl-1.75 shrink-0">
-              <A
-                href="/"
-                class="shrink-0 h-8 flex items-center justify-start px-2 w-full"
-                onClick={() => layout.mobileSidebar.hide()}
-              >
-                <Mark class="shrink-0" />
-              </A>
+            <div
+              classList={{
+                "@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base border-r border-border-weak-base flex flex-col gap-5.5 items-start self-stretch justify-between pb-5 transition-transform duration-200 ease-out": true,
+                "translate-x-0": layout.mobileSidebar.opened(),
+                "-translate-x-full": !layout.mobileSidebar.opened(),
+              }}
+              onPointerDown={(e) => e.stopPropagation()}
+            >
+              <div class="border-b border-border-weak-base w-full h-12 ml-px flex items-center pl-1.75 shrink-0">
+                <A
+                  href="/"
+                  class="shrink-0 h-8 flex items-center justify-start px-2 w-full"
+                  onClick={() => layout.mobileSidebar.hide()}
+                >
+                  <Mark class="shrink-0" />
+                </A>
+              </div>
+              <SidebarContent mobile />
             </div>
-            <SidebarContent mobile />
           </div>
-        </div>
 
-        <main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
+          <main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
+        </div>
+        <Toast.Region />
       </div>
-      <Toast.Region />
-    </div>
+    </>
   )
 }

+ 12 - 16
packages/desktop/src/index.tsx

@@ -1,24 +1,23 @@
 // @refresh reload
-import { render } from "solid-js/web"
-import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app"
-import { open, save } from "@tauri-apps/plugin-dialog"
-import { open as shellOpen } from "@tauri-apps/plugin-shell"
-import { type as ostype } from "@tauri-apps/plugin-os"
-import { check, Update } from "@tauri-apps/plugin-updater"
+import { AppBaseProviders, AppInterface, Platform, PlatformProvider } from "@opencode-ai/app"
+import { Logo } from "@opencode-ai/ui/logo"
+import { AsyncStorage } from "@solid-primitives/storage"
 import { invoke } from "@tauri-apps/api/core"
 import { getCurrentWindow } from "@tauri-apps/api/window"
+import { open, save } from "@tauri-apps/plugin-dialog"
+import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
 import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
+import { type as ostype } from "@tauri-apps/plugin-os"
 import { relaunch } from "@tauri-apps/plugin-process"
-import { AsyncStorage } from "@solid-primitives/storage"
-import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
+import { open as shellOpen } from "@tauri-apps/plugin-shell"
 import { Store } from "@tauri-apps/plugin-store"
-import { Logo } from "@opencode-ai/ui/logo"
-import { Suspense, createResource, ParentProps } from "solid-js"
+import { check, Update } from "@tauri-apps/plugin-updater"
+import { createResource, ParentProps, Show } from "solid-js"
+import { render } from "solid-js/web"
 
-import { UPDATER_ENABLED } from "./updater"
-import { createMenu } from "./menu"
 import pkg from "../package.json"
-import { Show } from "solid-js"
+import { createMenu } from "./menu"
+import { UPDATER_ENABLED } from "./updater"
 
 const root = document.getElementById("root")
 if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
@@ -269,9 +268,6 @@ root?.addEventListener("mousewheel", (e) => {
 render(() => {
   return (
     <PlatformProvider value={platform}>
-      {ostype() === "macos" && (
-        <div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
-      )}
       <AppBaseProviders>
         <ServerGate>
           <AppInterface />