Adam пре 2 месеци
родитељ
комит
679270d9e0

+ 0 - 1
packages/app/index.html

@@ -13,7 +13,6 @@
     <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
     <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
     <meta property="og:image" content="/social-share.png" />
     <meta property="og:image" content="/social-share.png" />
     <meta property="twitter:image" content="/social-share.png" />
     <meta property="twitter:image" content="/social-share.png" />
-    <!-- Theme preload script - applies cached theme to avoid FOUC -->
     <script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
     <script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
   </head>
   </head>
   <body class="antialiased overscroll-none text-12-regular overflow-hidden">
   <body class="antialiased overscroll-none text-12-regular overflow-hidden">

+ 27 - 74
packages/app/src/components/session/session-header.tsx

@@ -34,6 +34,17 @@ export function SessionHeader() {
   const sync = useSync()
   const sync = useSync()
 
 
   const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
   const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
+  const project = createMemo(() => {
+    const directory = projectDirectory()
+    if (!directory) return
+    return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
+  })
+  const name = createMemo(() => {
+    const current = project()
+    if (current) return current.name || getFilename(current.worktree)
+    return getFilename(projectDirectory())
+  })
+  const hotkey = createMemo(() => command.keybind("file.open"))
 
 
   const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
   const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
   const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
   const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
@@ -58,87 +69,29 @@ export function SessionHeader() {
     navigate(`/${params.dir}/session/${session.id}`)
     navigate(`/${params.dir}/session/${session.id}`)
   }
   }
 
 
-  const leftMount = createMemo(() => document.getElementById("opencode-titlebar-left"))
+  const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
   const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
   const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
 
 
   return (
   return (
     <>
     <>
-      <Show when={leftMount()}>
+      <Show when={centerMount()}>
         {(mount) => (
         {(mount) => (
           <Portal mount={mount()}>
           <Portal mount={mount()}>
-            <div class="flex items-center gap-3 min-w-0">
-              <div class="flex items-center gap-2 min-w-0">
-                <div class="hidden xl:flex items-center gap-2">
-                  <Select
-                    options={worktrees()}
-                    current={sync.project?.worktree ?? projectDirectory()}
-                    label={(x) => getFilename(x)}
-                    onSelect={(x) => (x ? navigateToProject(x) : undefined)}
-                    class="text-14-regular text-text-base"
-                    variant="ghost"
-                  >
-                    {/* @ts-ignore */}
-                    {(i) => (
-                      <div class="flex items-center gap-2">
-                        <Icon name="folder" size="small" />
-                        <div class="text-text-strong">{getFilename(i)}</div>
-                      </div>
-                    )}
-                  </Select>
-                  <div class="text-text-weaker">/</div>
-                </div>
-                <Show
-                  when={parentSession()}
-                  fallback={
-                    <>
-                      <Select
-                        options={sessions()}
-                        current={currentSession()}
-                        placeholder="New session"
-                        label={(x) => x.title}
-                        value={(x) => x.id}
-                        onSelect={navigateToSession}
-                        class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
-                        variant="ghost"
-                      />
-                    </>
-                  }
-                >
-                  <div class="flex items-center gap-2 min-w-0">
-                    <Select
-                      options={sessions()}
-                      current={parentSession()}
-                      placeholder="Back to parent session"
-                      label={(x) => x.title}
-                      value={(x) => x.id}
-                      onSelect={(session) => {
-                        const currentParent = parentSession()
-                        if (session && currentParent && session.id !== currentParent.id) {
-                          navigateToSession(session)
-                        }
-                      }}
-                      class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
-                      variant="ghost"
-                    />
-                    <div class="text-text-weaker">/</div>
-                    <Tooltip value="Back to parent session">
-                      <button
-                        type="button"
-                        class="flex items-center justify-center gap-1 p-1 rounded hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors flex-shrink-0"
-                        onClick={() => navigateToSession(parentSession())}
-                      >
-                        <Icon name="arrow-left" size="small" class="text-icon-base" />
-                      </button>
-                    </Tooltip>
-                  </div>
-                </Show>
-              </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" />
-                </TooltipKeybind>
+            <button
+              type="button"
+              class="hidden md:flex w-[320px] h-7 px-1.5 items-center gap-2 rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
+              onClick={() => command.trigger("file.open")}
+            >
+              <Icon name="magnifying-glass" size="small" class="text-text-weak" />
+              <span class="flex-1 min-w-0 text-14-regular text-text-weak truncate">Search {name()}</span>
+              <Show when={hotkey()}>
+                {(keybind) => (
+                  <span class="shrink-0 flex items-center justify-center h-5 px-2 rounded-md border border-border-weak-base bg-surface-base text-12-medium text-text-weak">
+                    {keybind()}
+                  </span>
+                )}
               </Show>
               </Show>
-            </div>
+            </button>
           </Portal>
           </Portal>
         )}
         )}
       </Show>
       </Show>

+ 115 - 0
packages/app/src/components/titlebar.tsx

@@ -0,0 +1,115 @@
+import { createEffect, createMemo, Show } from "solid-js"
+import { IconButton } from "@opencode-ai/ui/icon-button"
+import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
+import { useTheme } from "@opencode-ai/ui/theme"
+
+import { useLayout } from "@/context/layout"
+import { usePlatform } from "@/context/platform"
+import { useCommand } from "@/context/command"
+
+export function Titlebar() {
+  const layout = useLayout()
+  const platform = usePlatform()
+  const command = useCommand()
+  const theme = useTheme()
+
+  const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
+  const reserve = createMemo(
+    () => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"),
+  )
+
+  const getWin = () => {
+    if (platform.platform !== "desktop") return
+
+    const tauri = (
+      window as unknown as {
+        __TAURI__?: { window?: { getCurrentWindow?: () => { startDragging?: () => Promise<void> } } }
+      }
+    ).__TAURI__
+    if (!tauri?.window?.getCurrentWindow) return
+
+    return tauri.window.getCurrentWindow()
+  }
+
+  createEffect(() => {
+    if (platform.platform !== "desktop") return
+
+    const scheme = theme.colorScheme()
+    const value = scheme === "system" ? null : scheme
+
+    const tauri = (window as unknown as { __TAURI__?: { webviewWindow?: { getCurrentWebviewWindow?: () => unknown } } })
+      .__TAURI__
+    const get = tauri?.webviewWindow?.getCurrentWebviewWindow
+    if (!get) return
+
+    const win = get() as { setTheme?: (theme?: "light" | "dark" | null) => Promise<void> }
+    if (!win.setTheme) return
+
+    void win.setTheme(value).catch(() => undefined)
+  })
+
+  const interactive = (target: EventTarget | null) => {
+    if (!(target instanceof Element)) return false
+
+    const selector =
+      "button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']"
+
+    return !!target.closest(selector)
+  }
+
+  const drag = (e: MouseEvent) => {
+    if (platform.platform !== "desktop") return
+    if (e.buttons !== 1) return
+    if (interactive(e.target)) return
+
+    const win = getWin()
+    if (!win?.startDragging) return
+
+    e.preventDefault()
+    void win.startDragging().catch(() => undefined)
+  }
+
+  return (
+    <header class="h-10 shrink-0 bg-background-base flex items-center relative">
+      <div
+        classList={{
+          "flex items-center w-full min-w-0 pr-2": true,
+          "pl-2": !mac(),
+        }}
+        onMouseDown={drag}
+      >
+        <Show when={mac()}>
+          <div class="w-[72px] h-full shrink-0" data-tauri-drag-region />
+        </Show>
+        <IconButton
+          icon="menu"
+          variant="ghost"
+          class="xl:hidden size-8 rounded-md"
+          onClick={layout.mobileSidebar.toggle}
+        />
+        <TooltipKeybind
+          class="hidden xl:flex shrink-0"
+          placement="bottom"
+          title="Toggle sidebar"
+          keybind={command.keybind("sidebar.toggle")}
+        >
+          <IconButton
+            icon={layout.sidebar.opened() ? "layout-left" : "layout-right"}
+            variant="ghost"
+            class="size-8 rounded-md"
+            onClick={layout.sidebar.toggle}
+          />
+        </TooltipKeybind>
+        <div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
+        <div class="flex-1 h-full" data-tauri-drag-region />
+        <div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0" />
+        <Show when={reserve()}>
+          <div class="w-[120px] h-full shrink-0" data-tauri-drag-region />
+        </Show>
+      </div>
+      <div class="absolute inset-0 flex items-center justify-center pointer-events-none">
+        <div id="opencode-titlebar-center" class="pointer-events-auto" />
+      </div>
+    </header>
+  )
+}

+ 8 - 0
packages/app/src/context/layout.tsx

@@ -53,6 +53,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         sidebar: {
         sidebar: {
           opened: false,
           opened: false,
           width: 280,
           width: 280,
+          workspaces: false,
         },
         },
         terminal: {
         terminal: {
           height: 280,
           height: 280,
@@ -304,6 +305,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         resize(width: number) {
         resize(width: number) {
           setStore("sidebar", "width", width)
           setStore("sidebar", "width", width)
         },
         },
+        workspaces: createMemo(() => store.sidebar.workspaces ?? false),
+        setWorkspaces(value: boolean) {
+          setStore("sidebar", "workspaces", value)
+        },
+        toggleWorkspaces() {
+          setStore("sidebar", "workspaces", (x) => !x)
+        },
       },
       },
       terminal: {
       terminal: {
         height: createMemo(() => store.terminal.height),
         height: createMemo(() => store.terminal.height),

+ 1 - 1
packages/app/src/context/sync.tsx

@@ -14,7 +14,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
     const sdk = useSDK()
     const sdk = useSDK()
     const [store, setStore] = globalSync.child(sdk.directory)
     const [store, setStore] = globalSync.child(sdk.directory)
     const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
     const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
-    const chunk = 200
+    const chunk = 400
     const inflight = new Map<string, Promise<void>>()
     const inflight = new Map<string, Promise<void>>()
     const inflightDiff = new Map<string, Promise<void>>()
     const inflightDiff = new Map<string, Promise<void>>()
     const inflightTodo = new Map<string, Promise<void>>()
     const inflightTodo = new Map<string, Promise<void>>()

+ 4 - 0
packages/app/src/index.css

@@ -5,3 +5,7 @@
     cursor: default;
     cursor: default;
   }
   }
 }
 }
