From d628e1f20a8d92ef33adea4b13f46bf069354c86 Mon Sep 17 00:00:00 2001 From: Tim O'Farrell Date: Mon, 29 Dec 2025 12:04:06 -0700 Subject: [PATCH] feat: Add frontend support for public conversation sharing (#12047) Co-authored-by: openhands Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com> --- .../v1-conversation-service.api.ts | 17 +++ .../v1-conversation-service.types.ts | 1 + frontend/src/api/open-hands.types.ts | 1 + .../api/shared-conversation-service.api.ts | 60 ++++++++++ .../conversation-name-context-menu.tsx | 42 +++++++ .../conversation/conversation-name.tsx | 13 ++ .../use-update-conversation-public-flag.ts | 70 +++++++++++ .../query/use-shared-conversation-events.ts | 17 +++ .../hooks/query/use-shared-conversation.ts | 15 +++ .../use-conversation-name-context-menu.ts | 40 ++++++- frontend/src/i18n/declaration.ts | 7 ++ frontend/src/i18n/translation.json | 112 ++++++++++++++++++ frontend/src/routes.ts | 5 + frontend/src/routes/shared-conversation.tsx | 103 ++++++++++++++++ frontend/src/utils/feature-flags.ts | 2 + 15 files changed, 503 insertions(+), 2 deletions(-) create mode 100644 frontend/src/api/shared-conversation-service.api.ts create mode 100644 frontend/src/hooks/mutation/use-update-conversation-public-flag.ts create mode 100644 frontend/src/hooks/query/use-shared-conversation-events.ts create mode 100644 frontend/src/hooks/query/use-shared-conversation.ts create mode 100644 frontend/src/routes/shared-conversation.tsx diff --git a/frontend/src/api/conversation-service/v1-conversation-service.api.ts b/frontend/src/api/conversation-service/v1-conversation-service.api.ts index 25aa4a1130..e9e4315397 100644 --- a/frontend/src/api/conversation-service/v1-conversation-service.api.ts +++ b/frontend/src/api/conversation-service/v1-conversation-service.api.ts @@ -298,6 +298,23 @@ class V1ConversationService { return data; } + /** + * Update a V1 conversation's public flag + * @param conversationId The conversation ID + * @param isPublic Whether the conversation should be public + * @returns Updated conversation info + */ + static async updateConversationPublicFlag( + conversationId: string, + isPublic: boolean, + ): Promise { + const { data } = await openHands.patch( + `/api/v1/app-conversations/${conversationId}`, + { public: isPublic }, + ); + return data; + } + /** * Read a file from a specific conversation's sandbox workspace * @param conversationId The conversation ID diff --git a/frontend/src/api/conversation-service/v1-conversation-service.types.ts b/frontend/src/api/conversation-service/v1-conversation-service.types.ts index 7c8b04ccbf..8aa5ed105e 100644 --- a/frontend/src/api/conversation-service/v1-conversation-service.types.ts +++ b/frontend/src/api/conversation-service/v1-conversation-service.types.ts @@ -98,6 +98,7 @@ export interface V1AppConversation { execution_status: V1ConversationExecutionStatus | null; conversation_url: string | null; session_api_key: string | null; + public?: boolean; } export interface Skill { diff --git a/frontend/src/api/open-hands.types.ts b/frontend/src/api/open-hands.types.ts index 47d34fe567..02247c57d9 100644 --- a/frontend/src/api/open-hands.types.ts +++ b/frontend/src/api/open-hands.types.ts @@ -78,6 +78,7 @@ export interface Conversation { pr_number?: number[] | null; conversation_version?: "V0" | "V1"; sub_conversation_ids?: string[]; + public?: boolean; } export interface ResultSet { diff --git a/frontend/src/api/shared-conversation-service.api.ts b/frontend/src/api/shared-conversation-service.api.ts new file mode 100644 index 0000000000..6b4a9e0b2b --- /dev/null +++ b/frontend/src/api/shared-conversation-service.api.ts @@ -0,0 +1,60 @@ +import { OpenHandsEvent } from "#/types/v1/core"; +import { openHands } from "./open-hands-axios"; + +export interface SharedConversation { + id: string; + created_by_user_id: string | null; + sandbox_id: string; + selected_repository: string | null; + selected_branch: string | null; + git_provider: string | null; + title: string | null; + pr_number: number[]; + llm_model: string | null; + metrics: unknown | null; + parent_conversation_id: string | null; + sub_conversation_ids: string[]; + created_at: string; + updated_at: string; +} + +export interface EventPage { + items: OpenHandsEvent[]; + next_page_id: string | null; +} + +export const sharedConversationService = { + /** + * Get a single shared conversation by ID + */ + async getSharedConversation( + conversationId: string, + ): Promise { + const response = await openHands.get<(SharedConversation | null)[]>( + "/api/shared-conversations", + { params: { ids: conversationId } }, + ); + return response.data[0] || null; + }, + + /** + * Get events for a shared conversation + */ + async getSharedConversationEvents( + conversationId: string, + limit: number = 100, + pageId?: string, + ): Promise { + const response = await openHands.get( + "/api/shared-events/search", + { + params: { + conversation_id: conversationId, + limit, + ...(pageId && { page_id: pageId }), + }, + }, + ); + return response.data; + }, +}; diff --git a/frontend/src/components/features/conversation/conversation-name-context-menu.tsx b/frontend/src/components/features/conversation/conversation-name-context-menu.tsx index 672c10cb6b..c66233c19d 100644 --- a/frontend/src/components/features/conversation/conversation-name-context-menu.tsx +++ b/frontend/src/components/features/conversation/conversation-name-context-menu.tsx @@ -7,6 +7,7 @@ import { ContextMenuListItem } from "../context-menu/context-menu-list-item"; import { Divider } from "#/ui/divider"; import { I18nKey } from "#/i18n/declaration"; import { useActiveConversation } from "#/hooks/query/use-active-conversation"; +import { useConfig } from "#/hooks/query/use-config"; import EditIcon from "#/icons/u-edit.svg?react"; import RobotIcon from "#/icons/u-robot.svg?react"; @@ -16,6 +17,7 @@ import DownloadIcon from "#/icons/u-download.svg?react"; import CreditCardIcon from "#/icons/u-credit-card.svg?react"; import CloseIcon from "#/icons/u-close.svg?react"; import DeleteIcon from "#/icons/u-delete.svg?react"; +import LinkIcon from "#/icons/link-external.svg?react"; import { ConversationNameContextMenuIconText } from "./conversation-name-context-menu-icon-text"; import { CONTEXT_MENU_ICON_TEXT_CLASSNAME } from "#/utils/constants"; @@ -34,7 +36,9 @@ interface ConversationNameContextMenuProps { onShowSkills?: (event: React.MouseEvent) => void; onExportConversation?: (event: React.MouseEvent) => void; onDownloadViaVSCode?: (event: React.MouseEvent) => void; + onTogglePublic?: (event: React.MouseEvent) => void; onDownloadConversation?: (event: React.MouseEvent) => void; + onCopyShareLink?: (event: React.MouseEvent) => void; position?: "top" | "bottom"; } @@ -48,7 +52,9 @@ export function ConversationNameContextMenu({ onShowSkills, onExportConversation, onDownloadViaVSCode, + onTogglePublic, onDownloadConversation, + onCopyShareLink, position = "bottom", }: ConversationNameContextMenuProps) { const { width } = useWindowSize(); @@ -56,10 +62,16 @@ export function ConversationNameContextMenu({ const { t } = useTranslation(); const ref = useClickOutsideElement(onClose); const { data: conversation } = useActiveConversation(); + const { data: config } = useConfig(); // This is a temporary measure and may be re-enabled in the future const isV1Conversation = conversation?.conversation_version === "V1"; + // Check if we should show the public sharing option + // Only show for V1 conversations in SAAS mode + const shouldShowPublicSharing = + isV1Conversation && config?.APP_MODE === "saas" && onTogglePublic; + const hasDownload = Boolean(onDownloadViaVSCode || onDownloadConversation); const hasExport = Boolean(onExportConversation); const hasTools = Boolean(onShowAgentTools || onShowSkills); @@ -182,6 +194,36 @@ export function ConversationNameContextMenu({ )} + {shouldShowPublicSharing && ( + +
+
+ + {t(I18nKey.CONVERSATION$SHARE_PUBLICLY)} +
+ {conversation?.public && onCopyShareLink && ( + + )} +
+
+ )} + {onStop && ( { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: (variables: { conversationId: string; isPublic: boolean }) => + V1ConversationService.updateConversationPublicFlag( + variables.conversationId, + variables.isPublic, + ), + onMutate: async (variables) => { + // Cancel any outgoing refetches + await queryClient.cancelQueries({ + queryKey: ["user", "conversation", variables.conversationId], + }); + + // Snapshot the previous value + const previousConversation = queryClient.getQueryData([ + "user", + "conversation", + variables.conversationId, + ]); + + // Optimistically update the conversation + queryClient.setQueryData( + ["user", "conversation", variables.conversationId], + (old: unknown) => + old && typeof old === "object" + ? { ...old, public: variables.isPublic } + : old, + ); + + return { previousConversation }; + }, + onError: (err, variables, context) => { + // Rollback on error + if (context?.previousConversation) { + queryClient.setQueryData( + ["user", "conversation", variables.conversationId], + context.previousConversation, + ); + } + displayErrorToast( + t(I18nKey.CONVERSATION$FAILED_TO_UPDATE_PUBLIC_SHARING), + ); + }, + onSuccess: () => { + displaySuccessToast(t(I18nKey.CONVERSATION$PUBLIC_SHARING_UPDATED)); + }, + onSettled: (data, error, variables) => { + // Always refetch after error or success + queryClient.invalidateQueries({ + queryKey: ["user", "conversation", variables.conversationId], + }); + // Also invalidate the conversations list to update any cached data + queryClient.invalidateQueries({ + queryKey: ["user", "conversations"], + }); + }, + }); +}; diff --git a/frontend/src/hooks/query/use-shared-conversation-events.ts b/frontend/src/hooks/query/use-shared-conversation-events.ts new file mode 100644 index 0000000000..10c3379783 --- /dev/null +++ b/frontend/src/hooks/query/use-shared-conversation-events.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { sharedConversationService } from "#/api/shared-conversation-service.api"; + +export const useSharedConversationEvents = (conversationId?: string) => + useQuery({ + queryKey: ["shared-conversation-events", conversationId], + queryFn: () => { + if (!conversationId) { + throw new Error("Conversation ID is required"); + } + return sharedConversationService.getSharedConversationEvents( + conversationId, + ); + }, + enabled: !!conversationId, + retry: false, // Don't retry for shared conversations + }); diff --git a/frontend/src/hooks/query/use-shared-conversation.ts b/frontend/src/hooks/query/use-shared-conversation.ts new file mode 100644 index 0000000000..9fd6b51ce3 --- /dev/null +++ b/frontend/src/hooks/query/use-shared-conversation.ts @@ -0,0 +1,15 @@ +import { useQuery } from "@tanstack/react-query"; +import { sharedConversationService } from "#/api/shared-conversation-service.api"; + +export const useSharedConversation = (conversationId?: string) => + useQuery({ + queryKey: ["shared-conversation", conversationId], + queryFn: () => { + if (!conversationId) { + throw new Error("Conversation ID is required"); + } + return sharedConversationService.getSharedConversation(conversationId); + }, + enabled: !!conversationId, + retry: false, // Don't retry for shared conversations + }); diff --git a/frontend/src/hooks/use-conversation-name-context-menu.ts b/frontend/src/hooks/use-conversation-name-context-menu.ts index 0bc43bd4b6..f81ef8e8bc 100644 --- a/frontend/src/hooks/use-conversation-name-context-menu.ts +++ b/frontend/src/hooks/use-conversation-name-context-menu.ts @@ -10,8 +10,12 @@ import ConversationService from "#/api/conversation-service/conversation-service import { useDeleteConversation } from "./mutation/use-delete-conversation"; import { useUnifiedPauseConversationSandbox } from "./mutation/use-unified-stop-conversation"; import { useGetTrajectory } from "./mutation/use-get-trajectory"; +import { useUpdateConversationPublicFlag } from "./mutation/use-update-conversation-public-flag"; import { downloadTrajectory } from "#/utils/download-trajectory"; -import { displayErrorToast } from "#/utils/custom-toast-handlers"; +import { + displayErrorToast, + displaySuccessToast, +} from "#/utils/custom-toast-handlers"; import { I18nKey } from "#/i18n/declaration"; import { useEventStore } from "#/stores/use-event-store"; import { isV0Event } from "#/types/v1/type-guards"; @@ -36,10 +40,11 @@ export function useConversationNameContextMenu({ const { conversationId: currentConversationId } = useParams(); const navigate = useNavigate(); const events = useEventStore((state) => state.events); - const { data: conversation } = useActiveConversation(); const { mutate: deleteConversation } = useDeleteConversation(); const { mutate: stopConversation } = useUnifiedPauseConversationSandbox(); const { mutate: getTrajectory } = useGetTrajectory(); + const { mutate: updatePublicFlag } = useUpdateConversationPublicFlag(); + const { data: conversation } = useActiveConversation(); const metrics = useMetricsStore(); const [metricsModalVisible, setMetricsModalVisible] = React.useState(false); @@ -181,6 +186,35 @@ export function useConversationNameContextMenu({ onContextMenuToggle?.(false); }; + const handleTogglePublic = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + if (conversationId && conversation) { + // Toggle the current public state + const newPublicState = !conversation.public; + updatePublicFlag({ + conversationId, + isPublic: newPublicState, + }); + } + + onContextMenuToggle?.(false); + }; + + const handleCopyShareLink = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + if (conversationId) { + const shareUrl = `${window.location.origin}/shared/conversations/${conversationId}`; + navigator.clipboard.writeText(shareUrl); + displaySuccessToast(t(I18nKey.CONVERSATION$LINK_COPIED)); + } + + onContextMenuToggle?.(false); + }; + return { // Handlers handleDelete, @@ -192,6 +226,8 @@ export function useConversationNameContextMenu({ handleDisplayCost, handleShowAgentTools, handleShowSkills, + handleTogglePublic, + handleCopyShareLink, handleConfirmDelete, handleConfirmStop, diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts index 10189cb08e..1c6b0c618d 100644 --- a/frontend/src/i18n/declaration.ts +++ b/frontend/src/i18n/declaration.ts @@ -965,4 +965,11 @@ export enum I18nKey { OBSERVATION_MESSAGE$SKILL_READY = "OBSERVATION_MESSAGE$SKILL_READY", CONVERSATION$SHOW_SKILLS = "CONVERSATION$SHOW_SKILLS", SKILLS_MODAL$TITLE = "SKILLS_MODAL$TITLE", + CONVERSATION$SHARE_PUBLICLY = "CONVERSATION$SHARE_PUBLICLY", + CONVERSATION$PUBLIC_SHARING_UPDATED = "CONVERSATION$PUBLIC_SHARING_UPDATED", + CONVERSATION$FAILED_TO_UPDATE_PUBLIC_SHARING = "CONVERSATION$FAILED_TO_UPDATE_PUBLIC_SHARING", + CONVERSATION$NOT_FOUND = "CONVERSATION$NOT_FOUND", + CONVERSATION$NO_HISTORY_AVAILABLE = "CONVERSATION$NO_HISTORY_AVAILABLE", + CONVERSATION$SHARED_CONVERSATION = "CONVERSATION$SHARED_CONVERSATION", + CONVERSATION$LINK_COPIED = "CONVERSATION$LINK_COPIED", } diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index 12a860cd01..39dd9d0f78 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -15438,5 +15438,117 @@ "es": "Habilidades disponibles", "tr": "Kullanılabilir yetenekler", "uk": "Доступні навички" + }, + "CONVERSATION$SHARE_PUBLICLY": { + "en": "Public Share", + "ja": "公開で共有", + "zh-CN": "公开分享", + "zh-TW": "公開分享", + "ko-KR": "공개적으로 공유", + "no": "Del offentlig", + "it": "Condividi pubblicamente", + "pt": "Compartilhar publicamente", + "es": "Compartir públicamente", + "ar": "مشاركة عامة", + "fr": "Partager publiquement", + "tr": "Herkese açık paylaş", + "de": "Öffentlich teilen", + "uk": "Поділитися публічно" + }, + "CONVERSATION$PUBLIC_SHARING_UPDATED": { + "en": "Public sharing updated", + "ja": "公開共有が更新されました", + "zh-CN": "公开分享已更新", + "zh-TW": "公開分享已更新", + "ko-KR": "공개 공유가 업데이트되었습니다", + "no": "Offentlig deling oppdatert", + "it": "Condivisione pubblica aggiornata", + "pt": "Compartilhamento público atualizado", + "es": "Compartir público actualizado", + "ar": "تم تحديث المشاركة العامة", + "fr": "Partage public mis à jour", + "tr": "Herkese açık paylaşım güncellendi", + "de": "Öffentliche Freigabe aktualisiert", + "uk": "Публічний доступ оновлено" + }, + "CONVERSATION$FAILED_TO_UPDATE_PUBLIC_SHARING": { + "en": "Failed to update public sharing", + "ja": "公開共有の更新に失敗しました", + "zh-CN": "更新公开分享失败", + "zh-TW": "更新公開分享失敗", + "ko-KR": "공개 공유 업데이트에 실패했습니다", + "no": "Kunne ikke oppdatere offentlig deling", + "it": "Impossibile aggiornare la condivisione pubblica", + "pt": "Falha ao atualizar compartilhamento público", + "es": "Error al actualizar compartir público", + "ar": "فشل في تحديث المشاركة العامة", + "fr": "Échec de la mise à jour du partage public", + "tr": "Herkese açık paylaşım güncellenemedi", + "de": "Fehler beim Aktualisieren der öffentlichen Freigabe", + "uk": "Не вдалося оновити публічний доступ" + }, + "CONVERSATION$NOT_FOUND": { + "en": "Conversation not found", + "ja": "会話が見つかりません", + "zh-CN": "未找到对话", + "zh-TW": "找不到對話", + "ko-KR": "대화를 찾을 수 없습니다", + "no": "Samtale ikke funnet", + "it": "Conversazione non trovata", + "pt": "Conversa não encontrada", + "es": "Conversación no encontrada", + "ar": "المحادثة غير موجودة", + "fr": "Conversation introuvable", + "tr": "Konuşma bulunamadı", + "de": "Unterhaltung nicht gefunden", + "uk": "Розмову не знайдено" + }, + "CONVERSATION$NO_HISTORY_AVAILABLE": { + "en": "No conversation history available", + "ja": "会話履歴がありません", + "zh-CN": "没有可用的对话历史", + "zh-TW": "沒有可用的對話歷史", + "ko-KR": "사용 가능한 대화 기록이 없습니다", + "no": "Ingen samtalehistorikk tilgjengelig", + "it": "Nessuna cronologia conversazione disponibile", + "pt": "Nenhum histórico de conversa disponível", + "es": "No hay historial de conversación disponible", + "ar": "لا يوجد تاريخ محادثة متاح", + "fr": "Aucun historique de conversation disponible", + "tr": "Kullanılabilir konuşma geçmişi yok", + "de": "Keine Unterhaltungshistorie verfügbar", + "uk": "Історія розмов недоступна" + }, + "CONVERSATION$SHARED_CONVERSATION": { + "en": "Shared Conversation", + "ja": "公開会話", + "zh-CN": "公开对话", + "zh-TW": "公開對話", + "ko-KR": "공개 대화", + "no": "Offentlig samtale", + "it": "Conversazione pubblica", + "pt": "Conversa pública", + "es": "Conversación pública", + "ar": "محادثة عامة", + "fr": "Conversation publique", + "tr": "Herkese açık konuşma", + "de": "Öffentliche Unterhaltung", + "uk": "Публічна розмова" + }, + "CONVERSATION$LINK_COPIED": { + "en": "Link copied to clipboard", + "ja": "リンクをクリップボードにコピーしました", + "zh-CN": "链接已复制到剪贴板", + "zh-TW": "連結已複製到剪貼簿", + "ko-KR": "링크가 클립보드에 복사되었습니다", + "no": "Lenke kopiert til utklippstavle", + "it": "Link copiato negli appunti", + "pt": "Link copiado para a área de transferência", + "es": "Enlace copiado al portapapeles", + "ar": "تم نسخ الرابط إلى الحافظة", + "fr": "Lien copié dans le presse-papiers", + "tr": "Bağlantı panoya kopyalandı", + "de": "Link in die Zwischenablage kopiert", + "uk": "Посилання скопійовано в буфер обміну" } } diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index ecee511688..76b58fffa8 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -23,4 +23,9 @@ export default [ route("microagent-management", "routes/microagent-management.tsx"), route("oauth/device/verify", "routes/device-verify.tsx"), ]), + // Shared routes that don't require authentication + route( + "shared/conversations/:conversationId", + "routes/shared-conversation.tsx", + ), ] satisfies RouteConfig; diff --git a/frontend/src/routes/shared-conversation.tsx b/frontend/src/routes/shared-conversation.tsx new file mode 100644 index 0000000000..7153a79aab --- /dev/null +++ b/frontend/src/routes/shared-conversation.tsx @@ -0,0 +1,103 @@ +import React from "react"; +import { Link, useParams } from "react-router"; +import { useTranslation } from "react-i18next"; +import { I18nKey } from "#/i18n/declaration"; +import { useSharedConversation } from "#/hooks/query/use-shared-conversation"; +import { useSharedConversationEvents } from "#/hooks/query/use-shared-conversation-events"; +import { Messages as V1Messages } from "#/components/v1/chat"; +import { shouldRenderEvent } from "#/components/v1/chat/event-content-helpers/should-render-event"; +import { LoadingSpinner } from "#/components/shared/loading-spinner"; +import OpenHandsLogo from "#/assets/branding/openhands-logo.svg?react"; + +export default function SharedConversation() { + const { t } = useTranslation(); + const { conversationId } = useParams<{ conversationId: string }>(); + + const { + data: conversation, + isLoading: isLoadingConversation, + error: conversationError, + } = useSharedConversation(conversationId); + const { + data: eventsData, + isLoading: isLoadingEvents, + error: eventsError, + } = useSharedConversationEvents(conversationId); + + const isLoading = isLoadingConversation || isLoadingEvents; + const error = conversationError || eventsError; + + // Transform shared events to V1 format + const v1Events = eventsData?.items || []; + + // Filter events that should be rendered + const renderableEvents = React.useMemo( + () => v1Events.filter(shouldRenderEvent), + [v1Events], + ); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error || !conversation) { + return ( +
+
{t(I18nKey.CONVERSATION$NOT_FOUND)}
+
+ ); + } + + return ( +
+ {/* Header with logo, conversation title and branch info */} +
+
+ + + +
+

+ {conversation?.title || + t(I18nKey.CONVERSATION$SHARED_CONVERSATION)} +

+ {conversation?.selected_branch && ( +
+ {t(I18nKey.CONVERSATION$BRANCH)}: {conversation.selected_branch} +
+ )} + {conversation?.selected_repository && ( +
+ {t(I18nKey.CONVERSATION$REPOSITORY)}:{" "} + {conversation.selected_repository} +
+ )} +
+
+
+ + {/* Chat panel - read-only */} +
+
+ {renderableEvents.length > 0 ? ( + + ) : ( +
+
+ {t(I18nKey.CONVERSATION$NO_HISTORY_AVAILABLE)} +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/src/utils/feature-flags.ts b/frontend/src/utils/feature-flags.ts index 0f38a4d7ea..cba6d7caf3 100644 --- a/frontend/src/utils/feature-flags.ts +++ b/frontend/src/utils/feature-flags.ts @@ -18,3 +18,5 @@ export const VSCODE_IN_NEW_TAB = () => loadFeatureFlag("VSCODE_IN_NEW_TAB"); export const ENABLE_TRAJECTORY_REPLAY = () => loadFeatureFlag("TRAJECTORY_REPLAY"); export const USE_PLANNING_AGENT = () => loadFeatureFlag("USE_PLANNING_AGENT"); +export const ENABLE_PUBLIC_CONVERSATION_SHARING = () => + loadFeatureFlag("PUBLIC_CONVERSATION_SHARING");