diff --git a/app/components/copilot/ChatSharePanel.tsx b/app/components/copilot/ChatSharePanel.tsx
new file mode 100644
index 0000000..6a04f19
--- /dev/null
+++ b/app/components/copilot/ChatSharePanel.tsx
@@ -0,0 +1,180 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import { requestJSON } from "@/app/lib/api";
+import { useAsyncState } from "@/app/lib/hooks";
+import { Pill } from "@/app/lib/ui";
+import {
+ buildChatSharePath,
+ buildChatShareTitle,
+ buildProviderSharePayload,
+ type ShareableChatTurn,
+} from "@/app/lib/chatShare";
+
+type ChatSharePanelProps = {
+ chatId: string;
+ turns: ShareableChatTurn[];
+};
+
+export function ChatSharePanel({ chatId, turns }: ChatSharePanelProps) {
+ const [shareChannel, setShareChannel] = useState("#ops");
+ const [linkStatus, setLinkStatus] = useState("");
+ const [shareNotice, setShareNotice] = useState("");
+ const [isOpen, setIsOpen] = useState(false);
+ const [showProviderForm, setShowProviderForm] = useState(false);
+ const shareState = useAsyncState();
+
+ const shareTitle = useMemo(() => buildChatShareTitle(turns), [turns]);
+
+ const getChatUrl = () => {
+ if (typeof window === "undefined") return "";
+ return `${window.location.origin}${buildChatSharePath(chatId)}`;
+ };
+
+ const copyShareLink = async () => {
+ try {
+ if (!navigator.clipboard) {
+ throw new Error("Clipboard is not available in this browser");
+ }
+ await navigator.clipboard.writeText(getChatUrl());
+ setLinkStatus("Link copied");
+ } catch (err) {
+ setLinkStatus(err instanceof Error ? err.message : "Failed to copy link");
+ }
+ };
+
+ const shareChatLink = async () => {
+ const chatUrl = getChatUrl();
+ if (!chatUrl) return;
+
+ if (typeof navigator !== "undefined" && typeof navigator.share === "function") {
+ try {
+ await navigator.share({
+ title: shareTitle,
+ text: "Open this OpsOrch Copilot chat",
+ url: chatUrl,
+ });
+ setLinkStatus("Share sheet opened");
+ return;
+ } catch (err) {
+ const errorName = err instanceof Error ? err.name : "";
+ if (errorName === "AbortError") {
+ setLinkStatus("");
+ return;
+ }
+ }
+ }
+
+ await copyShareLink();
+ };
+
+ const sendSharedChat = async () => {
+ if (!shareChannel.trim()) return;
+
+ shareState.start();
+ try {
+ const payload = buildProviderSharePayload({
+ chatId,
+ chatUrl: getChatUrl(),
+ turns,
+ });
+ await requestJSON("/messages/send", {
+ method: "POST",
+ body: JSON.stringify({
+ channel: shareChannel.trim(),
+ ...payload,
+ }),
+ });
+ shareState.succeed();
+ setShareNotice(`Sent to ${shareChannel.trim()}`);
+ setShowProviderForm(false);
+ } catch (err) {
+ shareState.fail(err);
+ setShareNotice("");
+ }
+ };
+
+ return (
+
(
+ () =>
+ turns.map((turn) => ({
+ role: turn.role,
+ text: turn.role === "copilot" ? turn.answer?.conclusion || turn.text : turn.text,
+ })),
+ [turns],
+ );
+ const canShare = Boolean(chatId) && shareableTurns.length > 0;
const ask = async () => {
const trimmed = question.trim();
@@ -191,6 +202,7 @@ export function CopilotPanel({ initialChatId }: { initialChatId?: string }) {
title="Copilot"
action={
+ {canShare && chatId ? : null}