diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts index 2dc85a779ce..31c92566ed6 100644 --- a/packages/app/src/runtime/i18n/en.ts +++ b/packages/app/src/runtime/i18n/en.ts @@ -972,6 +972,8 @@ export const dict = { "session.header.open.ariaLabel": "Open in {{app}}", "session.header.open.menu": "Open options", "session.header.open.copyPath": "Copy path", + "session.project.copyRepositoryPath": "Copy local repository path", + "session.project.copyWorktreePath": "Copy worktree path", "session.header.open.finder": "Finder", "session.header.open.fileExplorer": "File Explorer", "session.header.open.fileManager": "File Manager", diff --git a/packages/app/src/session/session-identity-header.tsx b/packages/app/src/session/session-identity-header.tsx index 82474950805..3e7b0a4a969 100644 --- a/packages/app/src/session/session-identity-header.tsx +++ b/packages/app/src/session/session-identity-header.tsx @@ -4,9 +4,10 @@ import { IconButton } from "@opencode/ui/icon-button" import { Menu } from "@opencode/ui/menu" import { ProjectAvatar } from "@opencode/ui/project-avatar" import { Tooltip } from "@opencode/ui/tooltip" +import { getFilename } from "@opencode/util/path" import { createResizeObserver } from "@solid-primitives/resize-observer" import { useNavigate } from "@solidjs/router" -import { createMemo, For, Show, type ParentProps } from "solid-js" +import { createEffect, createMemo, For, onCleanup, Show, type ParentProps } from "solid-js" import { createStore } from "solid-js/store" import { useServer } from "@/runtime/server/current" import { ServerConnection } from "@/runtime/server/registry" @@ -17,7 +18,7 @@ import { getProjectAvatarVariant, useLayout, type LocalProject } from "@/shell/s import { tabKey, useTabs } from "@/shell/tabs/tabs" import { useSettingsSurface } from "@/settings/surface" import { pathKey } from "@/workspaces/path-key" -import { isProjectDirectory, isWorkspaceDirectory } from "@/workspaces/paths" +import { containsDirectory, isProjectDirectory, isWorkspaceDirectory } from "@/workspaces/paths" import { sessionHref } from "@/shell/routes/session" import { showToast } from "@/shell/notifications/toast" import { sessionTitle } from "./title" @@ -50,8 +51,29 @@ export function SessionProjectMenu(props: { projectTruncated: false, pathTruncated: false, pathFocused: false, + workspaceCopied: false, + workspaceCopyDismissed: false, }) const projectName = createMemo(() => displayName(props.project ?? { worktree: props.directory ?? "" })) + const workspaceDirectory = createMemo(() => { + const directory = props.directory + if (!directory) return "" + if (!props.workspace) return props.project?.worktree ?? directory + return props.project?.sandboxes?.find((item) => containsDirectory(item, directory)) ?? directory + }) + const workspaceName = createMemo(() => { + if (!props.workspace) return language.t("session.new.workspace.local") + return getFilename(workspaceDirectory()) + }) + createEffect(() => { + if (!state.workspaceCopied) return + const dismiss = setTimeout(() => setState("workspaceCopyDismissed", true), 2000) + const reset = setTimeout(() => setState("workspaceCopied", false), 2100) + onCleanup(() => { + clearTimeout(dismiss) + clearTimeout(reset) + }) + }) const canOpenPath = () => platform.platform === "desktop" && !!platform.openPath && server.isLocal && !!props.directory const openPath = () => { @@ -71,6 +93,14 @@ export function SessionProjectMenu(props: { project: current.worktree, }) } + const copyWorkspaceDirectory = () => { + const directory = workspaceDirectory() + if (!directory) return + void (platform.writeClipboardText?.(directory) ?? navigator.clipboard.writeText(directory)).then( + () => setState({ workspaceCopied: true, workspaceCopyDismissed: false }), + () => showToast({ title: language.t("common.requestFailed") }), + ) + } return ( setState({ open, pathFocused: false })} + onOpenChange={(open) => { + setState({ open, pathFocused: false }) + if (open) return + setState({ workspaceCopied: false, workspaceCopyDismissed: false }) + }} > {projectName()}} class="flex shrink-0"> + + anchor?.querySelector('[data-slot="session-project-copy-icon"]')?.getBoundingClientRect() + } + class="min-w-0" + > + setState({ workspaceCopied: false, workspaceCopyDismissed: false })} + > + + + + {workspaceName()} + + + + +