Pārlūkot izejas kodu

fix(desktop): remove status bar, new elements in header

Adam 1 mēnesi atpakaļ
vecāks
revīzija
a7c4f83ca2

+ 0 - 213
packages/app/src/components/header.tsx

@@ -1,213 +0,0 @@
-import { useGlobalSync } from "@/context/global-sync"
-import { useGlobalSDK } from "@/context/global-sdk"
-import { useLayout } from "@/context/layout"
-import { Session } from "@opencode-ai/sdk/v2/client"
-import { Button } from "@opencode-ai/ui/button"
-import { Icon } from "@opencode-ai/ui/icon"
-import { Mark } from "@opencode-ai/ui/logo"
-import { Popover } from "@opencode-ai/ui/popover"
-import { Select } from "@opencode-ai/ui/select"
-import { TextField } from "@opencode-ai/ui/text-field"
-import { Tooltip } from "@opencode-ai/ui/tooltip"
-import { base64Decode } from "@opencode-ai/util/encode"
-import { useCommand } from "@/context/command"
-import { getFilename } from "@opencode-ai/util/path"
-import { A, useParams } from "@solidjs/router"
-import { createMemo, createResource, Show } from "solid-js"
-import { IconButton } from "@opencode-ai/ui/icon-button"
-import { iife } from "@opencode-ai/util/iife"
-
-export function Header(props: {
-  navigateToProject: (directory: string) => void
-  navigateToSession: (session: Session | undefined) => void
-  onMobileMenuToggle?: () => void
-}) {
-  const globalSync = useGlobalSync()
-  const globalSDK = useGlobalSDK()
-  const layout = useLayout()
-  const params = useParams()
-  const command = useCommand()
-
-  return (
-    <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
-      <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"
-        onClick={props.onMobileMenuToggle}
-      >
-        <Icon name="menu" size="small" />
-      </button>
-      <A
-        href="/"
-        classList={{
-          "hidden xl:flex": true,
-          "w-12 shrink-0 px-4 py-3.5": true,
-          "items-center justify-start self-stretch": true,
-          "border-r border-border-weak-base": true,
-        }}
-        style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
-        data-tauri-drag-region
-      >
-        <Mark class="shrink-0" />
-      </A>
-      <div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
-        <Show when={layout.projects.list().length > 0 && params.dir}>
-          {(directory) => {
-            const currentDirectory = createMemo(() => base64Decode(directory()))
-            const store = createMemo(() => globalSync.child(currentDirectory())[0])
-            const sessions = createMemo(() => (store().session ?? []).filter((s) => !s.parentID))
-            const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
-            const shareEnabled = createMemo(() => store().config.share !== "disabled")
-            return (
-              <>
-                <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={layout.projects.list().map((project) => project.worktree)}
-                        current={currentDirectory()}
-                        label={(x) => getFilename(x)}
-                        onSelect={(x) => (x ? props.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>
-                    <Select
-                      options={sessions()}
-                      current={currentSession()}
-                      placeholder="New session"
-                      label={(x) => x.title}
-                      value={(x) => x.id}
-                      onSelect={props.navigateToSession}
-                      class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
-                      variant="ghost"
-                    />
-                  </div>
-                  <Show when={currentSession()}>
-                    <Tooltip
-                      class="hidden xl:block"
-                      value={
-                        <div class="flex items-center gap-2">
-                          <span>New session</span>
-                          <span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
-                        </div>
-                      }
-                    >
-                      <IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
-                    </Tooltip>
-                  </Show>
-                </div>
-                <div class="flex items-center gap-4">
-                  <Show when={currentSession()?.summary?.files}>
-                    <Tooltip
-                      class="hidden md:block shrink-0"
-                      value={
-                        <div class="flex items-center gap-2">
-                          <span>Toggle review</span>
-                          <span class="text-icon-base text-12-medium">{command.keybind("review.toggle")}</span>
-                        </div>
-                      }
-                    >
-                      <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>
-                    </Tooltip>
-                  </Show>
-                  <Tooltip
-                    class="hidden md:block shrink-0"
-                    value={
-                      <div class="flex items-center gap-2">
-                        <span>Toggle terminal</span>
-                        <span class="text-icon-base text-12-medium">{command.keybind("terminal.toggle")}</span>
-                      </div>
-                    }
-                  >
-                    <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>
-                  </Tooltip>
-                  <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: currentDirectory() })
-                                .then((r) => r.data?.share?.url)
-                                .catch((e) => {
-                                  console.error("Failed to share session", e)
-                                  return undefined
-                                })
-                            }
-                            return shareURL
-                          },
-                        )
-                        return (
-                          <Show when={url()}>
-                            {(url) => <TextField value={url()} readOnly copyable class="w-72" />}
-                          </Show>
-                        )
-                      })}
-                    </Popover>
-                  </Show>
-                </div>
-              </>
-            )
-          }}
-        </Show>
-      </div>
-    </header>
-  )
-}

