diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 5b1c2f855ed..2b1e7381eb1 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -1,6 +1,8 @@ import { useEffect, useRef, useState } from 'react'; import { IpcRendererEvent } from 'electron'; import { HashRouter, Routes, Route, useNavigate, useLocation } from 'react-router-dom'; +import { openSharedSessionFromDeepLink, type SessionLinksViewOptions } from './sessionLinks'; +import { type SharedSessionDetails } from './sharedSessions'; import { ErrorUI } from './components/ErrorBoundary'; import { ExtensionInstallModal } from './components/ExtensionInstallModal'; import { ToastContainer } from 'react-toastify'; @@ -14,6 +16,7 @@ import Hub from './components/hub'; import Pair from './components/pair'; import SettingsView, { SettingsViewOptions } from './components/settings/SettingsView'; import SessionsView from './components/sessions/SessionsView'; +import SharedSessionView from './components/sessions/SharedSessionView'; import SchedulesView from './components/schedule/SchedulesView'; import ProviderSettings from './components/settings/providers/ProviderSettingsPage'; import { useChat } from './hooks/useChat'; @@ -315,6 +318,48 @@ const WelcomeRoute = () => { ); }; +// Wrapper component for SharedSessionRoute to access parent state +const SharedSessionRouteWrapper = ({ + isLoadingSharedSession, + setIsLoadingSharedSession, + sharedSessionError, +}: { + isLoadingSharedSession: boolean; + setIsLoadingSharedSession: (loading: boolean) => void; + sharedSessionError: string | null; +}) => { + const location = useLocation(); + const navigate = useNavigate(); + const setView = createNavigationHandler(navigate); + + const historyState = window.history.state; + const sessionDetails = (location.state?.sessionDetails || + historyState?.sessionDetails) as SharedSessionDetails | null; + const error = location.state?.error || historyState?.error || sharedSessionError; + const shareToken = location.state?.shareToken || historyState?.shareToken; + const baseUrl = location.state?.baseUrl || historyState?.baseUrl; + + return ( + { + if (shareToken && baseUrl) { + setIsLoadingSharedSession(true); + try { + await openSharedSessionFromDeepLink(`goose://sessions/${shareToken}`, setView, baseUrl); + } catch (error) { + console.error('Failed to retry loading shared session:', error); + } finally { + setIsLoadingSharedSession(false); + } + } + }} + /> + ); +}; + const ExtensionsRoute = () => { const navigate = useNavigate(); const location = useLocation(); @@ -353,6 +398,8 @@ export default function App() { const [isLoadingSession, setIsLoadingSession] = useState(false); const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false); const [agentWaitingMessage, setAgentWaitingMessage] = useState(null); + const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false); + const [sharedSessionError, setSharedSessionError] = useState(null); // Add separate state for pair chat to maintain its own conversation const [pairChat, setPairChat] = useState({ @@ -399,6 +446,9 @@ export default function App() { case 'ConfigureProviders': window.location.hash = '#/configure-providers'; break; + case 'sharedSession': + window.location.hash = '#/shared-session'; + break; case 'recipeEditor': window.location.hash = '#/recipe-editor'; break; @@ -476,6 +526,44 @@ export default function App() { } }, []); + useEffect(() => { + const handleOpenSharedSession = async (_event: IpcRendererEvent, ...args: unknown[]) => { + const link = args[0] as string; + window.electron.logInfo(`Opening shared session from deep link ${link}`); + setIsLoadingSharedSession(true); + setSharedSessionError(null); + try { + await openSharedSessionFromDeepLink( + link, + (_view: View, _options?: SessionLinksViewOptions) => { + // Navigate to shared session view with the session data + window.location.hash = '#/shared-session'; + if (_options) { + window.history.replaceState(_options, '', '#/shared-session'); + } + } + ); + } catch (error) { + console.error('Unexpected error opening shared session:', error); + // Navigate to shared session view with error + window.location.hash = '#/shared-session'; + const shareToken = link.replace('goose://sessions/', ''); + const options = { + sessionDetails: null, + error: error instanceof Error ? error.message : 'Unknown error', + shareToken, + }; + window.history.replaceState(options, '', '#/shared-session'); + } finally { + setIsLoadingSharedSession(false); + } + }; + window.electron.on('open-shared-session', handleOpenSharedSession); + return () => { + window.electron.off('open-shared-session', handleOpenSharedSession); + }; + }, []); + // Handle recipe decode events from main process useEffect(() => { const handleLoadRecipeDeeplink = (_event: IpcRendererEvent, ...args: unknown[]) => { @@ -793,6 +881,18 @@ export default function App() { } /> + + + + } + /> = ({ setIsGoosehintsModalOpen } case 'ConfigureProviders': navigate('/configure-providers'); break; - + case 'sharedSession': + navigate('/shared-session', { state: viewOptions }); + break; case 'recipeEditor': navigate('/recipe-editor', { state: viewOptions }); break; diff --git a/ui/desktop/src/components/sessions/SessionHistoryView.tsx b/ui/desktop/src/components/sessions/SessionHistoryView.tsx index 2f3832a3283..713f97d0d81 100644 --- a/ui/desktop/src/components/sessions/SessionHistoryView.tsx +++ b/ui/desktop/src/components/sessions/SessionHistoryView.tsx @@ -1,9 +1,12 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Calendar, MessageSquareText, Folder, + Share2, Sparkles, + Copy, + Check, Target, LoaderCircle, AlertCircle, @@ -14,11 +17,21 @@ import { toast } from 'react-toastify'; import { MainPanelLayout } from '../Layout/MainPanelLayout'; import { ScrollArea } from '../ui/scroll-area'; import { formatMessageTimestamp } from '../../utils/timeUtils'; +import { createSharedSession } from '../../sharedSessions'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '../ui/dialog'; import ProgressiveMessageList from '../ProgressiveMessageList'; import { SearchView } from '../conversation/SearchView'; import { ContextManagerProvider } from '../context_management/ContextManager'; import { Message } from '../../types/message'; import BackButton from '../ui/BackButton'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip'; // Helper function to determine if a message is a user message (same as useChatEngine) const isUserMessage = (message: Message): boolean => { @@ -137,6 +150,74 @@ const SessionHistoryView: React.FC = ({ onRetry, showActionButtons = true, }) => { + const [isShareModalOpen, setIsShareModalOpen] = useState(false); + const [shareLink, setShareLink] = useState(''); + const [isSharing, setIsSharing] = useState(false); + const [isCopied, setIsCopied] = useState(false); + const [canShare, setCanShare] = useState(false); + + useEffect(() => { + const savedSessionConfig = localStorage.getItem('session_sharing_config'); + if (savedSessionConfig) { + try { + const config = JSON.parse(savedSessionConfig); + if (config.enabled && config.baseUrl) { + setCanShare(true); + } + } catch (error) { + console.error('Error parsing session sharing config:', error); + } + } + }, []); + + const handleShare = async () => { + setIsSharing(true); + + try { + const savedSessionConfig = localStorage.getItem('session_sharing_config'); + if (!savedSessionConfig) { + throw new Error('Session sharing is not configured. Please configure it in settings.'); + } + + const config = JSON.parse(savedSessionConfig); + if (!config.enabled || !config.baseUrl) { + throw new Error('Session sharing is not enabled or base URL is not configured.'); + } + + const shareToken = await createSharedSession( + config.baseUrl, + session.metadata.working_dir, + session.messages, + session.metadata.description || 'Shared Session', + session.metadata.total_tokens + ); + + const shareableLink = `goose://sessions/${shareToken}`; + setShareLink(shareableLink); + setIsShareModalOpen(true); + } catch (error) { + console.error('Error sharing session:', error); + toast.error( + `Failed to share session: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } finally { + setIsSharing(false); + } + }; + + const handleCopyLink = () => { + navigator.clipboard + .writeText(shareLink) + .then(() => { + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + }) + .catch((err) => { + console.error('Failed to copy link:', err); + toast.error('Failed to copy link to clipboard'); + }); + }; + const handleLaunchInNewWindow = () => { if (session) { console.log('Launching session in new window:', session.session_id); @@ -168,63 +249,136 @@ const SessionHistoryView: React.FC = ({ // Define action buttons const actionButtons = showActionButtons ? ( - + <> + + + + + {!canShare ? ( + +

