From fe4d4a7a3aaf5b4b7f5100869241e9816a1f08a1 Mon Sep 17 00:00:00 2001 From: David Hill Date: Wed, 11 Mar 2026 16:06:00 +0000 Subject: [PATCH] feat(app): add shared link button in session header When a session is published, show a link icon next to context that copies the share URL with a transient check + tooltip, and animate the header controls to make room for it. --- .../src/pages/session/message-timeline.tsx | 84 ++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index aa540e6a93..41d99da96d 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -17,6 +17,7 @@ import { showToast } from "@opencode-ai/ui/toast" import { Binary } from "@opencode-ai/util/binary" import { getFilename } from "@opencode-ai/util/path" import { Popover as KobaltePopover } from "@kobalte/core/popover" +import { Tooltip } from "@opencode-ai/ui/tooltip" import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" import { SessionContextUsage } from "@/components/session-context-usage" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -300,6 +301,7 @@ export function MessageTimeline(props: { const titleValue = createMemo(() => info()?.title) const shareUrl = createMemo(() => info()?.share?.url) const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") + const shared = createMemo(() => shareEnabled() && !!shareUrl()) const parentID = createMemo(() => info()?.parentID) const showHeader = createMemo(() => !!(titleValue() || parentID())) const stageCfg = { init: 1, batch: 3 } @@ -325,6 +327,11 @@ export function MessageTimeline(props: { dismiss: null as "escape" | "outside" | null, }) + const [clip, setClip] = createStore({ + ok: false, + timer: undefined as number | undefined, + }) + let more: HTMLButtonElement | undefined const [req, setReq] = createStore({ share: false, unshare: false }) @@ -365,6 +372,42 @@ export function MessageTimeline(props: { platform.openLink(url) } + createEffect(() => { + const url = shareUrl() + if (url) return + if (clip.timer) window.clearTimeout(clip.timer) + setClip({ ok: false, timer: undefined }) + }) + + onCleanup(() => { + if (!clip.timer) return + window.clearTimeout(clip.timer) + }) + + const copy = () => { + const url = shareUrl() + if (!url) return + navigator.clipboard + .writeText(url) + .then(() => { + if (clip.timer) window.clearTimeout(clip.timer) + setClip("ok", true) + setClip( + "timer", + window.setTimeout(() => { + setClip({ ok: false, timer: undefined }) + }, 3000), + ) + }) + .catch((err: unknown) => + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: err instanceof Error ? err.message : String(err), + }), + ) + } + const errorMessage = (err: unknown) => { if (err && typeof err === "object" && "data" in err) { const data = (err as { data?: { message?: string } }).data @@ -728,8 +771,45 @@ export function MessageTimeline(props: { {(id) => ( -
- +
+
+ +
+
+
+ + + +
+