diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index ae8fc200f2d..f74f3384b06 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -22,6 +22,7 @@ import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Popover } from "@opencode-ai/ui/popover" import { TextField } from "@opencode-ai/ui/text-field" import { Keybind } from "@opencode-ai/ui/keybind" +import { List } from "@opencode-ai/ui/list" import { showToast } from "@opencode-ai/ui/toast" import { StatusPopover } from "../status-popover" @@ -89,11 +90,30 @@ const detectOS = (platform: ReturnType): OS => { return "unknown" } +const errorText = (value: unknown): string => { + if (value && typeof value === "object" && "data" in value) { + const data = (value as { data?: { message?: unknown } }).data + if (typeof data?.message === "string" && data.message) return data.message + } + if (value && typeof value === "object" && "error" in value) { + const nested = errorText((value as { error?: unknown }).error) + if (nested) return nested + } + if (value && typeof value === "object" && "message" in value) { + const message = (value as { message?: unknown }).message + if (typeof message === "string" && message) return message + } + if (value instanceof Error && value.message) return value.message + if (typeof value === "string" && value) return value + return "" +} + const showRequestError = (language: ReturnType, err: unknown) => { + const message = errorText(err) showToast({ variant: "error", title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : String(err), + description: message || language.t("common.requestFailed"), }) } @@ -204,6 +224,112 @@ export function SessionHeader() { if (current) return current.name || getFilename(current.worktree) return getFilename(projectDirectory()) }) + const workspace = createMemo(() => { + const directory = projectDirectory() + if (!directory) return "" + + const current = project() + if (!current) return getFilename(directory) + + const root = getFilename(current.worktree) + if (directory === current.worktree) return root + + const leaf = getFilename(directory) + if (!leaf) return root + if (!root) return leaf + if (leaf === root) return root + return `${root}/${leaf}` + }) + + const [branchMenu, setBranchMenu] = createStore({ + open: false, + loading: false, + switching: false, + branch: "", + items: [] as { name: string; remote: boolean; worktree?: string }[], + }) + + const target = createMemo(() => { + const directory = projectDirectory() + if (!directory) return + return { + workspace: workspace(), + branch: sync.data.vcs?.branch || branchMenu.branch || "HEAD", + } + }) + + createEffect(() => { + const branch = sync.data.vcs?.branch + if (!branch) return + if (branchMenu.branch === branch) return + setBranchMenu("branch", branch) + }) + + const branches = createMemo(() => branchMenu.items) + const currentBranch = createMemo(() => { + const branch = sync.data.vcs?.branch || branchMenu.branch + if (!branch) return + return branches().find((item) => item.name === branch) + }) + const localGroup = () => language.t("session.header.branch.group.local") + const remoteGroup = () => language.t("session.header.branch.group.remote") + + createEffect(() => { + if (!branchMenu.open) return + const directory = projectDirectory() + if (!directory) return + setBranchMenu("loading", true) + void globalSDK.client.vcs + .branches({ directory }) + .then((result) => { + setBranchMenu("items", result.data ?? []) + }) + .catch((err: unknown) => { + setBranchMenu("items", []) + showRequestError(language, err) + }) + .finally(() => { + setBranchMenu("loading", false) + }) + }) + + const switchBranch = (name: string) => { + const directory = projectDirectory() + if (!directory) return + const current = sync.data.vcs?.branch || branchMenu.branch + if (current === name) { + setBranchMenu("open", false) + return + } + + const item = branchMenu.items.find((b) => b.name === name) + if (item?.worktree) { + showToast({ + variant: "error", + title: language.t("session.header.branch.inUse.title"), + description: language.t("session.header.branch.inUse.description", { branch: name }), + }) + return + } + + setBranchMenu("switching", true) + void globalSDK.client.vcs + .checkout({ + directory, + vcsCheckoutInput: { branch: name }, + }) + .then((result) => { + setBranchMenu("branch", result.data?.branch ?? name) + setBranchMenu("open", false) + }) + .catch((err: unknown) => { + showRequestError(language, err) + }) + .finally(() => { + setBranchMenu("switching", false) + }) + } + const hotkey = createMemo(() => command.keybind("file.open")) const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id)) @@ -296,12 +422,12 @@ export function SessionHeader() { platform, }) - const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center")) + const leftMount = createMemo(() => document.getElementById("opencode-titlebar-left")) const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right")) return ( <> - + {(mount) => (