From 5ca2dc87b7d07bcadde88bf60762406ec10b39f6 Mon Sep 17 00:00:00 2001 From: David Hill Date: Wed, 11 Mar 2026 15:39:59 +0000 Subject: [PATCH] fix(app): move session share into options menu Remove the title bar share control and surface sharing from the session options dropdown, opening the share popover in-place after selection to avoid flicker and keep consistent dismissal behavior. --- .../src/components/session/session-header.tsx | 211 +----------------- .../src/pages/session/message-timeline.tsx | 196 +++++++++++++++- 2 files changed, 191 insertions(+), 216 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 97f0530e98..c20161b987 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -4,23 +4,19 @@ 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 { TooltipKeybind } from "@opencode-ai/ui/tooltip" import { getFilename } from "@opencode-ai/util/path" import { useParams } from "@solidjs/router" 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 { decode64 } from "@/utils/base64" @@ -136,99 +132,11 @@ const showRequestError = (language: ReturnType, err: unknown }) } -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 params = useParams() const command = useCommand() const server = useServer() - const sync = useSync() const platform = usePlatform() const language = useLanguage() const terminal = useTerminal() @@ -246,9 +154,6 @@ export function SessionHeader() { }) 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 sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const view = createMemo(() => layout.view(sessionKey)) const os = createMemo(() => detectOS(platform)) @@ -361,14 +266,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")) @@ -523,112 +420,6 @@ export function SessionHeader() { - -
- {language.t("session.share.action.share")}} - > -
- - -
- } - > -
- -
- - -
-
- -
- - - -
props.renderedUserMessages.map((message) => message.id)) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) @@ -292,6 +298,8 @@ export function MessageTimeline(props: { return sync.session.get(id) }) const titleValue = createMemo(() => info()?.title) + const shareUrl = createMemo(() => info()?.share?.url) + const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") const parentID = createMemo(() => info()?.parentID) const showHeader = createMemo(() => !!(titleValue() || parentID())) const stageCfg = { init: 1, batch: 3 } @@ -308,9 +316,55 @@ export function MessageTimeline(props: { saving: false, menuOpen: false, pendingRename: false, + pendingShare: false, }) let titleRef: HTMLInputElement | undefined + const [share, setShare] = createStore({ + open: false, + dismiss: null as "escape" | "outside" | null, + }) + + let more: HTMLButtonElement | undefined + + const [req, setReq] = createStore({ share: false, unshare: false }) + + const shareSession = () => { + const id = sessionID() + if (!id || req.share) return + if (!shareEnabled()) return + setReq("share", true) + globalSDK.client.session + .share({ sessionID: id, directory: sdk.directory }) + .catch((error) => { + console.error("Failed to share session", error) + }) + .finally(() => { + setReq("share", false) + }) + } + + const unshareSession = () => { + const id = sessionID() + if (!id || req.unshare) return + if (!shareEnabled()) return + setReq("unshare", true) + globalSDK.client.session + .unshare({ sessionID: id, directory: sdk.directory }) + .catch((error) => { + console.error("Failed to unshare session", error) + }) + .finally(() => { + setReq("unshare", false) + }) + } + + const viewShare = () => { + const url = shareUrl() + if (!url) return + platform.openLink(url) + } + const errorMessage = (err: unknown) => { if (err && typeof err === "object" && "data" in err) { const data = (err as { data?: { message?: string } }).data @@ -323,7 +377,15 @@ export function MessageTimeline(props: { createEffect( on( sessionKey, - () => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }), + () => + setTitle({ + draft: "", + editing: false, + saving: false, + menuOpen: false, + pendingRename: false, + pendingShare: false, + }), { defer: true }, ), ) @@ -672,7 +734,10 @@ export function MessageTimeline(props: { gutter={4} placement="bottom-end" open={title.menuOpen} - onOpenChange={(open) => setTitle("menuOpen", open)} + onOpenChange={(open) => { + setTitle("menuOpen", open) + if (open) return + }} > { + more = el + }} /> { - if (!title.pendingRename) return - event.preventDefault() - setTitle("pendingRename", false) - openTitleEditor() + if (title.pendingRename) { + event.preventDefault() + setTitle("pendingRename", false) + openTitleEditor() + return + } + if (title.pendingShare) { + event.preventDefault() + setTitle("pendingShare", false) + requestAnimationFrame(() => setShare({ open: true, dismiss: null })) + } }} > {language.t("common.rename")} + + { + setTitle({ pendingShare: true, menuOpen: false }) + }} + > + + {language.t("session.share.action.share")} + + + void archiveSession(id())}> {language.t("common.archive")} @@ -711,6 +797,104 @@ export function MessageTimeline(props: { + + more} + placement="bottom-end" + gutter={4} + modal={false} + onOpenChange={(open) => { + if (open) setShare("dismiss", null) + setShare("open", open) + }} + > + + { + setShare({ dismiss: "escape", open: false }) + event.preventDefault() + event.stopPropagation() + }} + onPointerDownOutside={() => { + setShare({ dismiss: "outside", open: false }) + }} + onFocusOutside={() => { + setShare({ dismiss: "outside", open: false }) + }} + onCloseAutoFocus={(event) => { + if (share.dismiss === "outside") event.preventDefault() + setShare("dismiss", null) + }} + > +
+
+
+ {language.t("session.share.popover.title")} +
+
+ {shareUrl() + ? language.t("session.share.popover.description.shared") + : language.t("session.share.popover.description.unshared")} +
+
+
+ + {req.share + ? language.t("session.share.action.publishing") + : language.t("session.share.action.publish")} + + } + > +
+ +
+ + +
+
+
+
+
+
+
+
)}