+ To enable session sharing, go to Settings {'>'} Session {'>'}{' '} + Session Sharing. +

+
+ ) : null} +
+ + ) : null; return ( - -
- -
- {!isLoading && session.messages.length > 0 ? ( - <> -
- - - {formatMessageTimestamp(session.messages[0]?.created)} - - - - {session.metadata.message_count} - - {session.metadata.total_tokens !== null && ( + <> + +
+ +
+ {!isLoading && session.messages.length > 0 ? ( + <> +
- - {session.metadata.total_tokens.toLocaleString()} + + {formatMessageTimestamp(session.messages[0]?.created)} - )} -
-
- - - {session.metadata.working_dir} - + + + {session.metadata.message_count} + + {session.metadata.total_tokens !== null && ( + + + {session.metadata.total_tokens.toLocaleString()} + + )} +
+
+ + + {session.metadata.working_dir} + +
+ + ) : ( +
+ + Loading session details...
- - ) : ( -
- - Loading session details... -
- )} + )} +
+
+ + +
+
+ + + + + + + Share Session (beta) + + + Share this session link to give others a read only view of your goose chat. + + + +
+
+ + {shareLink} + + +
- - - -
- + + + + + + + ); }; diff --git a/ui/desktop/src/components/sessions/SharedSessionView.tsx b/ui/desktop/src/components/sessions/SharedSessionView.tsx new file mode 100644 index 00000000000..1f39d5cd16e --- /dev/null +++ b/ui/desktop/src/components/sessions/SharedSessionView.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { Calendar, MessageSquareText, Folder, Target, LoaderCircle, Share2 } from 'lucide-react'; +import { type SharedSessionDetails } from '../../sharedSessions'; +import { SessionMessages } from './SessionViewComponents'; +import { formatMessageTimestamp } from '../../utils/timeUtils'; +import { MainPanelLayout } from '../Layout/MainPanelLayout'; + +interface SharedSessionViewProps { + session: SharedSessionDetails | null; + isLoading: boolean; + error: string | null; + onRetry: () => void; +} + +// Custom SessionHeader component matching SessionHistoryView style +const SessionHeader: React.FC<{ + children: React.ReactNode; + title: string; +}> = ({ children, title }) => { + return ( +
+

