diff options
| author | Adam <[email protected]> | 2025-12-31 10:22:11 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-31 10:22:17 -0600 |
| commit | a7c4f83ca2e4d5da94de3df0c210dbb36b0bae86 (patch) | |
| tree | b488e22ac121b2dde31f08c1fb57f34d206402d2 | |
| parent | ed70a072015a17d79dca455242abb53f14757f2d (diff) | |
| download | opencode-a7c4f83ca2e4d5da94de3df0c210dbb36b0bae86.tar.gz opencode-a7c4f83ca2e4d5da94de3df0c210dbb36b0bae86.zip | |
fix(desktop): remove status bar, new elements in header
| -rw-r--r-- | packages/app/src/components/header.tsx | 213 | ||||
| -rw-r--r-- | packages/app/src/components/status-bar.tsx | 53 | ||||
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 13 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 228 | ||||
| -rw-r--r-- | packages/ui/src/components/icon.tsx | 1 |
5 files changed, 228 insertions, 280 deletions
diff --git a/packages/app/src/components/header.tsx b/packages/app/src/components/header.tsx deleted file mode 100644 index 2f77ecdfc..000000000 --- a/packages/app/src/components/header.tsx +++ /dev/null @@ -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> - ) -} diff --git a/packages/app/src/components/status-bar.tsx b/packages/app/src/components/status-bar.tsx deleted file mode 100644 index 0ca403d72..000000000 --- a/packages/app/src/components/status-bar.tsx +++ /dev/null @@ -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> - ) -} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index b8e7c5934..a45c4d792 100644 --- a/packages/app/src/pages/layout.tsx +++ b/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" @@ -874,6 +875,11 @@ export default function Layout(props: ParentProps) { <> <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" placement="right" @@ -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={{ diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 032a8375a..3e5884460 100644 --- a/packages/app/src/pages/session.tsx +++ b/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> ) } diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 5e1a8e32a..ffd34d851 100644 --- a/packages/ui/src/components/icon.tsx +++ b/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"> { |