+
+*[data-tauri-drag-region] {
+  app-region: drag;
+}

+ 351 - 172
packages/app/src/pages/layout.tsx

@@ -23,6 +23,8 @@ import { Button } from "@opencode-ai/ui/button"
 import { Icon } from "@opencode-ai/ui/icon"
 import { Icon } from "@opencode-ai/ui/icon"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
 import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
+import { HoverCard } from "@opencode-ai/ui/hover-card"
+import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
 import { Collapsible } from "@opencode-ai/ui/collapsible"
 import { Collapsible } from "@opencode-ai/ui/collapsible"
 import { DiffChanges } from "@opencode-ai/ui/diff-changes"
 import { DiffChanges } from "@opencode-ai/ui/diff-changes"
 import { Spinner } from "@opencode-ai/ui/spinner"
 import { Spinner } from "@opencode-ai/ui/spinner"
@@ -55,6 +57,8 @@ import { useCommand, type CommandOption } from "@/context/command"
 import { ConstrainDragXAxis } from "@/utils/solid-dnd"
 import { ConstrainDragXAxis } from "@/utils/solid-dnd"
 import { navStart } from "@/utils/perf"
 import { navStart } from "@/utils/perf"
 import { DialogSelectDirectory } from "@/components/dialog-select-directory"
 import { DialogSelectDirectory } from "@/components/dialog-select-directory"
+import { DialogEditProject } from "@/components/dialog-edit-project"
+import { Titlebar } from "@/components/titlebar"
 import { useServer } from "@/context/server"
 import { useServer } from "@/context/server"
 
 
 export default function Layout(props: ParentProps) {
 export default function Layout(props: ParentProps) {
@@ -814,20 +818,24 @@ export default function Layout(props: ParentProps) {
     const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
     const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
 
 
     return (
     return (
-      <div class={`relative size-10 shrink-0 ${props.class ?? ""}`}>
-        <Avatar
-          fallback={name()}
-          src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url}
-          {...getAvatarColors(props.project.icon?.color)}
-          class="size-full rounded-lg"
-          style={
-            notifications().length > 0 && props.notify ? { "-webkit-mask-image": mask, "mask-image": mask } : undefined
-          }
-        />
+      <div class={`relative size-8 shrink-0 rounded-sm ${props.class ?? ""}`}>
+        <div class="size-full rounded-sm overflow-clip">
+          <Avatar
+            fallback={name()}
+            src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url}
+            {...getAvatarColors(props.project.icon?.color)}
+            class="size-full rounded-sm"
+            style={
+              notifications().length > 0 && props.notify
+                ? { "-webkit-mask-image": mask, "mask-image": mask }
+                : undefined
+            }
+          />
+        </div>
         <Show when={notifications().length > 0 && props.notify}>
         <Show when={notifications().length > 0 && props.notify}>
           <div
           <div
             classList={{
             classList={{
-              "absolute -top-0.5 -right-0.5 size-2 rounded-full": true,
+              "absolute -top-px -right-px size-2 rounded-full z-10": true,
               "bg-icon-critical-base": hasError(),
               "bg-icon-critical-base": hasError(),
               "bg-text-interactive-base": !hasError(),
               "bg-text-interactive-base": !hasError(),
             }}
             }}
@@ -837,7 +845,7 @@ export default function Layout(props: ParentProps) {
     )
     )
   }
   }
 
 