{title}

+
{children}
+
+ ); +}; + +const SharedSessionView: React.FC = ({ + session, + isLoading, + error, + onRetry, +}) => { + return ( + +
+
+
+ + Shared Session +
+
+ + +
+ {!isLoading && session && session.messages.length > 0 ? ( + <> +
+ + + {formatMessageTimestamp(session.messages[0]?.created)} + + + + {session.message_count} + + {session.total_tokens !== null && ( + + + {session.total_tokens.toLocaleString()} + + )} +
+
+ + + {session.working_dir} + +
+ + ) : ( +
+ + Loading session details... +
+ )} +
+
+ + +
+
+ ); +}; + +export default SharedSessionView; diff --git a/ui/desktop/src/components/settings/SettingsView.tsx b/ui/desktop/src/components/settings/SettingsView.tsx index dcbed3ab597..e414134e9c1 100644 --- a/ui/desktop/src/components/settings/SettingsView.tsx +++ b/ui/desktop/src/components/settings/SettingsView.tsx @@ -2,11 +2,12 @@ import { ScrollArea } from '../ui/scroll-area'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; import { View, ViewOptions } from '../../utils/navigationUtils'; import ModelsSection from './models/ModelsSection'; +import SessionSharingSection from './sessions/SessionSharingSection'; import AppSettingsSection from './app/AppSettingsSection'; import ConfigSettings from './config/ConfigSettings'; import { ExtensionConfig } from '../../api'; import { MainPanelLayout } from '../Layout/MainPanelLayout'; -import { Bot, Monitor, MessageSquare } from 'lucide-react'; +import { Bot, Share2, Monitor, MessageSquare } from 'lucide-react'; import { useState, useEffect } from 'react'; import ChatSettingsSection from './chat/ChatSettingsSection'; import { CONFIGURATION_ENABLED } from '../../updates'; @@ -36,6 +37,7 @@ export default function SettingsView({ update: 'app', models: 'models', modes: 'chat', + sharing: 'sharing', styles: 'chat', tools: 'chat', app: 'app', @@ -91,6 +93,14 @@ export default function SettingsView({ Chat + + + Session + App @@ -113,6 +123,13 @@ export default function SettingsView({ + + + + ({ status: null, message: '' }); + + // isUrlConfigured is true if the user has configured a baseUrl and it is valid. + const isUrlConfigured = + !envBaseUrlShare && + sessionSharingConfig.enabled && + isValidUrl(String(sessionSharingConfig.baseUrl)); + + // Only load saved config from localStorage if the env variable is not provided. + useEffect(() => { + if (envBaseUrlShare) { + // If env variable is set, save the forced configuration to localStorage + const forcedConfig = { + enabled: true, + baseUrl: typeof envBaseUrlShare === 'string' ? envBaseUrlShare : '', + }; + localStorage.setItem('session_sharing_config', JSON.stringify(forcedConfig)); + } else { + const savedSessionConfig = localStorage.getItem('session_sharing_config'); + if (savedSessionConfig) { + try { + const config = JSON.parse(savedSessionConfig); + setSessionSharingConfig(config); + } catch (error) { + console.error('Error parsing session sharing config:', error); + } + } + } + }, [envBaseUrlShare]); + + // Helper to check if the user's input is a valid URL + function isValidUrl(value: string): boolean { + if (!value) return false; + try { + new URL(value); + return true; + } catch { + return false; + } + } + + // Toggle sharing (only allowed when env is not set). + const toggleSharing = () => { + if (envBaseUrlShare) { + return; // Do nothing if the environment variable forces sharing. + } + setSessionSharingConfig((prev) => { + const updated = { ...prev, enabled: !prev.enabled }; + localStorage.setItem('session_sharing_config', JSON.stringify(updated)); + return updated; + }); + }; + + // Handle changes to the base URL field + const handleBaseUrlChange = (e: React.ChangeEvent) => { + const newBaseUrl = e.target.value; + setSessionSharingConfig((prev) => ({ + ...prev, + baseUrl: newBaseUrl, + })); + + // Clear previous test results when URL changes + setTestResult({ status: null, message: '' }); + + if (isValidUrl(newBaseUrl)) { + setUrlError(''); + const updated = { ...sessionSharingConfig, baseUrl: newBaseUrl }; + localStorage.setItem('session_sharing_config', JSON.stringify(updated)); + } else { + setUrlError('Invalid URL format. Please enter a valid URL (e.g. https://example.com/api).'); + } + }; + + // Test connection to the configured URL + const testConnection = async () => { + const baseUrl = sessionSharingConfig.baseUrl; + if (!baseUrl) return; + + setTestResult({ status: 'testing', message: 'Testing connection...' }); + + try { + // Create an AbortController for timeout + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => controller.abort(), 10000); // 10 second timeout + + const response = await fetch(baseUrl, { + method: 'GET', + headers: { + Accept: 'application/json, text/plain, */*', + }, + signal: controller.signal, + }); + + window.clearTimeout(timeoutId); + + // Consider any response (even 404) as a successful connection + // since it means we can reach the server + if (response.status < 500) { + setTestResult({ + status: 'success', + message: 'Connection successful!', + }); + } else { + setTestResult({ + status: 'error', + message: `Server error: HTTP ${response.status}. The server may not be configured correctly.`, + }); + } + } catch (error) { + console.error('Connection test failed:', error); + let errorMessage = 'Connection failed. '; + + if (error instanceof TypeError && error.message.includes('fetch')) { + errorMessage += + 'Unable to reach the server. Please check the URL and your network connection.'; + } else if (error instanceof Error) { + if (error.name === 'AbortError') { + errorMessage += 'Connection timed out. The server may be slow or unreachable.'; + } else { + errorMessage += error.message; + } + } else { + errorMessage += 'Unknown error occurred.'; + } + + setTestResult({ + status: 'error', + message: errorMessage, + }); + } + }; + + return ( +
+ + + Session Sharing + + {(envBaseUrlShare as string) + ? 'Session sharing is configured but fully opt-in — your sessions are only shared when you explicitly click the share button.' + : 'You can enable session sharing to share your sessions with others.'} + + + +
+ {/* Toggle for enabling session sharing */} +
+ + + {envBaseUrlShare ? ( + + ) : ( + + )} +
+ + {/* Base URL field (only visible if enabled) */} + {sessionSharingConfig.enabled && ( +
+
+ + {isUrlConfigured && } +
+
+ +
+ {urlError &&

{urlError}

} + + {(isUrlConfigured || (envBaseUrlShare as string)) && ( +
+ + + {/* Test Results */} + {testResult.status && testResult.status !== 'testing' && ( +
+ {testResult.status === 'success' ? ( + + ) : ( + + )} + {testResult.message} +
+ )} +
+ )} +
+ )} +
+
+
+
+ ); +} diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 8b8f870af7d..18658fbc723 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -297,6 +297,8 @@ async function processProtocolUrl(parsedUrl: URL, window: BrowserWindow) { if (parsedUrl.hostname === 'extension') { window.webContents.send('add-extension', pendingDeepLink); + } else if (parsedUrl.hostname === 'sessions') { + window.webContents.send('open-shared-session', pendingDeepLink); } else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') { const recipeDeeplink = parsedUrl.searchParams.get('config'); const scheduledJobId = parsedUrl.searchParams.get('scheduledJob'); @@ -359,6 +361,8 @@ app.on('open-url', async (_event, url) => { if (parsedUrl.hostname === 'extension') { firstOpenWindow.webContents.send('add-extension', pendingDeepLink); + } else if (parsedUrl.hostname === 'sessions') { + firstOpenWindow.webContents.send('open-shared-session', pendingDeepLink); } } }); @@ -461,7 +465,16 @@ const getGooseProvider = () => { ]; }; +const getSharingUrl = () => { + // checks app env for sharing url + loadShellEnv(app.isPackaged); // will try to take it from the zshrc file + // if GOOSE_BASE_URL_SHARE is found, we will set process.env.GOOSE_BASE_URL_SHARE, otherwise we return what it is set + // to in the env at bundle time + return process.env.GOOSE_BASE_URL_SHARE; +}; + const getVersion = () => { + // checks app env for sharing url loadShellEnv(app.isPackaged); // will try to take it from the zshrc file // to in the env at bundle time return process.env.GOOSE_VERSION; @@ -469,6 +482,8 @@ const getVersion = () => { const [provider, model, predefinedModels] = getGooseProvider(); +const sharingUrl = getSharingUrl(); + const gooseVersion = getVersion(); const SERVER_SECRET = process.env.GOOSE_EXTERNAL_BACKEND @@ -593,7 +608,7 @@ const createChat = async ( GOOSE_PORT: port, // Ensure this specific window gets the correct port GOOSE_WORKING_DIR: working_dir, REQUEST_DIR: dir, - + GOOSE_BASE_URL_SHARE: sharingUrl, GOOSE_VERSION: gooseVersion, recipe: recipe, }), @@ -648,7 +663,7 @@ const createChat = async ( GOOSE_PORT: port, // Ensure this specific window's config gets the correct port GOOSE_WORKING_DIR: working_dir, REQUEST_DIR: dir, - + GOOSE_BASE_URL_SHARE: sharingUrl, recipe: recipe, }; diff --git a/ui/desktop/src/sessionLinks.ts b/ui/desktop/src/sessionLinks.ts new file mode 100644 index 00000000000..565ee941748 --- /dev/null +++ b/ui/desktop/src/sessionLinks.ts @@ -0,0 +1,86 @@ +import { fetchSharedSessionDetails, SharedSessionDetails } from './sharedSessions'; +import { View } from './utils/navigationUtils'; + +export interface SessionLinksViewOptions { + sessionDetails?: SharedSessionDetails | null; + error?: string; + shareToken?: string; + baseUrl?: string; + + [key: string]: unknown; +} + +/** + * Handles opening a shared session from a deep link + * @param url The deep link URL (goose://sessions/:shareToken) + * @param setView Function to set the current view + * @param baseUrl Optional base URL for the session sharing API + * @returns Promise that resolves when the session is opened + */ +export async function openSharedSessionFromDeepLink( + url: string, + setView: (view: View, options?: SessionLinksViewOptions) => void, + baseUrl?: string +): Promise { + try { + if (!url.startsWith('goose://sessions/')) { + throw new Error('Invalid URL: URL must use the goose://sessions/ scheme'); + } + + // Extract the share token from the URL + const shareToken: string = url.replace('goose://sessions/', ''); + + if (!shareToken || shareToken.trim() === '') { + throw new Error('Invalid URL: Missing share token'); + } + + // If no baseUrl is provided, check if there's one in localStorage + if (!baseUrl) { + const savedSessionConfig = localStorage.getItem('session_sharing_config'); + if (savedSessionConfig) { + try { + const config = JSON.parse(savedSessionConfig); + if (config.enabled && config.baseUrl) { + baseUrl = config.baseUrl; + } else { + throw new Error( + 'Session sharing is not enabled or base URL is not configured. Check the settings page.' + ); + } + } catch (error) { + console.error('Error parsing session sharing config:', error); + throw new Error( + 'Session sharing is not enabled or base URL is not configured. Check the settings page.' + ); + } + } else { + throw new Error('Session sharing is not configured'); + } + } + + // Fetch the shared session details + const sessionDetails = await fetchSharedSessionDetails(baseUrl!, shareToken); + + // Navigate to the shared session view + setView('sharedSession', { + sessionDetails, + shareToken, + baseUrl, + }); + + return sessionDetails; + } catch (error) { + const errorMessage = `Failed to open shared session: ${error instanceof Error ? error.message : 'Unknown error'}`; + console.error(errorMessage); + + // Navigate to the shared session view with the error instead of throwing + setView('sharedSession', { + sessionDetails: null, + error: error instanceof Error ? error.message : 'Unknown error', + shareToken: url.replace('goose://sessions/', ''), + baseUrl, + }); + + return null; + } +} diff --git a/ui/desktop/src/sharedSessions.ts b/ui/desktop/src/sharedSessions.ts new file mode 100644 index 00000000000..2dd469aa2ad --- /dev/null +++ b/ui/desktop/src/sharedSessions.ts @@ -0,0 +1,114 @@ +import { Message } from './types/message'; +import { safeJsonParse } from './utils/jsonUtils'; + +export interface SharedSessionDetails { + share_token: string; + created_at: number; + base_url: string; + description: string; + working_dir: string; + messages: Message[]; + message_count: number; + total_tokens: number | null; +} + +/** + * Fetches details for a specific shared session + * @param baseUrl The base URL for session sharing API + * @param shareToken The share token of the session to fetch + * @returns Promise with shared session details + */ +export async function fetchSharedSessionDetails( + baseUrl: string, + shareToken: string +): Promise { + try { + const response = await fetch(`${baseUrl}/sessions/share/${shareToken}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + // Origin: 'http://localhost:5173', // required to bypass Cloudflare security filter + }, + credentials: 'include', + }); + + if (!response.ok) { + throw new Error(`Failed to fetch shared session: ${response.status} ${response.statusText}`); + } + + const data = await safeJsonParse( + response, + 'Failed to parse shared session' + ); + + if (baseUrl != data.base_url) { + throw new Error(`Base URL mismatch for shared session: ${baseUrl} != ${data.base_url}`); + } + + return { + share_token: data.share_token, + created_at: data.created_at, + base_url: data.base_url, + description: data.description, + working_dir: data.working_dir, + messages: data.messages, + message_count: data.message_count, + total_tokens: data.total_tokens, + }; + } catch (error) { + console.error('Error fetching shared session:', error); + throw error; + } +} + +/** + * Creates a new shared session + * @param baseUrl The base URL for session sharing API + * @param workingDir The working directory for the shared session + * @param messages The messages to include in the shared session + * @param description Description for the shared session + * @param totalTokens Total token count for the session, or null if not available + * @param userName The user name for who is sharing the session + * @returns Promise with the share token + */ +export async function createSharedSession( + baseUrl: string, + workingDir: string, + messages: Message[], + description: string, + totalTokens: number | null +): Promise { + try { + const response = await fetch(`${baseUrl}/sessions/share`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + working_dir: workingDir, + messages, + description: description, + base_url: baseUrl, + total_tokens: totalTokens ?? null, + }), + }); + + if (!response.ok) { + if (response.status === 302) { + throw new Error( + `Failed to create shared session. Please check that you are connected to VPN - ${response.status} ${response.statusText}` + ); + } + throw new Error(`Failed to create shared session: ${response.status} ${response.statusText}`); + } + + const data = await safeJsonParse<{ share_token: string }>( + response, + 'Failed to parse shared session response' + ); + return data.share_token; + } catch (error) { + console.error('Error creating shared session:', error); + throw error; + } +} diff --git a/ui/desktop/src/utils/appInitialization.ts b/ui/desktop/src/utils/appInitialization.ts index d45674b3910..054142036ca 100644 --- a/ui/desktop/src/utils/appInitialization.ts +++ b/ui/desktop/src/utils/appInitialization.ts @@ -181,7 +181,7 @@ const handleViewTypeDeepLink = (viewType: string, recipeConfig: unknown) => { recipes: '#/recipes', permission: '#/permission', ConfigureProviders: '#/configure-providers', - + sharedSession: '#/shared-session', recipeEditor: '#/recipe-editor', welcome: '#/welcome', }; diff --git a/ui/desktop/src/utils/navigationUtils.ts b/ui/desktop/src/utils/navigationUtils.ts index 59f727ea9cd..dd866b0a3b8 100644 --- a/ui/desktop/src/utils/navigationUtils.ts +++ b/ui/desktop/src/utils/navigationUtils.ts @@ -13,6 +13,7 @@ export type View = | 'settingsV2' | 'sessions' | 'schedules' + | 'sharedSession' | 'loading' | 'recipeEditor' | 'recipes' @@ -60,6 +61,9 @@ export const createNavigationHandler = (navigate: NavigateFunction) => { case 'ConfigureProviders': navigate('/configure-providers', { state: options }); break; + case 'sharedSession': + navigate('/shared-session', { state: options }); + break; case 'recipeEditor': navigate('/recipe-editor', { state: options }); break;