import { AppIcon } from "@opencode-ai/ui/app-icon" import { Button } from "@opencode-ai/ui/button" 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" 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" import { decode64 } from "@/utils/base64" import { Persist, persisted } from "@/utils/persist" import { StatusPopover } from "../status-popover" const OPEN_APPS = [ "vscode", "cursor", "zed", "textmate", "antigravity", "finder", "terminal", "iterm2", "ghostty", "warp", "xcode", "android-studio", "powershell", "sublime-text", ] as const type OpenApp = (typeof OPEN_APPS)[number] type OS = "macos" | "windows" | "linux" | "unknown" const MAC_APPS = [ { id: "vscode", label: "VS Code", icon: "vscode", openWith: "Visual Studio Code", }, { id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" }, { id: "zed", label: "Zed", icon: "zed", openWith: "Zed" }, { id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" }, { id: "antigravity", label: "Antigravity", icon: "antigravity", openWith: "Antigravity", }, { id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" }, { id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" }, { id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" }, { id: "warp", label: "Warp", icon: "warp", openWith: "Warp" }, { id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" }, { id: "android-studio", label: "Android Studio", icon: "android-studio", openWith: "Android Studio", }, { id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text", }, ] as const const WINDOWS_APPS = [ { id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" }, { id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" }, { id: "zed", label: "Zed", icon: "zed", openWith: "zed" }, { id: "powershell", label: "PowerShell", icon: "powershell", openWith: "powershell", }, { id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text", }, ] as const const LINUX_APPS = [ { id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" }, { id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" }, { id: "zed", label: "Zed", icon: "zed", openWith: "zed" }, { id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text", }, ] 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(["finder", "vscode", "cursor", "zed"]) const openIconSize = (id: OpenIcon) => (OPEN_ICON_BASE.has(id) ? "size-4" : "size-[19px]") const detectOS = (platform: ReturnType): OS => { if (platform.platform === "desktop" && platform.os) return platform.os if (typeof navigator !== "object") return "unknown" const value = navigator.platform || navigator.userAgent if (/Mac/i.test(value)) return "macos" if (/Win/i.test(value)) return "windows" if (/Linux/i.test(value)) return "linux" return "unknown" } const showRequestError = (language: ReturnType, err: unknown) => { showToast({ variant: "error", title: language.t("common.requestFailed"), description: err instanceof Error ? err.message : String(err), }) } function useSessionShare(args: { globalSDK: ReturnType currentSession: () => | { share?: { url?: string } } | undefined sessionID: () => string | undefined projectDirectory: () => string platform: ReturnType }) { 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() const { params, view } = useSessionLayout() const projectDirectory = createMemo(() => decode64(params.dir) ?? "") const project = createMemo(() => { const directory = projectDirectory() if (!directory) return return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) }) const name = createMemo(() => { const current = project() if (current) return current.name || getFilename(current.worktree) 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>>({ finder: true, }) const apps = createMemo(() => { if (os() === "macos") return MAC_APPS if (os() === "windows") return WINDOWS_APPS return LINUX_APPS }) const fileManager = createMemo(() => { if (os() === "macos") return { label: "Finder", icon: "finder" as const } if (os() === "windows") return { label: "File Explorer", icon: "file-explorer" as const } return { label: "File Manager", icon: "finder" as const } }) createEffect(() => { if (platform.platform !== "desktop") return if (!platform.checkAppExists) return const list = apps() setExists(Object.fromEntries(list.map((app) => [app.id, undefined])) as Partial>) void Promise.all( list.map((app) => Promise.resolve(platform.checkAppExists?.(app.openWith)) .then((value) => Boolean(value)) .catch(() => false) .then((ok) => [app.id, ok] as const), ), ).then((entries) => { setExists(Object.fromEntries(entries) as Partial>) }) }) const options = createMemo(() => { return [ { id: "finder", label: fileManager().label, icon: fileManager().icon }, ...apps().filter((app) => exists[app.id]), ] as const }) const toggleTerminal = () => { const next = !view().terminal.opened() view().terminal.toggle() if (!next) return const id = terminal.active() if (!id) return focusTerminalById(id) } const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp })) const [menu, setMenu] = createStore({ open: false }) const [openRequest, setOpenRequest] = createStore({ app: undefined as OpenApp | undefined, }) const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal()) const current = createMemo( () => options().find((o) => o.id === prefs.app) ?? options()[0] ?? ({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const), ) const opening = createMemo(() => openRequest.app !== undefined) const selectApp = (app: OpenApp) => { if (!options().some((item) => item.id === app)) return setPrefs("app", app) } const openDir = (app: OpenApp) => { if (opening() || !canOpen() || !platform.openPath) return const directory = projectDirectory() if (!directory) return const item = options().find((o) => o.id === app) const openWith = item && "openWith" in item ? item.openWith : undefined setOpenRequest("app", app) platform .openPath(directory, openWith) .catch((err: unknown) => showRequestError(language, err)) .finally(() => { setOpenRequest("app", undefined) }) } const copyPath = () => { const directory = projectDirectory() if (!directory) return navigator.clipboard .writeText(directory) .then(() => { showToast({ variant: "success", icon: "circle-check", title: language.t("session.share.copy.copied"), description: directory, }) }) .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")) return ( <> {(mount) => ( )} {(mount) => (
} >
setMenu("open", open)} > {language.t("session.header.openIn")} { if (!OPEN_APPS.includes(value as OpenApp)) return selectApp(value as OpenApp) }} > {(o) => ( { setMenu("open", false) openDir(o.id) }} >
{o.label}
)}
{ setMenu("open", false) copyPath() }} >
{language.t("session.header.open.copyPath")}
{language.t("session.share.action.share")}} >
} >
)} ) }