Skip to content

Commit 06bc4dc

Browse files
feat(desktop): implement session unshare button (#8660)
1 parent 0ccf9bd commit 06bc4dc

File tree

2 files changed

+212
-34
lines changed

2 files changed

+212
-34
lines changed

packages/app/src/components/session/session-header.tsx

Lines changed: 146 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
import { createMemo, createResource, Show } from "solid-js"
1+
import { createEffect, createMemo, onCleanup, Show } from "solid-js"
2+
import { createStore } from "solid-js/store"
23
import { Portal } from "solid-js/web"
34
import { useParams } from "@solidjs/router"
45
import { useLayout } from "@/context/layout"
56
import { useCommand } from "@/context/command"
67
// import { useServer } from "@/context/server"
78
// import { useDialog } from "@opencode-ai/ui/context/dialog"
9+
import { usePlatform } from "@/context/platform"
810
import { useSync } from "@/context/sync"
911
import { useGlobalSDK } from "@/context/global-sdk"
1012
import { getFilename } from "@opencode-ai/util/path"
1113
import { base64Decode } from "@opencode-ai/util/encode"
12-
import { iife } from "@opencode-ai/util/iife"
14+
1315
import { Icon } from "@opencode-ai/ui/icon"
1416
import { IconButton } from "@opencode-ai/ui/icon-button"
1517
import { Button } from "@opencode-ai/ui/button"
@@ -26,6 +28,7 @@ export function SessionHeader() {
2628
// const server = useServer()
2729
// const dialog = useDialog()
2830
const sync = useSync()
31+
const platform = usePlatform()
2932

3033
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
3134
const project = createMemo(() => {
@@ -45,6 +48,78 @@ export function SessionHeader() {
4548
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
4649
const view = createMemo(() => layout.view(sessionKey()))
4750

51+
const [state, setState] = createStore({
52+
share: false,
53+
unshare: false,
54+
copied: false,
55+
timer: undefined as number | undefined,
56+
})
57+
const shareUrl = createMemo(() => currentSession()?.share?.url)
58+
59+
createEffect(() => {
60+
const url = shareUrl()
61+
if (url) return
62+
if (state.timer) window.clearTimeout(state.timer)
63+
setState({ copied: false, timer: undefined })
64+
})
65+
66+
onCleanup(() => {
67+
if (state.timer) window.clearTimeout(state.timer)
68+
})
69+
70+
function shareSession() {
71+
const session = currentSession()
72+
if (!session || state.share) return
73+
setState("share", true)
74+
globalSDK.client.session
75+
.share({ sessionID: session.id, directory: projectDirectory() })
76+
.catch((error) => {
77+
console.error("Failed to share session", error)
78+
})
79+
.finally(() => {
80+
setState("share", false)
81+
})
82+
}
83+
84+
function unshareSession() {
85+
const session = currentSession()
86+
if (!session || state.unshare) return
87+
setState("unshare", true)
88+
globalSDK.client.session
89+
.unshare({ sessionID: session.id, directory: projectDirectory() })
90+
.catch((error) => {
91+
console.error("Failed to unshare session", error)
92+
})
93+
.finally(() => {
94+
setState("unshare", false)
95+
})
96+
}
97+
98+
function copyLink() {
99+
const url = shareUrl()
100+
if (!url) return
101+
navigator.clipboard
102+
.writeText(url)
103+
.then(() => {
104+
if (state.timer) window.clearTimeout(state.timer)
105+
setState("copied", true)
106+
const timer = window.setTimeout(() => {
107+
setState("copied", false)
108+
setState("timer", undefined)
109+
}, 3000)
110+
setState("timer", timer)
111+
})
112+
.catch((error) => {
113+
console.error("Failed to copy share link", error)
114+
})
115+
}
116+
117+
function viewShare() {
118+
const url = shareUrl()
119+
if (!url) return
120+
platform.openLink(url)
121+
}
122+
48123
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
49124
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
50125

@@ -159,40 +234,77 @@ export function SessionHeader() {
159234
</TooltipKeybind>
160235
</div>
161236
<Show when={shareEnabled() && currentSession()}>
162-
<Popover
163-
title="Share session"
164-
trigger={
165-
<Tooltip class="shrink-0" value="Share session">
166-
<IconButton icon="share" variant="ghost" class="" />
167-
</Tooltip>
168-
}
169-
>
170-
{iife(() => {
171-
const [url] = createResource(
172-
() => currentSession(),
173-
async (session) => {
174-
if (!session) return
175-
let shareURL = session.share?.url
176-
if (!shareURL) {
177-
shareURL = await globalSDK.client.session
178-
.share({ sessionID: session.id, directory: projectDirectory() })
179-
.then((r) => r.data?.share?.url)
180-
.catch((e) => {
181-
console.error("Failed to share session", e)
182-
return undefined
183-
})
237+
<div class="flex items-center">
238+
<Popover
239+
title="Publish on web"
240+
description={
241+
shareUrl()
242+
? "This session is public on the web. It is accessible to anyone with the link."
243+
: "Share session publicly on the web. It will be accessible to anyone with the link."
244+
}
245+
trigger={
246+
<Tooltip class="shrink-0" value="Share session">
247+
<Button variant="secondary" classList={{ "rounded-r-none": shareUrl() !== undefined }}>
248+
Share
249+
</Button>
250+
</Tooltip>
251+
}
252+
>
253+
<div class="flex flex-col gap-2">
254+
<Show
255+
when={shareUrl()}
256+
fallback={
257+
<div class="flex">
258+
<Button
259+
size="large"
260+
variant="primary"
261+
class="w-1/2"
262+
onClick={shareSession}
263+
disabled={state.share}
264+
>
265+
{state.share ? "Publishing..." : "Publish"}
266+
</Button>
267+
</div>
184268
}
185-
return shareURL
186-
},
187-
{ initialValue: "" },
188-
)
189-
return (
190-
<Show when={url.latest}>
191-
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
269+
>
270+
<div class="flex flex-col gap-2 w-72">
271+
<TextField value={shareUrl() ?? ""} readOnly copyable class="w-full" />
272+
<div class="grid grid-cols-2 gap-2">
273+
<Button
274+
size="large"
275+
variant="secondary"
276+
class="w-full shadow-none border border-border-weak-base"
277+
onClick={unshareSession}
278+
disabled={state.unshare}
279+
>
280+
{state.unshare ? "Unpublishing..." : "Unpublish"}
281+
</Button>
282+
<Button
283+
size="large"
284+
variant="primary"
285+
class="w-full"
286+
onClick={viewShare}
287+
disabled={state.unshare}
288+
>
289+
View
290+
</Button>
291+
</div>
292+
</div>
192293
</Show>
193-
)
194-
})}
195-
</Popover>
294+
</div>
295+
</Popover>
296+
<Show when={shareUrl()}>
297+
<Tooltip value={state.copied ? "Copied" : "Copy link"} placement="top" gutter={8}>
298+
<IconButton
299+
icon={state.copied ? "check" : "copy"}
300+
variant="secondary"
301+
class="rounded-l-none border-l border-border-weak-base"
302+
onClick={copyLink}
303+
disabled={state.unshare}
304+
/>
305+
</Tooltip>
306+
</Show>
307+
</div>
196308
</Show>
197309
</div>
198310
</Portal>

packages/app/src/pages/session.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,72 @@ export default function Page() {
654654
disabled: !params.id || visibleUserMessages().length === 0,
655655
onSelect: () => dialog.show(() => <DialogFork />),
656656
},
657+
...(sync.data.config.share !== "disabled"
658+
? [
659+
{
660+
id: "session.share",
661+
title: "Share session",
662+
description: "Share this session and copy the URL to clipboard",
663+
category: "Session",
664+
slash: "share",
665+
disabled: !params.id || !!info()?.share?.url,
666+
onSelect: async () => {
667+
if (!params.id) return
668+
await sdk.client.session
669+
.share({ sessionID: params.id })
670+
.then((res) => {
671+
navigator.clipboard.writeText(res.data!.share!.url).catch(() =>
672+
showToast({
673+
title: "Failed to copy URL to clipboard",
674+
variant: "error",
675+
}),
676+
)
677+
})
678+
.then(() =>
679+
showToast({
680+
title: "Session shared",
681+
description: "Share URL copied to clipboard!",
682+
variant: "success",
683+
}),
684+
)
685+
.catch(() =>
686+
showToast({
687+
title: "Failed to share session",
688+
description: "An error occurred while sharing the session",
689+
variant: "error",
690+
}),
691+
)
692+
},
693+
},
694+
{
695+
id: "session.unshare",
696+
title: "Unshare session",
697+
description: "Stop sharing this session",
698+
category: "Session",
699+
slash: "unshare",
700+
disabled: !params.id || !info()?.share?.url,
701+
onSelect: async () => {
702+
if (!params.id) return
703+
await sdk.client.session
704+
.unshare({ sessionID: params.id })
705+
.then(() =>
706+
showToast({
707+
title: "Session unshared",
708+
description: "Session unshared successfully!",
709+
variant: "success",
710+
}),
711+
)
712+
.catch(() =>
713+
showToast({
714+
title: "Failed to unshare session",
715+
description: "An error occurred while unsharing the session",
716+
variant: "error",
717+
}),
718+
)
719+
},
720+
},
721+
]
722+
: []),
657723
])
658724

659725
const handleKeyDown = (event: KeyboardEvent) => {

0 commit comments

Comments
 (0)