summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2026-03-12 18:26:50 +0000
committerGitHub <[email protected]>2026-03-12 18:26:50 +0000
commit184732fc2097166921dd46fbb9a8ce433a96b237 (patch)
tree6dd45cc5483f968fe4ad3c19456eb62c54b97b94 /packages/app/src/components
parentb66222baf7a09af692e8de06179c1c3e51715269 (diff)
downloadopencode-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.tsx31
-rw-r--r--packages/app/src/components/server/server-row.tsx14
-rw-r--r--packages/app/src/components/session/session-header.tsx283
-rw-r--r--packages/app/src/components/status-popover.tsx12
-rw-r--r--packages/app/src/components/titlebar.tsx23
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>