diff options
| author | David Hill <[email protected]> | 2026-03-12 18:26:50 +0000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-12 18:26:50 +0000 |
| commit | 184732fc2097166921dd46fbb9a8ce433a96b237 (patch) | |
| tree | 6dd45cc5483f968fe4ad3c19456eb62c54b97b94 /packages/app/src/components/session | |
| parent | b66222baf7a09af692e8de06179c1c3e51715269 (diff) | |
| download | opencode-184732fc2097166921dd46fbb9a8ce433a96b237.tar.gz opencode-184732fc2097166921dd46fbb9a8ce433a96b237.zip | |
fix(app): titlebar cleanup (#17206)
Diffstat (limited to 'packages/app/src/components/session')
| -rw-r--r-- | packages/app/src/components/session/session-header.tsx | 283 |
1 files changed, 19 insertions, 264 deletions
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 9476f8b9b..8cb704bf1 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -4,9 +4,7 @@ import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Keybind } from "@opencode-ai/ui/keybind" -import { Popover } from "@opencode-ai/ui/popover" import { Spinner } from "@opencode-ai/ui/spinner" -import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@opencode-ai/ui/toast" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { getFilename } from "@opencode-ai/util/path" @@ -14,12 +12,10 @@ import { createEffect, createMemo, For, onCleanup, Show } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" import { useCommand } from "@/context/command" -import { useGlobalSDK } from "@/context/global-sdk" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { usePlatform } from "@/context/platform" import { useServer } from "@/context/server" -import { useSync } from "@/context/sync" import { useTerminal } from "@/context/terminal" import { focusTerminalById } from "@/pages/session/helpers" import { useSessionLayout } from "@/pages/session/session-layout" @@ -112,12 +108,6 @@ const LINUX_APPS = [ }, ] as const -type OpenOption = (typeof MAC_APPS)[number] | (typeof WINDOWS_APPS)[number] | (typeof LINUX_APPS)[number] -type OpenIcon = OpenApp | "file-explorer" -const OPEN_ICON_BASE = new Set<OpenIcon>(["finder", "vscode", "cursor", "zed"]) - -const openIconSize = (id: OpenIcon) => (OPEN_ICON_BASE.has(id) ? "size-4" : "size-[19px]") - const detectOS = (platform: ReturnType<typeof usePlatform>): OS => { if (platform.platform === "desktop" && platform.os) return platform.os if (typeof navigator !== "object") return "unknown" @@ -136,98 +126,10 @@ const showRequestError = (language: ReturnType<typeof useLanguage>, err: unknown }) } -function useSessionShare(args: { - globalSDK: ReturnType<typeof useGlobalSDK> - currentSession: () => - | { - share?: { - url?: string - } - } - | undefined - sessionID: () => string | undefined - projectDirectory: () => string - platform: ReturnType<typeof usePlatform> -}) { - const [state, setState] = createStore({ - share: false, - unshare: false, - copied: false, - timer: undefined as number | undefined, - }) - const shareUrl = createMemo(() => args.currentSession()?.share?.url) - - createEffect(() => { - const url = shareUrl() - if (url) return - if (state.timer) window.clearTimeout(state.timer) - setState({ copied: false, timer: undefined }) - }) - - onCleanup(() => { - if (state.timer) window.clearTimeout(state.timer) - }) - - const shareSession = () => { - const sessionID = args.sessionID() - if (!sessionID || state.share) return - setState("share", true) - args.globalSDK.client.session - .share({ sessionID, directory: args.projectDirectory() }) - .catch((error) => { - console.error("Failed to share session", error) - }) - .finally(() => { - setState("share", false) - }) - } - - const unshareSession = () => { - const sessionID = args.sessionID() - if (!sessionID || state.unshare) return - setState("unshare", true) - args.globalSDK.client.session - .unshare({ sessionID, directory: args.projectDirectory() }) - .catch((error) => { - console.error("Failed to unshare session", error) - }) - .finally(() => { - setState("unshare", false) - }) - } - - const copyLink = (onError: (error: unknown) => void) => { - const url = shareUrl() - if (!url) return - navigator.clipboard - .writeText(url) - .then(() => { - if (state.timer) window.clearTimeout(state.timer) - setState("copied", true) - const timer = window.setTimeout(() => { - setState("copied", false) - setState("timer", undefined) - }, 3000) - setState("timer", timer) - }) - .catch(onError) - } - - const viewShare = () => { - const url = shareUrl() - if (!url) return - args.platform.openLink(url) - } - - return { state, shareUrl, shareSession, unshareSession, copyLink, viewShare } -} - export function SessionHeader() { - const globalSDK = useGlobalSDK() const layout = useLayout() const command = useCommand() const server = useServer() - const sync = useSync() const platform = usePlatform() const language = useLanguage() const terminal = useTerminal() @@ -245,10 +147,6 @@ export function SessionHeader() { return getFilename(projectDirectory()) }) const hotkey = createMemo(() => command.keybind("file.open")) - - const currentSession = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) - const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") - const showShare = createMemo(() => shareEnabled() && !!params.id) const os = createMemo(() => detectOS(platform)) const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ @@ -356,14 +254,6 @@ export function SessionHeader() { .catch((err: unknown) => showRequestError(language, err)) } - const share = useSessionShare({ - globalSDK, - currentSession, - sessionID: () => params.id, - projectDirectory, - platform, - }) - const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center")) const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right")) @@ -391,7 +281,9 @@ export function SessionHeader() { <Show when={hotkey()}> {(keybind) => ( - <Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0">{keybind()}</Keybind> + <Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0 text-text-weaker"> + {keybind()} + </Keybind> )} </Show> </Button> @@ -402,7 +294,6 @@ export function SessionHeader() { {(mount) => ( <Portal mount={mount()}> <div class="flex items-center gap-2"> - <StatusPopover /> <Show when={projectDirectory()}> <div class="hidden xl:flex items-center"> <Show @@ -427,7 +318,7 @@ export function SessionHeader() { <div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden"> <Button variant="ghost" - class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none disabled:!cursor-default" + class="rounded-none h-full py-0 pr-1.5 pl-px gap-1.5 border-none shadow-none disabled:!cursor-default" classList={{ "bg-surface-raised-base-active": opening(), }} @@ -435,17 +326,13 @@ export function SessionHeader() { disabled={opening()} aria-label={language.t("session.header.open.ariaLabel", { app: current().label })} > - <div class="flex size-5 shrink-0 items-center justify-center"> - <Show - when={opening()} - fallback={<AppIcon id={current().icon} class={openIconSize(current().icon)} />} - > + <div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5"> + <Show when={opening()} fallback={<AppIcon id={current().icon} />}> <Spinner class="size-3.5 text-icon-base" /> </Show> </div> <span class="text-12-regular text-text-strong">{language.t("common.open")}</span> </Button> - <div class="self-stretch w-px bg-border-weak-base" /> <DropdownMenu gutter={4} placement="bottom-end" @@ -457,17 +344,20 @@ export function SessionHeader() { icon="chevron-down" variant="ghost" disabled={opening()} - class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default" + class="rounded-none h-full w-[20px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default" classList={{ "bg-surface-raised-base-active": opening(), }} aria-label={language.t("session.header.open.menu")} /> <DropdownMenu.Portal> - <DropdownMenu.Content> + <DropdownMenu.Content class="[&_[data-slot=dropdown-menu-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]+[data-slot=dropdown-menu-radio-item]]:mt-1"> <DropdownMenu.Group> - <DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel> + <DropdownMenu.GroupLabel class="!px-1 !py-1"> + {language.t("session.header.openIn")} + </DropdownMenu.GroupLabel> <DropdownMenu.RadioGroup + class="mt-1" value={current().id} onChange={(value) => { if (!OPEN_APPS.includes(value as OpenApp)) return @@ -484,8 +374,8 @@ export function SessionHeader() { openDir(o.id) }} > - <div class="flex size-5 shrink-0 items-center justify-center"> - <AppIcon id={o.icon} class={openIconSize(o.icon)} /> + <div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5"> + <AppIcon id={o.icon} /> </div> <DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel> <DropdownMenu.ItemIndicator> @@ -518,113 +408,10 @@ export function SessionHeader() { </Show> </div> </Show> - <Show when={showShare()}> - <div class="flex items-center"> - <Popover - title={language.t("session.share.popover.title")} - description={ - share.shareUrl() - ? language.t("session.share.popover.description.shared") - : language.t("session.share.popover.description.unshared") - } - gutter={4} - placement="bottom-end" - shift={-64} - class="rounded-xl [&_[data-slot=popover-close-button]]:hidden" - triggerAs={Button} - triggerProps={{ - variant: "ghost", - class: - "rounded-md h-[24px] px-3 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-base-active", - classList: { - "rounded-r-none": share.shareUrl() !== undefined, - "border-r-0": share.shareUrl() !== undefined, - }, - style: { scale: 1 }, - }} - trigger={<span class="text-12-regular">{language.t("session.share.action.share")}</span>} - > - <div class="flex flex-col gap-2"> - <Show - when={share.shareUrl()} - fallback={ - <div class="flex"> - <Button - size="large" - variant="primary" - class="w-1/2" - onClick={share.shareSession} - disabled={share.state.share} - > - {share.state.share - ? language.t("session.share.action.publishing") - : language.t("session.share.action.publish")} - </Button> - </div> - } - > - <div class="flex flex-col gap-2"> - <TextField - value={share.shareUrl() ?? ""} - readOnly - copyable - copyKind="link" - tabIndex={-1} - class="w-full" - /> - <div class="grid grid-cols-2 gap-2"> - <Button - size="large" - variant="secondary" - class="w-full shadow-none border border-border-weak-base" - onClick={share.unshareSession} - disabled={share.state.unshare} - > - {share.state.unshare - ? language.t("session.share.action.unpublishing") - : language.t("session.share.action.unpublish")} - </Button> - <Button - size="large" - variant="primary" - class="w-full" - onClick={share.viewShare} - disabled={share.state.unshare} - > - {language.t("session.share.action.view")} - </Button> - </div> - </div> - </Show> - </div> - </Popover> - <Show when={share.shareUrl()} fallback={<div aria-hidden="true" />}> - <Tooltip - value={ - share.state.copied - ? language.t("session.share.copy.copied") - : language.t("session.share.copy.copyLink") - } - placement="top" - gutter={8} - > - <IconButton - icon={share.state.copied ? "check" : "link"} - variant="ghost" - class="rounded-l-none h-[24px] border border-border-weak-base bg-surface-panel shadow-none" - onClick={() => share.copyLink((error) => showRequestError(language, error))} - disabled={share.state.unshare} - aria-label={ - share.state.copied - ? language.t("session.share.copy.copied") - : language.t("session.share.copy.copyLink") - } - /> - </Tooltip> - </Show> - </div> - </Show> <div class="flex items-center gap-1"> + <Tooltip placement="bottom" value={language.t("status.popover.trigger")}> + <StatusPopover /> + </Tooltip> <TooltipKeybind title={language.t("command.terminal.toggle")} keybind={command.keybind("terminal.toggle")} @@ -637,23 +424,7 @@ export function SessionHeader() { aria-expanded={view().terminal.opened()} aria-controls="terminal-panel" > - <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0"> - <Icon - size="small" - name={view().terminal.opened() ? "layout-bottom-partial" : "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={view().terminal.opened() ? "layout-bottom" : "layout-bottom-partial"} - class="hidden group-active/terminal-toggle:inline-block" - /> - </div> + <Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} /> </Button> </TooltipKeybind> @@ -670,23 +441,7 @@ export function SessionHeader() { aria-expanded={view().reviewPanel.opened()} aria-controls="review-panel" > - <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0"> - <Icon - size="small" - name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-right"} - class="group-hover/review-toggle:hidden" - /> - <Icon - size="small" - name="layout-right-partial" - class="hidden group-hover/review-toggle:inline-block" - /> - <Icon - size="small" - name={view().reviewPanel.opened() ? "layout-right" : "layout-right-partial"} - class="hidden group-active/review-toggle:inline-block" - /> - </div> + <Icon size="small" name={view().reviewPanel.opened() ? "review-active" : "review"} /> </Button> </TooltipKeybind> |