-  const SessionItem = (props: { session: Session; slug: string; mobile?: boolean }): JSX.Element => {
+  const SessionItem = (props: { session: Session; slug: string; mobile?: boolean; dense?: boolean }): JSX.Element => {
     const notification = useNotification()
     const notification = useNotification()
     const notifications = createMemo(() => notification.session.unseen(props.session.id))
     const notifications = createMemo(() => notification.session.unseen(props.session.id))
     const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
     const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
@@ -859,47 +867,62 @@ export default function Layout(props: ParentProps) {
       return status?.type === "busy" || status?.type === "retry"
       return status?.type === "busy" || status?.type === "retry"
     })
     })
 
 
+    const tint = createMemo(() => {
+      const messages = sessionStore.message[props.session.id]
+      if (!messages) return undefined
+      const user = messages
+        .slice()
+        .reverse()
+        .find((m) => m.role === "user")
+      if (!user?.agent) return undefined
+
+      const agent = sessionStore.agent.find((a) => a.name === user.agent)
+      return agent?.color
+    })
+
     return (
     return (
       <div
       <div
         data-session-id={props.session.id}
         data-session-id={props.session.id}
-        class="group/session relative w-full rounded-md cursor-default transition-colors
+        class="group/session relative w-full rounded-md cursor-default transition-colors px-3
                hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
                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}>
         <Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
           <A
           <A
             href={`${props.slug}/session/${props.session.id}`}
             href={`${props.slug}/session/${props.session.id}`}
-            class="flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none pl-4 pr-2 py-1 transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7"
+            class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
             onMouseEnter={() => prefetchSession(props.session, "high")}
             onMouseEnter={() => prefetchSession(props.session, "high")}
             onFocus={() => prefetchSession(props.session, "high")}
             onFocus={() => prefetchSession(props.session, "high")}
           >
           >
-            <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 flex items-center gap-2">
-              <Switch>
-                <Match when={isWorking()}>
-                  <Spinner class="size-2.5" />
-                </Match>
-                <Match when={hasPermissions()}>
-                  <div class="size-1.5 rounded-full bg-surface-warning-strong" />
-                </Match>
-                <Match when={hasError()}>
-                  <div class="size-1.5 rounded-full bg-text-diff-delete-base" />
-                </Match>
-                <Match when={notifications().length > 0}>
-                  <div class="size-1.5 rounded-full bg-text-interactive-base" />
-                </Match>
-              </Switch>
+            <div class="flex items-center gap-1 w-full">
+              <div
+                class="shrink-0 size-6 flex items-center justify-center"
+                style={{ color: tint() ?? "var(--icon-interactive-base)" }}
+              >
+                <Switch>
+                  <Match when={isWorking()}>
+                    <Spinner class="size-[15px]" />
+                  </Match>
+                  <Match when={hasPermissions()}>
+                    <div class="size-1.5 rounded-full bg-surface-warning-strong" />
+                  </Match>
+                  <Match when={hasError()}>
+                    <div class="size-1.5 rounded-full bg-text-diff-delete-base" />
+                  </Match>
+                  <Match when={notifications().length > 0}>
+                    <div class="size-1.5 rounded-full bg-text-interactive-base" />
+                  </Match>
+                </Switch>
+              </div>
+              <span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
+                {props.session.title}
+              </span>
               <Show when={props.session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
               <Show when={props.session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
             </div>
             </div>
           </A>
           </A>
         </Tooltip>
         </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">
+        <div
+          class={`hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"}`}
+        >
           <TooltipKeybind
           <TooltipKeybind
             placement={props.mobile ? "bottom" : "right"}
             placement={props.mobile ? "bottom" : "right"}
             title="Archive session"
             title="Archive session"
@@ -914,26 +937,81 @@ export default function Layout(props: ParentProps) {
 
 
   const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
   const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
     const sortable = createSortable(props.project.worktree)
     const sortable = createSortable(props.project.worktree)
-    const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
     const selected = createMemo(() => {
     const selected = createMemo(() => {
       const current = params.dir ? base64Decode(params.dir) : ""
       const current = params.dir ? base64Decode(params.dir) : ""
       return props.project.worktree === current || props.project.sandboxes?.includes(current)
       return props.project.worktree === current || props.project.sandboxes?.includes(current)
     })
     })
 
 
+    const workspaces = createMemo(() => workspaceIds(props.project).slice(0, 2))
+    const label = (directory: string) => {
+      const [data] = globalSync.child(directory)
+      const kind = directory === props.project.worktree ? "local" : "sandbox"
+      const name = data.vcs?.branch ?? getFilename(directory)
+      return `${kind} : ${name}`
+    }
+
+    const sessions = (directory: string) => {
+      const [data] = globalSync.child(directory)
+      return data.session
+        .filter((session) => session.directory === data.path.directory)
+        .filter((session) => !session.parentID)
+        .toSorted(sortSessions)
+        .slice(0, 2)
+    }
+
+    const trigger = (
+      <button
+        type="button"
+        classList={{
+          "flex items-center justify-center size-10 p-1 rounded-md border transition-colors cursor-default": true,
+          "bg-surface-base-hover border-icon-strong-base": selected(),
+          "bg-transparent border-transparent hover:bg-surface-base-hover hover:border-border-weak-base": !selected(),
+        }}
+        onClick={() => navigateToProject(props.project.worktree)}
+      >
+        <ProjectIcon project={props.project} notify />
+      </button>
+    )
+
     return (
     return (
       // @ts-ignore
       // @ts-ignore
       <div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
       <div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
-        <Tooltip placement={props.mobile ? "bottom" : "right"} value={name()}>
-          <Button
-            variant="ghost"
-            size="large"
-            class="flex items-center justify-center p-0 size-12 rounded-xl"
-            data-selected={selected()}
-            onClick={() => navigateToProject(props.project.worktree)}
-          >
-            <ProjectIcon project={props.project} notify />
-          </Button>
-        </Tooltip>
+        <HoverCard openDelay={0} closeDelay={0} placement="right-start" gutter={10} trigger={trigger}>
+          <div class="-m-3 flex flex-col w-72">
+            <div class="px-3 py-2 text-12-medium text-text-strong">Recent sessions</div>
+            <div class="px-2 pb-2 flex flex-col gap-2">
+              <For each={workspaces()}>
+                {(directory) => (
+                  <div class="flex flex-col gap-1">
+                    <div class="px-2 py-0.5 flex items-center gap-1 min-w-0">
+                      <div class="shrink-0 size-6 flex items-center justify-center">
+                        <Icon name="branch" size="small" class="text-icon-base" />
+                      </div>
+                      <span class="truncate text-14-medium text-text-strong">{label(directory)}</span>
+                    </div>
+                    <For each={sessions(directory)}>
+                      {(session) => (
+                        <SessionItem session={session} slug={base64Encode(directory)} dense mobile={props.mobile} />
+                      )}
+                    </For>
+                  </div>
+                )}
+              </For>
+            </div>
+            <div class="px-2 py-2 border-t border-border-weak-base">
+              <Button
+                variant="ghost"
+                class="flex w-full text-left justify-start text-text-base px-2"
+                onClick={() => {
+                  layout.sidebar.open()
+                  navigateToProject(props.project.worktree)
+                }}
+              >
+                View all sessions
+              </Button>
+            </div>
+          </div>
+        </HoverCard>
       </div>
       </div>
     )
     )
   }
   }
@@ -967,7 +1045,7 @@ export default function Layout(props: ParentProps) {
     return (
     return (
       <Show when={label()}>
       <Show when={label()}>
         {(value) => (
         {(value) => (
-          <div class="bg-background-base rounded-md px-2 py-1 text-12-medium text-text-strong">{value()}</div>
+          <div class="bg-background-base rounded-md px-2 py-1 text-14-medium text-text-strong">{value()}</div>
         )}
         )}
       </Show>
       </Show>
     )
     )
@@ -1003,39 +1081,59 @@ export default function Layout(props: ParentProps) {
         <Collapsible
         <Collapsible
           variant="ghost"
           variant="ghost"
           open={open()}
           open={open()}
-          class="gap-1.5 shrink-0"
+          class="shrink-0"
           onOpenChange={(value) => setStore("workspaceExpanded", props.directory, value)}
           onOpenChange={(value) => setStore("workspaceExpanded", props.directory, value)}
         >
         >
-          <Collapsible.Trigger class="group/trigger flex items-center justify-between w-full px-2 py-1.5 rounded-md hover:bg-surface-raised-base-hover">
-            <div class="flex items-center gap-2 min-w-0">
-              <Icon
-                name="chevron-right"
-                size="small"
-                class="text-text-subtle transition-transform duration-50 group-data-[expanded]/trigger:rotate-90"
-              />
-              <span class="truncate text-12-medium text-text-strong">{title()}</span>
+          <div class="px-2 py-1">
+            <div class="group/trigger relative">
+              <Collapsible.Trigger class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md hover:bg-surface-raised-base-hover">
+                <div class="flex items-center gap-1 min-w-0">
+                  <div class="flex items-center justify-center shrink-0 size-6">
+                    <Icon name="branch" size="small" />
+                  </div>
+                  <span class="truncate text-14-medium text-text-strong">{title()}</span>
+                  <Icon
+                    name={open() ? "chevron-down" : "chevron-right"}
+                    size="small"
+                    class="shrink-0 text-icon-base opacity-0 transition-opacity group-hover/trigger:opacity-100 group-focus-within/trigger:opacity-100"
+                  />
+                </div>
+              </Collapsible.Trigger>
+              <div class="absolute right-1 top-1/2 -translate-y-1/2 hidden items-center gap-0.5 pointer-events-none group-hover/trigger:flex group-focus-within/trigger:flex">
+                <Tooltip class="pointer-events-auto" value="More options" placement="top">
+                  <IconButton icon="dot-grid" variant="ghost" class="size-6 rounded-md" />
+                </Tooltip>
+                <Tooltip class="pointer-events-auto" value="New session" placement="top">
+                  <IconButton
+                    icon="plus-small"
+                    variant="ghost"
+                    class="size-6 rounded-md"
+                    onClick={() => navigate(`/${slug()}/session`)}
+                  />
+                </Tooltip>
+              </div>
             </div>
             </div>
-          </Collapsible.Trigger>
+          </div>
           <Collapsible.Content>
           <Collapsible.Content>
-            <nav class="flex flex-col gap-1 pl-2">
-              <For each={sessions()}>
-                {(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />}
-              </For>
+            <nav class="flex flex-col gap-1 px-2">
               <Button
               <Button
                 as={A}
                 as={A}
                 href={`${slug()}/session`}
                 href={`${slug()}/session`}
                 variant="ghost"
                 variant="ghost"
                 size="large"
                 size="large"
-                icon="plus-small"
-                class="flex w-full text-left justify-start text-text-base rounded-md px-3"
+                icon="edit"
+                class="hidden _flex w-full text-left justify-start text-text-base rounded-md px-3"
               >
               >
                 New session
                 New session
               </Button>
               </Button>
+              <For each={sessions()}>
+                {(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />}
+              </For>
               <Show when={hasMore()}>
               <Show when={hasMore()}>
                 <div class="relative w-full py-1">
                 <div class="relative w-full py-1">
                   <Button
                   <Button
                     variant="ghost"
                     variant="ghost"
-                    class="flex w-full text-left justify-start text-12-medium opacity-50 px-3.5"
+                    class="flex w-full text-left justify-start text-12-medium text-text-weak px-10"
                     size="large"
                     size="large"
                     onClick={loadMore}
                     onClick={loadMore}
                   >
                   >
@@ -1050,9 +1148,53 @@ export default function Layout(props: ParentProps) {
     )
     )
   }
   }
 
 
+  const LocalWorkspace = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
+    const [workspaceStore, setWorkspaceStore] = globalSync.child(props.project.worktree)
+    const slug = createMemo(() => base64Encode(props.project.worktree))
+    const sessions = createMemo(() =>
+      workspaceStore.session
+        .filter((session) => session.directory === workspaceStore.path.directory)
+        .filter((session) => !session.parentID)
+        .toSorted(sortSessions),
+    )
+    const hasMore = createMemo(() => workspaceStore.session.length >= workspaceStore.limit)
+    const loadMore = async () => {
+      setWorkspaceStore("limit", (limit) => limit + 5)
+      await globalSync.project.loadSessions(props.project.worktree)
+    }
+
+    return (
+      <div
+        ref={(el) => {
+          if (!props.mobile) scrollContainerRef = el
+        }}
+        class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar"
+      >
+        <nav class="flex flex-col gap-1 px-2">
+          <For each={sessions()}>
+            {(session) => <SessionItem session={session} slug={slug()} mobile={props.mobile} />}
+          </For>
+          <Show when={hasMore()}>
+            <div class="relative w-full py-1">
+              <Button
+                variant="ghost"
+                class="flex w-full text-left justify-start text-12-medium text-text-weak px-10"
+                size="large"
+                onClick={loadMore}
+              >
+                Load more
+              </Button>
+            </div>
+          </Show>
+        </nav>
+      </div>
+    )
+  }
+
   const SidebarContent = (sidebarProps: { mobile?: boolean }) => {
   const SidebarContent = (sidebarProps: { mobile?: boolean }) => {
     const expanded = () => sidebarProps.mobile || layout.sidebar.opened()
     const expanded = () => sidebarProps.mobile || layout.sidebar.opened()
 
 
+    const sync = useGlobalSync()
     const project = createMemo(() => currentProject())
     const project = createMemo(() => currentProject())
     const projectName = createMemo(() => {
     const projectName = createMemo(() => {
       const current = project()
       const current = project()
@@ -1091,9 +1233,11 @@ export default function Layout(props: ParentProps) {
       navigate(`/${base64Encode(created.directory)}/session`)
       navigate(`/${base64Encode(created.directory)}/session`)
     }
     }
 
 
+    const homedir = createMemo(() => sync.data.path.home)
+
     return (
     return (
       <div class="flex h-full w-full overflow-hidden">
       <div class="flex h-full w-full overflow-hidden">
-        <div class="w-16 shrink-0 bg-background-base border-r border-border-weak-base flex flex-col items-center overflow-hidden">
+        <div class="w-16 shrink-0 bg-background-base flex flex-col items-center overflow-hidden">
           <div class="flex-1 min-h-0 w-full">
           <div class="flex-1 min-h-0 w-full">
             <DragDropProvider
             <DragDropProvider
               onDragStart={handleDragStart}
               onDragStart={handleDragStart}
@@ -1103,7 +1247,7 @@ export default function Layout(props: ParentProps) {
             >
             >
               <DragDropSensors />
               <DragDropSensors />
               <ConstrainDragXAxis />
               <ConstrainDragXAxis />
-              <div class="h-full w-full flex flex-col items-center gap-2 px-2 py-3 overflow-y-auto no-scrollbar">
+              <div class="h-full w-full flex flex-col items-center gap-3 px-3 py-2 overflow-y-auto no-scrollbar">
                 <SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
                 <SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
                   <For each={layout.projects.list()}>
                   <For each={layout.projects.list()}>
                     {(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
                     {(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
@@ -1120,7 +1264,7 @@ export default function Layout(props: ParentProps) {
                     </div>
                     </div>
                   }
                   }
                 >
                 >
-                  <IconButton icon="plus" variant="ghost" class="size-12 rounded-xl" onClick={chooseProject} />
+                  <IconButton icon="plus" variant="ghost" size="large" onClick={chooseProject} />
                 </Tooltip>
                 </Tooltip>
               </div>
               </div>
               <DragOverlay>
               <DragOverlay>
@@ -1128,12 +1272,25 @@ export default function Layout(props: ParentProps) {
               </DragOverlay>
               </DragOverlay>
             </DragDropProvider>
             </DragDropProvider>
           </div>
           </div>
+          <div class="shrink-0 w-full pt-3 pb-3 flex flex-col items-center gap-2">
+            <Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value="Settings">
+              <IconButton icon="settings-gear" variant="ghost" size="large" onClick={command.show} />
+            </Tooltip>
+            <Tooltip placement={sidebarProps.mobile ? "bottom" : "right"} value="Help">
+              <IconButton
+                icon="help"
+                variant="ghost"
+                size="large"
+                onClick={() => platform.openLink("https://opencode.ai/desktop-feedback")}
+              />
+            </Tooltip>
+          </div>
         </div>
         </div>
 
 
         <Show when={expanded()}>
         <Show when={expanded()}>
           <div
           <div
             classList={{
             classList={{
-              "flex flex-col min-h-0 bg-background-base border-r border-border-weak-base": true,
+              "flex flex-col min-h-0 bg-background-stronger border border-border-weak-base rounded-tl-sm": true,
               "flex-1 min-w-0": sidebarProps.mobile,
               "flex-1 min-w-0": sidebarProps.mobile,
             }}
             }}
             style={{ width: sidebarProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }}
             style={{ width: sidebarProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }}
@@ -1141,69 +1298,125 @@ export default function Layout(props: ParentProps) {
             <Show when={project()}>
             <Show when={project()}>
               {(p) => (
               {(p) => (
                 <>
                 <>
-                  <div class="shrink-0 h-12 flex items-center justify-between px-3 border-b border-border-weak-base">
-                    <div class="min-w-0 truncate text-14-medium text-text-strong">{projectName()}</div>
-                    <Button variant="ghost" size="large" icon="plus-small" onClick={createWorkspace}>
-                      New workspace
-                    </Button>
+                  <div class="shrink-0 px-2 py-1">
+                    <div class="flex items-start justify-between gap-2 p-2">
+                      <div class="flex flex-col min-w-0">
+                        <span class="text-16-medium text-text-strong truncate">{projectName()}</span>
+                        <Tooltip placement="right" value={project()?.worktree} class="shrink-0">
+                          <span class="text-12-regular text-text-base truncate">
+                            {project()?.worktree.replace(homedir(), "~")}
+                          </span>
+                        </Tooltip>
+                      </div>
+                      <DropdownMenu>
+                        <DropdownMenu.Trigger
+                          as={IconButton}
+                          icon="dot-grid"
+                          variant="ghost"
+                          class="shrink-0 size-6 rounded-md"
+                        />
+                        <DropdownMenu.Portal>
+                          <DropdownMenu.Content>
+                            <DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p()} />)}>
+                              <DropdownMenu.ItemLabel>Edit project</DropdownMenu.ItemLabel>
+                            </DropdownMenu.Item>
+                            <DropdownMenu.Item onSelect={() => closeProject(p().worktree)}>
+                              <DropdownMenu.ItemLabel>Close project</DropdownMenu.ItemLabel>
+                            </DropdownMenu.Item>
+                            <DropdownMenu.Separator />
+                            <DropdownMenu.Item onSelect={() => layout.sidebar.toggleWorkspaces()}>
+                              <DropdownMenu.ItemLabel>
+                                {layout.sidebar.workspaces() ? "Disable workspaces" : "Enable workspaces"}
+                              </DropdownMenu.ItemLabel>
+                            </DropdownMenu.Item>
+                          </DropdownMenu.Content>
+                        </DropdownMenu.Portal>
+                      </DropdownMenu>
+                    </div>
                   </div>
                   </div>
 
 
-                  <div class="flex-1 min-h-0">
-                    <DragDropProvider
-                      onDragStart={handleWorkspaceDragStart}
-                      onDragEnd={handleWorkspaceDragEnd}
-                      onDragOver={handleWorkspaceDragOver}
-                      collisionDetector={closestCenter}
-                    >
-                      <DragDropSensors />
-                      <ConstrainDragXAxis />
-                      <div
-                        ref={(el) => {
-                          if (!sidebarProps.mobile) scrollContainerRef = el
-                        }}
-                        class="h-full w-full flex flex-col gap-2 px-2 py-2 overflow-y-auto no-scrollbar"
-                      >
-                        <SortableProvider ids={workspaces()}>
-                          <For each={workspaces()}>
-                            {(directory) => (
-                              <SortableWorkspace directory={directory} project={p()} mobile={sidebarProps.mobile} />
-                            )}
-                          </For>
-                        </SortableProvider>
+                  <Show
+                    when={layout.sidebar.workspaces()}
+                    fallback={
+                      <>
+                        <div class="py-4 px-3">
+                          <Button
+                            size="large"
+                            icon="plus-small"
+                            class="w-full"
+                            onClick={() => {
+                              navigate(`/${base64Encode(p().worktree)}/session`)
+                              layout.mobileSidebar.hide()
+                            }}
+                          >
+                            New session
+                          </Button>
+                        </div>
+                        <div class="flex-1 min-h-0">
+                          <LocalWorkspace project={p()} mobile={sidebarProps.mobile} />
+                        </div>
+                      </>
+                    }
+                  >
+                    <>
+                      <div class="py-4 px-3">
+                        <Button size="large" icon="plus-small" class="w-full" onClick={createWorkspace}>
+                          New workspace
+                        </Button>
                       </div>
                       </div>
-                      <DragOverlay>
-                        <WorkspaceDragOverlay />
-                      </DragOverlay>
-                    </DragDropProvider>
-                  </div>
+                      <div class="flex-1 min-h-0">
+                        <DragDropProvider
+                          onDragStart={handleWorkspaceDragStart}
+                          onDragEnd={handleWorkspaceDragEnd}
+                          onDragOver={handleWorkspaceDragOver}
+                          collisionDetector={closestCenter}
+                        >
+                          <DragDropSensors />
+                          <ConstrainDragXAxis />
+                          <div
+                            ref={(el) => {
+                              if (!sidebarProps.mobile) scrollContainerRef = el
+                            }}
+                            class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar"
+                          >
+                            <SortableProvider ids={workspaces()}>
+                              <For each={workspaces()}>
+                                {(directory) => (
+                                  <SortableWorkspace directory={directory} project={p()} mobile={sidebarProps.mobile} />
+                                )}
+                              </For>
+                            </SortableProvider>
+                          </div>
+                          <DragOverlay>
+                            <WorkspaceDragOverlay />
+                          </DragOverlay>
+                        </DragDropProvider>
+                      </div>
+                    </>
+                  </Show>
                 </>
                 </>
               )}
               )}
             </Show>
             </Show>
             <Show when={!project()}>
             <Show when={!project()}>
-              <div class="p-3 text-12-regular text-text-weak">Open a project to see workspaces.</div>
+              <div class="p-3 text-12-regular text-text-weak">Open a project to see sessions.</div>
             </Show>
             </Show>
-            <Show when={providers.all().length > 0}>
-              <div class="shrink-0 px-2 py-3 border-t border-border-weak-base flex flex-col gap-1.5">
-                <Button
-                  class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
-                  variant="ghost"
-                  size="large"
-                  icon="plus"
-                  onClick={connectProvider}
-                >
-                  Connect provider
-                </Button>
-                <Button
-                  as={"a"}
-                  href="https://opencode.ai/desktop-feedback"
-                  target="_blank"
-                  class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
-                  variant="ghost"
-                  size="large"
-                  icon="bubble-5"
-                >
-                  Share feedback
-                </Button>
+            <Show when={providers.all().length > 0 && providers.paid().length === 0}>
+              <div class="shrink-0 px-2 py-3 border-t border-border-weak-base">
+                <div class="rounded-md bg-background-base shadow-xs-border-base">
+                  <div class="p-3 flex flex-col gap-2">
+                    <div class="text-12-medium text-text-strong">Getting started</div>
+                    <div class="text-text-base">OpenCode includes free models so you can start immediately.</div>
+                    <div class="text-text-base">Connect any provider to use models, inc. Claude, GPT, Gemini etc.</div>
+                  </div>
+                  <Button
+                    class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-md rounded-t-none shadow-none border-t border-border-weak-base px-3"
+                    size="large"
+                    icon="plus"
+                    onClick={connectProvider}
+                  >
+                    Connect provider
+                  </Button>
+                </div>
               </div>
               </div>
             </Show>
             </Show>
           </div>
           </div>
@@ -1212,50 +1425,9 @@ export default function Layout(props: ParentProps) {
     )
     )
   }
   }
 
 
-  const isMac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
-  const reserveWindowButtons = createMemo(
-    () => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"),
-  )
-
   return (
   return (
-    <div class="relative flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
-      <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex items-center">
-        <div
-          classList={{
-            "flex items-center w-full min-w-0 pr-2": true,
-            "pl-2": !isMac(),
-          }}
-        >
-          <Show when={isMac()}>
-            <div class="w-[72px] h-full shrink-0" data-tauri-drag-region />
-          </Show>
-          <IconButton
-            icon="menu"
-            variant="ghost"
-            class="xl:hidden size-8 rounded-md"
-            onClick={layout.mobileSidebar.toggle}
-          />
-          <TooltipKeybind
-            class="hidden xl:flex shrink-0"
-            placement="bottom"
-            title="Toggle sidebar"
-            keybind={command.keybind("sidebar.toggle")}
-          >
-            <IconButton
-              icon={layout.sidebar.opened() ? "layout-left" : "layout-right"}
-              variant="ghost"
-              class="size-8 rounded-md"
-              onClick={layout.sidebar.toggle}
-            />
-          </TooltipKeybind>
-          <div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
-          <div class="flex-1 h-full" data-tauri-drag-region />
-          <div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0" />
-          <Show when={reserveWindowButtons()}>
-            <div class="w-[120px] h-full shrink-0" data-tauri-drag-region />
-          </Show>
-        </div>
-      </header>
+    <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">
+      <Titlebar />
       <div class="flex-1 min-h-0 flex">
       <div class="flex-1 min-h-0 flex">
         <div
         <div
           classList={{
           classList={{
@@ -1282,7 +1454,7 @@ export default function Layout(props: ParentProps) {
         <div class="xl:hidden">
         <div class="xl:hidden">
           <div
           <div
             classList={{
             classList={{
-              "fixed inset-0 bg-black/50 z-40 transition-opacity duration-200": true,
+              "fixed inset-0 z-40 transition-opacity duration-200": true,
               "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
               "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
               "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
               "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
             }}
             }}
@@ -1302,7 +1474,14 @@ export default function Layout(props: ParentProps) {
           </div>
           </div>
         </div>
         </div>
 
 
-        <main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
+        <main
+          classList={{
+            "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base": true,
+            "border-l rounded-tl-sm": !layout.sidebar.opened(),
+          }}
+        >
+          {props.children}
+        </main>
       </div>
       </div>
       <Toast.Region />
       <Toast.Region />
     </div>
     </div>

+ 16 - 3
packages/app/src/pages/session.tsx

@@ -885,6 +885,19 @@ export default function Page() {
     window.history.replaceState(null, "", `#${anchor(id)}`)
     window.history.replaceState(null, "", `#${anchor(id)}`)
   }
   }
 
 
+  const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
+    const root = scroller
+    if (!root) {
+      el.scrollIntoView({ behavior, block: "start" })
+      return
+    }
+
+    const a = el.getBoundingClientRect()
+    const b = root.getBoundingClientRect()
+    const top = a.top - b.top + root.scrollTop
+    root.scrollTo({ top, behavior })
+  }
+
   const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
   const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
     setActiveMessage(message)
     setActiveMessage(message)
 
 
@@ -896,7 +909,7 @@ export default function Page() {
 
 
       requestAnimationFrame(() => {
       requestAnimationFrame(() => {
         const el = document.getElementById(anchor(message.id))
         const el = document.getElementById(anchor(message.id))
-        if (el) el.scrollIntoView({ behavior, block: "start" })
+        if (el) scrollToElement(el, behavior)
       })
       })
 
 
       updateHash(message.id)
       updateHash(message.id)
@@ -904,7 +917,7 @@ export default function Page() {
     }
     }
 
 
     const el = document.getElementById(anchor(message.id))
     const el = document.getElementById(anchor(message.id))
-    if (el) el.scrollIntoView({ behavior, block: "start" })
+    if (el) scrollToElement(el, behavior)
     updateHash(message.id)
     updateHash(message.id)
   }
   }
 
 
@@ -956,7 +969,7 @@ export default function Page() {
 
 
       const hashTarget = document.getElementById(hash)
       const hashTarget = document.getElementById(hash)
       if (hashTarget) {
       if (hashTarget) {
-        hashTarget.scrollIntoView({ behavior: "auto", block: "start" })
+        scrollToElement(hashTarget, "auto")
         return
         return
       }
       }
 
 

+ 1 - 1
packages/desktop/index.html

@@ -17,7 +17,7 @@
   </head>
   </head>
   <body class="antialiased overscroll-none text-12-regular overflow-hidden">
   <body class="antialiased overscroll-none text-12-regular overflow-hidden">
     <noscript>You need to enable JavaScript to run this app.</noscript>
     <noscript>You need to enable JavaScript to run this app.</noscript>
-    <div id="root" class="flex flex-col h-screen"></div>
+    <div id="root" class="flex flex-col h-dvh"></div>
     <script src="/src/index.tsx" type="module"></script>
     <script src="/src/index.tsx" type="module"></script>
   </body>
   </body>
 </html>
 </html>

+ 1 - 0
packages/desktop/src-tauri/Cargo.toml

@@ -41,6 +41,7 @@ semver = "1.0.27"
 reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
 reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
 uuid = { version = "1.19.0", features = ["v4"] }
 uuid = { version = "1.19.0", features = ["v4"] }
 
 
+
 [target.'cfg(target_os = "linux")'.dependencies]
 [target.'cfg(target_os = "linux")'.dependencies]
 gtk = "0.18.2"
 gtk = "0.18.2"
 webkit2gtk = "=2.0.1"
 webkit2gtk = "=2.0.1"

+ 1 - 0
packages/desktop/src-tauri/capabilities/default.json

@@ -7,6 +7,7 @@
     "core:default",
     "core:default",
     "opener:default",
     "opener:default",
     "core:window:allow-start-dragging",
     "core:window:allow-start-dragging",
+    "core:window:allow-set-theme",
     "core:webview:allow-set-webview-zoom",
     "core:webview:allow-set-webview-zoom",
     "core:window:allow-is-focused",
     "core:window:allow-is-focused",
     "core:window:allow-show",
     "core:window:allow-show",

+ 24 - 21
packages/desktop/src-tauri/src/lib.rs

@@ -14,7 +14,7 @@ use std::{
     sync::{Arc, Mutex},
     sync::{Arc, Mutex},
     time::{Duration, Instant},
     time::{Duration, Instant},
 };
 };
-use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, WebviewWindow};
+use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewWindowBuilder};
 use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
 use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
 use tauri_plugin_shell::process::{CommandChild, CommandEvent};
 use tauri_plugin_shell::process::{CommandChild, CommandEvent};
 use tauri_plugin_store::StoreExt;
 use tauri_plugin_store::StoreExt;
@@ -237,7 +237,14 @@ pub fn run() {
             }
             }
         }))
         }))
         .plugin(tauri_plugin_os::init())
         .plugin(tauri_plugin_os::init())
-        .plugin(tauri_plugin_window_state::Builder::new().build())
+        .plugin(
+            tauri_plugin_window_state::Builder::new()
+                .with_state_flags(
+                    tauri_plugin_window_state::StateFlags::all()
+                        - tauri_plugin_window_state::StateFlags::DECORATIONS,
+                )
+                .build(),
+        )
         .plugin(tauri_plugin_store::Builder::new().build())
         .plugin(tauri_plugin_store::Builder::new().build())
         .plugin(tauri_plugin_dialog::init())
         .plugin(tauri_plugin_dialog::init())
         .plugin(tauri_plugin_shell::init())
         .plugin(tauri_plugin_shell::init())
@@ -268,29 +275,25 @@ pub fn run() {
                 .map(|m| m.size().to_logical(m.scale_factor()))
                 .map(|m| m.size().to_logical(m.scale_factor()))
                 .unwrap_or(LogicalSize::new(1920, 1080));
                 .unwrap_or(LogicalSize::new(1920, 1080));
 
 
-            #[allow(unused_mut)]
-            let mut window_builder =
-                WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
-                    .title("OpenCode")
-                    .inner_size(size.width as f64, size.height as f64)
-                    .decorations(true)
-                    .zoom_hotkeys_enabled(true)
-                    .disable_drag_drop_handler()
-                    .initialization_script(format!(
-                        r#"
+            let config = app
+                .config()
+                .app
+                .windows
+                .iter()
+                .find(|w| w.label == "main")
+                .expect("main window config missing");
+
+            let window_builder = WebviewWindowBuilder::from_config(&app, config)
+                .expect("Failed to create window builder from config")
+                .inner_size(size.width as f64, size.height as f64)
+                .initialization_script(format!(
+                    r#"
                       window.__OPENCODE__ ??= {{}};
                       window.__OPENCODE__ ??= {{}};
                       window.__OPENCODE__.updaterEnabled = {updater_enabled};
                       window.__OPENCODE__.updaterEnabled = {updater_enabled};
                     "#
                     "#
-                    ));
-
-            #[cfg(target_os = "macos")]
-            {
-                window_builder = window_builder
-                    .title_bar_style(tauri::TitleBarStyle::Overlay)
-                    .hidden_title(true);
-            }
+                ));
 
 
-            window_builder.build().expect("Failed to create window");
+            let _window = window_builder.build().expect("Failed to create window");
 
 
             let (tx, rx) = oneshot::channel();
             let (tx, rx) = oneshot::channel();
             app.manage(ServerState::new(None, rx));
             app.manage(ServerState::new(None, rx));

+ 14 - 0
packages/desktop/src-tauri/tauri.conf.json

@@ -11,6 +11,20 @@
     "frontendDist": "../dist"
     "frontendDist": "../dist"
   },
   },
   "app": {
   "app": {
+    "windows": [
+      {
+        "label": "main",
+        "create": false,
+        "title": "OpenCode",
+        "url": "/",
+        "decorations": true,
+        "dragDropEnabled": false,
+        "zoomHotkeysEnabled": true,
+        "titleBarStyle": "Overlay",
+        "hiddenTitle": true,
+        "trafficLightPosition": { "x": 12.0, "y": 18.0 }
+      }
+    ],
     "withGlobalTauri": true,
     "withGlobalTauri": true,
     "security": {
     "security": {
       "csp": null
       "csp": null

+ 21 - 0
packages/desktop/src-tauri/tauri.prod.conf.json

@@ -2,6 +2,27 @@
   "$schema": "https://schema.tauri.app/config/2",
   "$schema": "https://schema.tauri.app/config/2",
   "productName": "OpenCode",
   "productName": "OpenCode",
   "identifier": "ai.opencode.desktop",
   "identifier": "ai.opencode.desktop",
+  "app": {
+    "windows": [
+      {
+        "label": "main",
+        "create": false,
+        "title": "OpenCode",
+        "url": "/",
+        "decorations": true,
+        "dragDropEnabled": false,
+        "zoomHotkeysEnabled": true,
+        "titleBarStyle": "Overlay",
+        "hiddenTitle": true,
+        "trafficLightPosition": { "x": 12.0, "y": 18.0 }
+      }
+    ],
+    "withGlobalTauri": true,
+    "security": {
+      "csp": null
+    },
+    "macOSPrivateApi": true
+  },
   "bundle": {
   "bundle": {
     "createUpdaterArtifacts": true,
     "createUpdaterArtifacts": true,
     "icon": [
     "icon": [

+ 55 - 0
packages/ui/src/components/hover-card.css

@@ -0,0 +1,55 @@
+[data-slot="hover-card-trigger"] {
+  display: inline-flex;
+}
+
+[data-component="hover-card-content"] {
+  z-index: 50;
+  min-width: 200px;
+  max-width: 320px;
+  border-radius: var(--radius-md);
+  background-color: var(--surface-raised-stronger-non-alpha);
+
+  border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
+  background-clip: padding-box;
+  box-shadow: var(--shadow-md);
+
+  transform-origin: var(--kb-hovercard-content-transform-origin);
+
+  &:focus-within {
+    outline: none;
+  }
+
+  &[data-closed] {
+    animation: hover-card-close 0.15s ease-out;
+  }
+
+  &[data-expanded] {
+    animation: hover-card-open 0.15s ease-out;
+  }
+
+  [data-slot="hover-card-body"] {
+    padding: 12px;
+  }
+}
+
+@keyframes hover-card-open {
+  from {
+    opacity: 0;
+    transform: scale(0.96);
+  }
+  to {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
+@keyframes hover-card-close {
+  from {
+    opacity: 1;
+    transform: scale(1);
+  }
+  to {
+    opacity: 0;
+    transform: scale(0.96);
+  }
+}

+ 31 - 0
packages/ui/src/components/hover-card.tsx

@@ -0,0 +1,31 @@
+import { HoverCard as Kobalte } from "@kobalte/core/hover-card"
+import { ComponentProps, JSXElement, ParentProps, splitProps } from "solid-js"
+
+export interface HoverCardProps extends ParentProps, Omit<ComponentProps<typeof Kobalte>, "children"> {
+  trigger: JSXElement
+  class?: ComponentProps<"div">["class"]
+  classList?: ComponentProps<"div">["classList"]
+}
+
+export function HoverCard(props: HoverCardProps) {
+  const [local, rest] = splitProps(props, ["trigger", "class", "classList", "children"])
+
+  return (
+    <Kobalte gutter={4} {...rest}>
+      <Kobalte.Trigger as="div" data-slot="hover-card-trigger">
+        {local.trigger}
+      </Kobalte.Trigger>
+      <Kobalte.Portal>
+        <Kobalte.Content
+          data-component="hover-card-content"
+          classList={{
+            ...(local.classList ?? {}),
+            [local.class ?? ""]: !!local.class,
+          }}
+        >
+          <div data-slot="hover-card-body">{local.children}</div>
+        </Kobalte.Content>
+      </Kobalte.Portal>
+    </Kobalte>
+  )
+}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 1
packages/ui/src/components/icon.tsx


+ 6 - 1
packages/ui/src/components/spinner.tsx

@@ -10,9 +10,14 @@ const squares = Array.from({ length: 16 }, (_, i) => ({
   outer: outerIndices.has(i),
   outer: outerIndices.has(i),
 }))
 }))
 
 
-export function Spinner(props: { class?: string; classList?: ComponentProps<"div">["classList"] }) {
+export function Spinner(props: {
+  class?: string
+  classList?: ComponentProps<"div">["classList"]
+  style?: ComponentProps<"div">["style"]
+}) {
   return (
   return (
     <svg
     <svg
+      {...props}
       viewBox="0 0 15 15"
       viewBox="0 0 15 15"
       data-component="spinner"
       data-component="spinner"
       classList={{
       classList={{

+ 1 - 0
packages/ui/src/styles/index.css

@@ -19,6 +19,7 @@
 @import "../components/dropdown-menu.css" layer(components);
 @import "../components/dropdown-menu.css" layer(components);
 @import "../components/dialog.css" layer(components);
 @import "../components/dialog.css" layer(components);
 @import "../components/file-icon.css" layer(components);
 @import "../components/file-icon.css" layer(components);
+@import "../components/hover-card.css" layer(components);
 @import "../components/provider-icon.css" layer(components);
 @import "../components/provider-icon.css" layer(components);
 @import "../components/icon.css" layer(components);
 @import "../components/icon.css" layer(components);
 @import "../components/icon-button.css" layer(components);
 @import "../components/icon-button.css" layer(components);

Неке датотеке нису приказане због велике количине промена