+ 0 - 53
packages/app/src/components/status-bar.tsx

@@ -1,53 +0,0 @@
-import { createMemo, Show, type ParentProps } from "solid-js"
-import { useSync } from "@/context/sync"
-import { useGlobalSync } from "@/context/global-sync"
-import { useServer } from "@/context/server"
-import { useDialog } from "@opencode-ai/ui/context/dialog"
-import { Button } from "@opencode-ai/ui/button"
-import { DialogSelectServer } from "@/components/dialog-select-server"
-
-export function StatusBar(props: ParentProps) {
-  const dialog = useDialog()
-  const server = useServer()
-  const sync = useSync()
-  const globalSync = useGlobalSync()
-
-  const directoryDisplay = createMemo(() => {
-    const directory = sync.data.path.directory || ""
-    const home = globalSync.data.path.home || ""
-    const short = home && directory.startsWith(home) ? directory.replace(home, "~") : directory
-    const branch = sync.data.vcs?.branch
-    return branch ? `${short}:${branch}` : short
-  })
-
-  return (
-    <div class="h-8 w-full shrink-0 flex items-center justify-between px-2 border-t border-border-weak-base bg-background-base">
-      <div class="flex items-center gap-3">
-        <div class="flex items-center gap-1">
-          <Button
-            size="small"
-            variant="ghost"
-            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,
-              }}
-            />
-
-            <span class="text-12-regular text-text-weak">{server.name}</span>
-          </Button>
-        </div>
-        <Show when={directoryDisplay()}>
-          <span class="text-12-regular text-text-weak">{directoryDisplay()}</span>
-        </Show>
-      </div>
-      <div class="flex items-center">{props.children}</div>
-    </div>
-  )
-}

+ 7 - 6
packages/app/src/pages/layout.tsx

@@ -26,6 +26,7 @@ import { Tooltip } 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 { Mark } from "@opencode-ai/ui/logo"
 import { getFilename } from "@opencode-ai/util/path"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
 import { Session } from "@opencode-ai/sdk/v2/client"
@@ -45,7 +46,7 @@ import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { useNotification } from "@/context/notification"
 import { Binary } from "@opencode-ai/util/binary"
-import { Header } from "@/components/header"
+
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
 import { DialogSelectProvider } from "@/components/dialog-select-provider"
