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 | |
| parent | b66222baf7a09af692e8de06179c1c3e51715269 (diff) | |
| download | opencode-184732fc2097166921dd46fbb9a8ce433a96b237.tar.gz opencode-184732fc2097166921dd46fbb9a8ce433a96b237.zip | |
fix(app): titlebar cleanup (#17206)
Diffstat (limited to 'packages/app/src/components')
| -rw-r--r-- | packages/app/src/components/prompt-input.tsx | 31 | ||||
| -rw-r--r-- | packages/app/src/components/server/server-row.tsx | 14 | ||||
| -rw-r--r-- | packages/app/src/components/session/session-header.tsx | 283 | ||||
| -rw-r--r-- | packages/app/src/components/status-popover.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/titlebar.tsx | 23 |
5 files changed, 46 insertions, 317 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index e129b499a..af9c7530f 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -26,7 +26,6 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { IconButton } from "@opencode-ai/ui/icon-button" import { Select } from "@opencode-ai/ui/select" -import { RadioGroup } from "@opencode-ai/ui/radio-group" import { useDialog } from "@opencode-ai/ui/context/dialog" import { ModelSelectorPopover } from "@/components/dialog-select-model" import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" @@ -1488,36 +1487,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => { </TooltipKeybind> </div> </div> - <div class="shrink-0"> - <RadioGroup - options={["shell", "normal"] as const} - current={store.mode} - value={(mode) => mode} - label={(mode) => ( - <TooltipKeybind - placement="top" - gutter={4} - openDelay={2000} - title={language.t(mode === "shell" ? "prompt.mode.shell" : "prompt.mode.normal")} - keybind={command.keybind(mode === "shell" ? "prompt.mode.shell" : "prompt.mode.normal")} - class="size-full flex items-center justify-center" - > - <Icon - name={mode === "shell" ? "console" : "prompt"} - class="size-[18px]" - classList={{ - "text-icon-strong-base": store.mode === mode, - "text-icon-weak": store.mode !== mode, - }} - /> - </TooltipKeybind> - )} - onSelect={(mode) => mode && setMode(mode)} - fill - pad="none" - class="w-[68px]" - /> - </div> </div> </DockTray> </Show> diff --git a/packages/app/src/components/server/server-row.tsx b/packages/app/src/components/server/server-row.tsx index 5bb290ec3..8a4b7be4d 100644 --- a/packages/app/src/components/server/server-row.tsx +++ b/packages/app/src/components/server/server-row.tsx @@ -65,22 +65,26 @@ export function ServerRow(props: ServerRowProps) { return ( <Tooltip - class="flex-1" + class="flex-1 min-w-0" value={tooltipValue()} + contentStyle={{ "max-width": "none", "white-space": "nowrap" }} placement="top-start" inactive={!truncated() && !props.conn.displayName} > <div class={props.class} classList={{ "opacity-50": props.dimmed }}> - <div class="flex flex-col items-start"> - <div class="flex flex-row items-center gap-2"> - <span ref={nameRef} class={props.nameClass ?? "truncate"}> + <div class="flex flex-col items-start min-w-0 w-full"> + <div class="flex flex-row items-center gap-2 min-w-0 w-full"> + <span ref={nameRef} class={`${props.nameClass ?? "truncate"} min-w-0`}> {name()} </span> <Show when={badge()} fallback={ <Show when={props.status?.version}> - <span ref={versionRef} class={props.versionClass ?? "text-text-weak text-14-regular truncate"}> + <span + ref={versionRef} + class={`${props.versionClass ?? "text-text-weak text-14-regular truncate"} min-w-0`} + > v{props.status?.version} </span> </Show> 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> diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx index 8073746c9..7048808c8 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -169,6 +169,7 @@ export function StatusPopover() { const language = useLanguage() const navigate = useNavigate() + const [shown, setShown] = createSignal(false) const servers = createMemo(() => { const current = server.current const list = server.list @@ -199,18 +200,23 @@ export function StatusPopover() { return ( <Popover + open={shown()} + onOpenChange={setShown} triggerAs={Button} triggerProps={{ variant: "ghost", - class: "titlebar-icon w-6 h-6 p-0 box-border", + class: "titlebar-icon w-8 h-6 p-0 box-border", "aria-label": language.t("status.popover.trigger"), style: { scale: 1 }, }} trigger={ - <div class="flex size-4 items-center justify-center"> + <div class="relative size-4"> + <div class="badge-mask-tight size-4 flex items-center justify-center"> + <Icon name={shown() ? "status-active" : "status"} size="small" /> + </div> <div classList={{ - "size-1.5 rounded-full": true, + "absolute -top-px -right-px size-1.5 rounded-full": true, "bg-icon-success-base": overallHealthy(), "bg-icon-critical-base": !overallHealthy() && server.healthy() !== undefined, "bg-border-weak-base": server.healthy() === undefined, diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 3e2374f43..3ab811e92 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -58,6 +58,12 @@ export function Titlebar() { }) const path = () => `${location.pathname}${location.search}${location.hash}` + const creating = createMemo(() => { + if (!params.dir) return false + if (params.id) return false + const parts = location.pathname.replace(/\/+$/, "").split("/") + return parts.at(-1) === "session" + }) createEffect(() => { const current = path() @@ -206,19 +212,7 @@ export function Titlebar() { aria-label={language.t("command.sidebar.toggle")} aria-expanded={layout.sidebar.opened()} > - <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0"> - <Icon - size="small" - name={layout.sidebar.opened() ? "layout-left-partial" : "layout-left"} - class="group-hover/sidebar-toggle:hidden" - /> - <Icon size="small" name="layout-left-partial" class="hidden group-hover/sidebar-toggle:inline-block" /> - <Icon - size="small" - name={layout.sidebar.opened() ? "layout-left" : "layout-left-partial"} - class="hidden group-active/sidebar-toggle:inline-block" - /> - </div> + <Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} /> </Button> </TooltipKeybind> <div class="hidden xl:flex items-center shrink-0"> @@ -231,13 +225,14 @@ export function Titlebar() { > <Button variant="ghost" - icon="new-session" + icon={creating() ? "new-session-active" : "new-session"} class="titlebar-icon w-8 h-6 p-0 box-border" onClick={() => { if (!params.dir) return navigate(`/${params.dir}/session`) }} aria-label={language.t("command.session.new")} + aria-current={creating() ? "page" : undefined} /> </TooltipKeybind> </Show> |
