feat(desktop): implement session unshare button (#8660)

This commit is contained in:
Mani Sundararajan
2026-01-18 06:12:07 -05:00
committed by GitHub
parent 0ccf9bd9ac
commit 06bc4dcb06
2 changed files with 212 additions and 34 deletions

View File

@@ -1,15 +1,17 @@
import { createMemo, createResource, Show } from "solid-js"
import { createEffect, createMemo, onCleanup, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { Portal } from "solid-js/web"
import { useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
import { useCommand } from "@/context/command"
// import { useServer } from "@/context/server"
// import { useDialog } from "@opencode-ai/ui/context/dialog"
import { usePlatform } from "@/context/platform"
import { useSync } from "@/context/sync"
import { useGlobalSDK } from "@/context/global-sdk"
import { getFilename } from "@opencode-ai/util/path"
import { base64Decode } from "@opencode-ai/util/encode"
import { iife } from "@opencode-ai/util/iife"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Button } from "@opencode-ai/ui/button"
@@ -26,6 +28,7 @@ export function SessionHeader() {
// const server = useServer()
// const dialog = useDialog()
const sync = useSync()
const platform = usePlatform()
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const project = createMemo(() => {
@@ -45,6 +48,78 @@ export function SessionHeader() {
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const view = createMemo(() => layout.view(sessionKey()))
const [state, setState] = createStore({
share: false,
unshare: false,
copied: false,
timer: undefined as number | undefined,
})
const shareUrl = createMemo(() => 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)
})
function shareSession() {
const session = currentSession()
if (!session || state.share) return
setState("share", true)
globalSDK.client.session
.share({ sessionID: session.id, directory: projectDirectory() })
.catch((error) => {
console.error("Failed to share session", error)
})
.finally(() => {
setState("share", false)
})
}
function unshareSession() {
const session = currentSession()
if (!session || state.unshare) return
setState("unshare", true)
globalSDK.client.session
.unshare({ sessionID: session.id, directory: projectDirectory() })
.catch((error) => {
console.error("Failed to unshare session", error)
})
.finally(() => {
setState("unshare", false)
})
}
function copyLink() {
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((error) => {
console.error("Failed to copy share link", error)
})
}
function viewShare() {
const url = shareUrl()
if (!url) return
platform.openLink(url)
}
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
@@ -159,40 +234,77 @@ export function SessionHeader() {
</TooltipKeybind>
</div>
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
</Tooltip>
}
>
{iife(() => {
const [url] = createResource(
() => currentSession(),
async (session) => {
if (!session) return
let shareURL = session.share?.url
if (!shareURL) {
shareURL = await globalSDK.client.session
.share({ sessionID: session.id, directory: projectDirectory() })
.then((r) => r.data?.share?.url)
.catch((e) => {
console.error("Failed to share session", e)
return undefined
})
<div class="flex items-center">
<Popover
title="Publish on web"
description={
shareUrl()
? "This session is public on the web. It is accessible to anyone with the link."
: "Share session publicly on the web. It will be accessible to anyone with the link."
}
trigger={
<Tooltip class="shrink-0" value="Share session">
<Button variant="secondary" classList={{ "rounded-r-none": shareUrl() !== undefined }}>
Share
</Button>
</Tooltip>
}
>
<div class="flex flex-col gap-2">
<Show
when={shareUrl()}
fallback={
<div class="flex">
<Button
size="large"
variant="primary"
class="w-1/2"
onClick={shareSession}
disabled={state.share}
>
{state.share ? "Publishing..." : "Publish"}
</Button>
</div>
}
return shareURL
},
{ initialValue: "" },
)
return (
<Show when={url.latest}>
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
>
<div class="flex flex-col gap-2 w-72">
<TextField value={shareUrl() ?? ""} readOnly copyable 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={unshareSession}
disabled={state.unshare}
>
{state.unshare ? "Unpublishing..." : "Unpublish"}
</Button>
<Button
size="large"
variant="primary"
class="w-full"
onClick={viewShare}
disabled={state.unshare}
>
View
</Button>
</div>
</div>
</Show>
)
})}
</Popover>
</div>
</Popover>
<Show when={shareUrl()}>
<Tooltip value={state.copied ? "Copied" : "Copy link"} placement="top" gutter={8}>
<IconButton
icon={state.copied ? "check" : "copy"}
variant="secondary"
class="rounded-l-none border-l border-border-weak-base"
onClick={copyLink}
disabled={state.unshare}
/>
</Tooltip>
</Show>
</div>
</Show>
</div>
</Portal>

View File

@@ -654,6 +654,72 @@ export default function Page() {
disabled: !params.id || visibleUserMessages().length === 0,
onSelect: () => dialog.show(() => <DialogFork />),
},
...(sync.data.config.share !== "disabled"
? [
{
id: "session.share",
title: "Share session",
description: "Share this session and copy the URL to clipboard",
category: "Session",
slash: "share",
disabled: !params.id || !!info()?.share?.url,
onSelect: async () => {
if (!params.id) return
await sdk.client.session
.share({ sessionID: params.id })
.then((res) => {
navigator.clipboard.writeText(res.data!.share!.url).catch(() =>
showToast({
title: "Failed to copy URL to clipboard",
variant: "error",
}),
)
})
.then(() =>
showToast({
title: "Session shared",
description: "Share URL copied to clipboard!",
variant: "success",
}),
)
.catch(() =>
showToast({
title: "Failed to share session",
description: "An error occurred while sharing the session",
variant: "error",
}),
)
},
},
{
id: "session.unshare",
title: "Unshare session",
description: "Stop sharing this session",
category: "Session",
slash: "unshare",
disabled: !params.id || !info()?.share?.url,
onSelect: async () => {
if (!params.id) return
await sdk.client.session
.unshare({ sessionID: params.id })
.then(() =>
showToast({
title: "Session unshared",
description: "Session unshared successfully!",
variant: "success",
}),
)
.catch(() =>
showToast({
title: "Failed to unshare session",
description: "An error occurred while unsharing the session",
variant: "error",
}),
)
},
},
]
: []),
])
const handleKeyDown = (event: KeyboardEvent) => {