summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/src/components/session/session-header.tsx180
-rw-r--r--packages/app/src/pages/session.tsx66
2 files changed, 212 insertions, 34 deletions
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx
index 7070f0c93..4c709feef 100644
--- a/packages/app/src/components/session/session-header.tsx
+++ b/packages/app/src/components/session/session-header.tsx
@@ -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>
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index dbdbbc7eb..d76ff99b3 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -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) => {