@@ -873,6 +874,11 @@ export default function Layout(props: ParentProps) {
     return (
       <>
         <div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
+          <Show when={!sidebarProps.mobile}>
+            <A href="/" class="shrink-0 h-8 flex items-center justify-start px-2" data-tauri-drag-region>
+              <Mark class="shrink-0" />
+            </A>
+          </Show>
           <Show when={!sidebarProps.mobile}>
             <Tooltip
               class="shrink-0"
@@ -1018,11 +1024,6 @@ 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">
-      <Header
-        navigateToProject={navigateToProject}
-        navigateToSession={navigateToSession}
-        onMobileMenuToggle={mobileSidebar.toggle}
-      />
       <div class="flex-1 min-h-0 flex">
         <div
           classList={{

+ 220 - 8
packages/app/src/pages/session.tsx

@@ -51,17 +51,26 @@ import { DialogSelectFile } from "@/components/dialog-select-file"
 import { DialogSelectModel } from "@/components/dialog-select-model"
 import { DialogSelectMcp } from "@/components/dialog-select-mcp"
 import { useCommand } from "@/context/command"
-import { useNavigate, useParams } from "@solidjs/router"
+import { A, useNavigate, useParams } from "@solidjs/router"
 import { UserMessage } from "@opencode-ai/sdk/v2"
 import { useSDK } from "@/context/sdk"
 import { usePrompt } from "@/context/prompt"
 import { extractPromptFromParts } from "@/utils/prompt"
 import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
-import { StatusBar } from "@/components/status-bar"
-import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
-import { SessionLspIndicator } from "@/components/session-lsp-indicator"
 import { usePermission } from "@/context/permission"
 import { showToast } from "@opencode-ai/ui/toast"
+import { useServer } from "@/context/server"
+import { Button } from "@opencode-ai/ui/button"
+import { DialogSelectServer } from "@/components/dialog-select-server"
+import { SessionLspIndicator } from "@/components/session-lsp-indicator"
+import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
+import { useGlobalSDK } from "@/context/global-sdk"
+import { Popover } from "@opencode-ai/ui/popover"
+import { Select } from "@opencode-ai/ui/select"
+import { TextField } from "@opencode-ai/ui/text-field"
+import { base64Encode } from "@opencode-ai/util/encode"
+import { iife } from "@opencode-ai/util/iife"
+import { Session } from "@opencode-ai/sdk/v2/client"
 
 function same<T>(a: readonly T[], b: readonly T[]) {
   if (a === b) return true
@@ -69,6 +78,212 @@ function same<T>(a: readonly T[], b: readonly T[]) {
   return a.every((x, i) => x === b[i])
 }
 
+function Header(props: { onMobileMenuToggle?: () => void }) {
+  const globalSDK = useGlobalSDK()
+  const layout = useLayout()
+  const params = useParams()
+  const navigate = useNavigate()
+  const command = useCommand()
+  const server = useServer()
+  const dialog = useDialog()
+  const sync = useSync()
+
+  const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
+  const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
+  const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
+  const branch = createMemo(() => sync.data.vcs?.branch)
+
+  function navigateToProject(directory: string) {
+    navigate(`/${base64Encode(directory)}`)
+  }
+
+  function navigateToSession(session: Session | undefined) {
+    if (!session) return
+    navigate(`/${params.dir}/session/${session.id}`)
+  }
+
+  return (
+    <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
+      <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"
+        onClick={props.onMobileMenuToggle}
+      >
+        <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 gap-2 min-w-0">
+            <div class="hidden xl:flex items-center gap-2">
+              <Select
+                options={layout.projects.list().map((project) => project.worktree)}
+                current={sync.directory}
+                label={(x) => {
+                  const name = getFilename(x)
+                  const b = x === sync.directory ? branch() : undefined
+                  return b ? `${name}:${b}` : name
+                }}
+                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>
+            <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>
+          <Show when={currentSession()}>
+            <Tooltip
+              class="hidden xl:block"
+              value={
+                <div class="flex items-center gap-2">
+                  <span>New session</span>
+                  <span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
+                </div>
+              }
+            >
+              <IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
+            </Tooltip>
+          </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
+                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}>
+              <Tooltip
+                class="hidden md:block shrink-0"
+                value={
+                  <div class="flex items-center gap-2">
+                    <span>Toggle review</span>
+                    <span class="text-icon-base text-12-medium">{command.keybind("review.toggle")}</span>
+                  </div>
+                }
+              >
+                <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>
+              </Tooltip>
+            </Show>
+            <Tooltip
+              class="hidden md:block shrink-0"
+              value={
+                <div class="flex items-center gap-2">
+                  <span>Toggle terminal</span>
+                  <span class="text-icon-base text-12-medium">{command.keybind("terminal.toggle")}</span>
+                </div>
+              }
+            >
+              <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>
+            </Tooltip>
+          </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: sync.directory })
+                        .then((r) => r.data?.share?.url)
+                        .catch((e) => {
+                          console.error("Failed to share session", e)
+                          return undefined
+                        })
+                    }
+                    return shareURL
+                  },
+                )
+                return <Show when={url()}>{(url) => <TextField value={url()} readOnly copyable class="w-72" />}</Show>
+              })}
+            </Popover>
+          </Show>
+        </div>
+      </div>
+    </header>
+  )
+}
+
 export default function Page() {
   const layout = useLayout()
   const local = useLocal()
@@ -718,6 +933,7 @@ export default function Page() {
 
   return (
     <div class="relative bg-background-base size-full overflow-hidden flex flex-col">
+      <Header />
       <div class="md:hidden flex-1 min-h-0 flex flex-col bg-background-stronger">
         <Switch>
           <Match when={!params.id}>
@@ -1002,10 +1218,6 @@ export default function Page() {
           </DragDropProvider>
         </div>
       </Show>
-      <StatusBar>
-        <SessionLspIndicator />
-        <SessionMcpIndicator />
-      </StatusBar>
     </div>
   )
 }

+ 1 - 0
packages/ui/src/components/icon.tsx

@@ -57,6 +57,7 @@ const icons = {
   share: `<path d="M10.0013 12.0846L10.0013 3.33464M13.7513 6.66797L10.0013 2.91797L6.2513 6.66797M17.0846 10.418V17.0846H2.91797V10.418" stroke="currentColor" stroke-linecap="square"/>`,
   download: `<path d="M13.9583 10.6257L10 14.584L6.04167 10.6257M10 2.08398V13.959M16.25 17.9173H3.75" stroke="currentColor" stroke-linecap="square"/>`,
   menu: `<path d="M2.5 5H17.5M2.5 10H17.5M2.5 15H17.5" stroke="currentColor" stroke-linecap="square"/>`,
+  server: `<rect x="3.35547" y="1.92969" width="13.2857" height="16.1429" stroke="currentColor"/><rect x="3.35547" y="11.9297" width="13.2857" height="6.14286" stroke="currentColor"/><rect x="12.8555" y="14.2852" width="1.42857" height="1.42857" fill="currentColor"/><rect x="10" y="14.2852" width="1.42857" height="1.42857" fill="currentColor"/>`,
 }
 
 export interface IconProps extends ComponentProps<"svg"> {