chore: Remove share session via base url (#9994)

This commit is contained in:
Lifei Zhou
2026-06-25 13:01:08 +10:00
committed by GitHub
parent db26fb79c9
commit 24166fc344
22 changed files with 16 additions and 2180 deletions
@@ -1,419 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Calendar,
MessageSquareText,
Folder,
Share2,
Sparkles,
Copy,
Check,
Target,
LoaderCircle,
AlertCircle,
} from 'lucide-react';
import { defineMessages, useIntl } from '../../i18n';
import { resumeSession } from '../../sessions';
import { Button } from '../ui/button';
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 { errorMessage } from '../../utils/conversionUtils';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
import ProgressiveMessageList from '../ProgressiveMessageList';
import { SearchView } from '../conversation/SearchView';
import BackButton from '../ui/BackButton';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip';
import { Message, Session } from '../../api';
import { useNavigation } from '../../hooks/useNavigation';
const i18n = defineMessages({
errorLoadingDetails: {
id: 'sessionHistory.error.loading',
defaultMessage: 'Error Loading Session Details',
},
tryAgain: {
id: 'sessionHistory.error.tryAgain',
defaultMessage: 'Try Again',
},
searchPlaceholder: {
id: 'sessionHistory.searchPlaceholder',
defaultMessage: 'Search history...',
},
noMessages: {
id: 'sessionHistory.empty.title',
defaultMessage: 'No messages found',
},
noMessagesDesc: {
id: 'sessionHistory.empty.description',
defaultMessage: "This session doesn't contain any messages",
},
loadingDetails: {
id: 'sessionHistory.loading',
defaultMessage: 'Loading session details...',
},
sharing: {
id: 'sessionHistory.sharing',
defaultMessage: 'Sharing...',
},
share: {
id: 'sessionHistory.share',
defaultMessage: 'Share',
},
shareTooltip: {
id: 'sessionHistory.shareTooltip',
defaultMessage: 'To enable session sharing, go to <b>Settings</b> > <b>Session</b> > <b>Session Sharing</b>.',
},
resume: {
id: 'sessionHistory.resume',
defaultMessage: 'Resume',
},
shareSessionTitle: {
id: 'sessionHistory.shareModal.title',
defaultMessage: 'Share Session (beta)',
},
shareSessionDescription: {
id: 'sessionHistory.shareModal.description',
defaultMessage: 'Share this session link to give others a read only view of your goose chat.',
},
copy: {
id: 'sessionHistory.copy',
defaultMessage: 'Copy',
},
cancel: {
id: 'sessionHistory.cancel',
defaultMessage: 'Cancel',
},
failedToShare: {
id: 'sessionHistory.toast.shareFailed',
defaultMessage: 'Failed to share session: {error}',
},
failedToCopy: {
id: 'sessionHistory.toast.copyFailed',
defaultMessage: 'Failed to copy link to clipboard',
},
couldNotLaunch: {
id: 'sessionHistory.toast.launchFailed',
defaultMessage: 'Could not launch session: {error}',
},
});
const isUserMessage = (message: Message): boolean => {
if (message.role === 'assistant') {
return false;
}
return !message.content.every(
(c) => c.type === 'actionRequired' && c.data.actionType === 'toolConfirmation'
);
};
const filterMessagesForDisplay = (messages: Message[]): Message[] => {
return messages;
};
interface SessionHistoryViewProps {
session: Session;
isLoading: boolean;
error: string | null;
onBack: () => void;
onRetry: () => void;
showActionButtons?: boolean;
}
// Custom SessionHeader component similar to SessionListView style
const SessionHeader: React.FC<{
onBack: () => void;
children: React.ReactNode;
title: string;
actionButtons?: React.ReactNode;
}> = ({ onBack, children, title, actionButtons }) => {
return (
<div className="flex flex-col pb-8 border-b">
<div className="flex items-center pt-0 mb-1">
<BackButton onClick={onBack} />
</div>
<h1 className="text-4xl font-light mb-4 pt-6">{title}</h1>
<div className="flex items-center">{children}</div>
{actionButtons && <div className="flex items-center space-x-3 mt-4">{actionButtons}</div>}
</div>
);
};
const SessionMessages: React.FC<{
messages: Message[];
isLoading: boolean;
error: string | null;
onRetry: () => void;
}> = ({ messages, isLoading, error, onRetry }) => {
const intl = useIntl();
const filteredMessages = filterMessagesForDisplay(messages);
return (
<ScrollArea className="h-full w-full">
<div className="pb-24 pt-8">
<div className="flex flex-col space-y-6">
{isLoading ? (
<div className="flex justify-center items-center py-12">
<LoaderCircle className="animate-spin h-8 w-8 text-text-primary" />
</div>
) : error ? (
<div className="flex flex-col items-center justify-center py-8 text-text-secondary">
<div className="text-red-500 mb-4">
<AlertCircle size={32} />
</div>
<p className="text-md mb-2">{intl.formatMessage(i18n.errorLoadingDetails)}</p>
<p className="text-sm text-center mb-4">{error}</p>
<Button onClick={onRetry} variant="default">
{intl.formatMessage(i18n.tryAgain)}
</Button>
</div>
) : filteredMessages?.length > 0 ? (
<div className="max-w-4xl mx-auto w-full">
<SearchView placeholder={intl.formatMessage(i18n.searchPlaceholder)}>
<ProgressiveMessageList
messages={filteredMessages}
chat={{
sessionId: 'session-preview',
}}
toolCallNotifications={new Map()}
append={() => {}} // Read-only for session history
isUserMessage={isUserMessage} // Use the same function as BaseChat
batchSize={15} // Same as BaseChat default
batchDelay={30} // Same as BaseChat default
showLoadingThreshold={30} // Same as BaseChat default
/>
</SearchView>
</div>
) : (
<div className="flex flex-col items-center justify-center py-8 text-text-secondary">
<MessageSquareText className="w-12 h-12 mb-4" />
<p className="text-lg mb-2">{intl.formatMessage(i18n.noMessages)}</p>
<p className="text-sm">{intl.formatMessage(i18n.noMessagesDesc)}</p>
</div>
)}
</div>
</div>
</ScrollArea>
);
};
const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
session,
isLoading,
error,
onBack,
onRetry,
showActionButtons = true,
}) => {
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
const [shareLink, setShareLink] = useState<string>('');
const [isSharing, setIsSharing] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const [canShare, setCanShare] = useState(false);
const intl = useIntl();
const messages = session.conversation || [];
const setView = useNavigation();
useEffect(() => {
window.electron.getSetting('sessionSharing').then((config) => {
if (config.enabled && config.baseUrl) {
setCanShare(true);
}
});
}, []);
const handleShare = async () => {
setIsSharing(true);
try {
const config = await window.electron.getSetting('sessionSharing');
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.working_dir,
messages,
session.name || 'Shared Session',
session.usage?.total_tokens || 0
);
const shareableLink = `goose://sessions/${shareToken}`;
setShareLink(shareableLink);
setIsShareModalOpen(true);
} catch (error) {
console.error('Error sharing session:', error);
toast.error(intl.formatMessage(i18n.failedToShare, { error: errorMessage(error, '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(intl.formatMessage(i18n.failedToCopy));
});
};
const handleResumeSession = () => {
try {
resumeSession(session, setView);
} catch (error) {
toast.error(intl.formatMessage(i18n.couldNotLaunch, { error: errorMessage(error) }));
}
};
const actionButtons = showActionButtons ? (
<>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={handleShare}
disabled={!canShare || isSharing}
size="sm"
variant="outline"
className={canShare ? '' : 'cursor-not-allowed opacity-50'}
>
{isSharing ? (
<>
<LoaderCircle className="w-4 h-4 mr-2 animate-spin" />
{intl.formatMessage(i18n.sharing)}
</>
) : (
<>
<Share2 className="w-4 h-4" />
{intl.formatMessage(i18n.share)}
</>
)}
</Button>
</TooltipTrigger>
{!canShare ? (
<TooltipContent>
<p>
{intl.formatMessage(i18n.shareTooltip, {
b: (chunks: React.ReactNode) => <b>{chunks}</b>,
})}
</p>
</TooltipContent>
) : null}
</Tooltip>
<Button onClick={handleResumeSession} size="sm" variant="outline">
<Sparkles className="w-4 h-4" />
{intl.formatMessage(i18n.resume)}
</Button>
</>
) : null;
return (
<>
<MainPanelLayout>
<div className="flex-1 flex flex-col min-h-0 px-8">
<SessionHeader
onBack={onBack}
title={session.name}
actionButtons={!isLoading ? actionButtons : null}
>
<div className="flex flex-col">
{!isLoading ? (
<>
<div className="flex items-center text-text-secondary text-sm space-x-5 font-mono">
<span className="flex items-center">
<Calendar className="w-4 h-4 mr-1" />
{formatMessageTimestamp(messages[0]?.created)}
</span>
<span className="flex items-center">
<MessageSquareText className="w-4 h-4 mr-1" />
{session.message_count}
</span>
{session.usage?.total_tokens != null && (
<span className="flex items-center">
<Target className="w-4 h-4 mr-1" />
{session.usage.total_tokens.toLocaleString()}
</span>
)}
</div>
<div className="flex items-center text-text-secondary text-sm mt-1 font-mono">
<span className="flex items-center">
<Folder className="w-4 h-4 mr-1" />
{session.working_dir}
</span>
</div>
</>
) : (
<div className="flex items-center text-text-secondary text-sm">
<LoaderCircle className="w-4 h-4 mr-2 animate-spin" />
<span>{intl.formatMessage(i18n.loadingDetails)}</span>
</div>
)}
</div>
</SessionHeader>
<SessionMessages
messages={messages}
isLoading={isLoading}
error={error}
onRetry={onRetry}
/>
</div>
</MainPanelLayout>
<Dialog open={isShareModalOpen} onOpenChange={setIsShareModalOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex justify-center items-center gap-2">
<Share2 className="w-6 h-6 text-text-primary" />
{intl.formatMessage(i18n.shareSessionTitle)}
</DialogTitle>
<DialogDescription>
{intl.formatMessage(i18n.shareSessionDescription)}
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="relative rounded-full border border-border-primary px-3 py-2 flex items-center bg-gray-100 dark:bg-gray-600">
<code className="text-sm text-text-primary dark:text-text-inverse overflow-x-hidden break-all pr-8 w-full">
{shareLink}
</code>
<Button
shape="pill"
variant="ghost"
className="absolute right-2 top-1/2 -translate-y-1/2"
onClick={handleCopyLink}
disabled={isCopied}
>
{isCopied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
<span className="sr-only">{intl.formatMessage(i18n.copy)}</span>
</Button>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsShareModalOpen(false)}>
{intl.formatMessage(i18n.cancel)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};
export default SessionHistoryView;
@@ -232,11 +232,10 @@ function useDebounce<T>(value: T, delay: number): T {
interface SessionListViewProps {
onSelectSession: (sessionId: string) => void;
selectedSessionId?: string | null;
}
const SessionListView: React.FC<SessionListViewProps> = React.memo(
({ onSelectSession, selectedSessionId }) => {
({ onSelectSession }) => {
const intl = useIntl();
const [sessions, setSessions] = useState<SessionListItem[]>([]);
const [isPrefetchingSessions, setIsPrefetchingSessions] = useState(false);
@@ -274,16 +273,6 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
const loadGenerationRef = useRef(0);
const hasLoadedRef = useRef(false);
// Track session to element ref
const sessionRefs = useRef<Record<string, HTMLElement>>({});
const setSessionRefs = (itemId: string, element: HTMLDivElement | null) => {
if (element) {
sessionRefs.current[itemId] = element;
} else {
delete sessionRefs.current[itemId];
}
};
const fileInputRef = useRef<HTMLInputElement>(null);
const visibleDateGroups = useMemo(() => {
@@ -435,18 +424,6 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
});
}, [memoizedDateGroups]);
// Scroll to the selected session when returning from session history view
useEffect(() => {
if (selectedSessionId) {
const element = sessionRefs.current[selectedSessionId];
if (element) {
element.scrollIntoView({
block: 'center',
});
}
}
}, [selectedSessionId, sessions]);
// Handle immediate search input (updates search term for debouncing).
const handleSearch = useCallback((term: string) => {
setSearchTerm(term);
@@ -727,7 +704,6 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
<Card
onClick={handleCardClick}
className="h-full py-3 px-4 hover:shadow-default cursor-pointer transition-all duration-150 flex flex-col justify-between relative group"
ref={(el) => setSessionRefs(session.id, el)}
>
<div>
<h3 className="text-base break-words line-clamp-2 w-full mb-1">{displayName}</h3>
@@ -1,54 +1,10 @@
import React, { useState, useEffect, useCallback } from 'react';
import { defineMessages, useIntl } from '../../i18n';
import React, { useCallback } from 'react';
import SessionListView from './SessionListView';
import SessionHistoryView from './SessionHistoryView';
import { useLocation } from 'react-router-dom';
import { getSession, Session } from '../../api';
import { useNavigation } from '../../hooks/useNavigation';
const i18n = defineMessages({
loading: {
id: 'sessionsView.loading',
defaultMessage: 'Loading...',
},
failedToLoad: {
id: 'sessionsView.error.failedToLoad',
defaultMessage: 'Failed to load session details. Please try again later.',
},
});
const SessionsView: React.FC = () => {
const intl = useIntl();
const [selectedSession, setSelectedSession] = useState<Session | null>(null);
const [showSessionHistory, setShowSessionHistory] = useState(false);
const [isLoadingSession, setIsLoadingSession] = useState(false);
const [error, setError] = useState<string | null>(null);
const [initialSessionId, setInitialSessionId] = useState<string | null>(null);
const location = useLocation();
const setView = useNavigation();
const loadSessionDetails = async (sessionId: string) => {
setIsLoadingSession(true);
setError(null);
setShowSessionHistory(true);
try {
const response = await getSession<true>({
path: { session_id: sessionId },
throwOnError: true,
});
setSelectedSession(response.data);
} catch (err) {
console.error(`Failed to load session details for ${sessionId}:`, err);
setError(intl.formatMessage(i18n.failedToLoad));
// Keep the selected session null if there's an error
setSelectedSession(null);
setShowSessionHistory(false);
} finally {
setIsLoadingSession(false);
setInitialSessionId(null);
}
};
const handleSelectSession = useCallback(
async (sessionId: string) => {
setView('pair', {
@@ -59,59 +15,7 @@ const SessionsView: React.FC = () => {
[setView]
);
// Check if a session ID was passed in the location state.
useEffect(() => {
const state = location.state as { selectedSessionId?: string } | null;
if (state?.selectedSessionId) {
// Set immediate loading state to prevent flash of session list
setIsLoadingSession(true);
setInitialSessionId(state.selectedSessionId);
handleSelectSession(state.selectedSessionId);
// Clear the state to prevent reloading on navigation
window.history.replaceState({}, document.title);
}
}, [location.state, handleSelectSession]);
const handleBackToSessions = () => {
setShowSessionHistory(false);
setError(null);
};
const handleRetryLoadSession = () => {
if (selectedSession) {
loadSessionDetails(selectedSession.id);
}
};
// If we're loading an initial session or have a selected showSessionHistory, show the session history view
// Otherwise, show the sessions list view
return (showSessionHistory && selectedSession) || (isLoadingSession && initialSessionId) ? (
<SessionHistoryView
session={
selectedSession || {
id: initialSessionId || '',
conversation: [],
name: intl.formatMessage(i18n.loading),
working_dir: '',
message_count: 0,
usage: {},
created_at: '',
updated_at: '',
extension_data: {},
user_set_name: false,
}
}
isLoading={isLoadingSession}
error={error}
onBack={handleBackToSessions}
onRetry={handleRetryLoadSession}
/>
) : (
<SessionListView
onSelectSession={handleSelectSession}
selectedSessionId={selectedSession?.id ?? null}
/>
);
return <SessionListView onSelectSession={handleSelectSession} />;
};
export default SessionsView;
@@ -1,104 +0,0 @@
import React from 'react';
import { Calendar, MessageSquareText, Folder, Target, LoaderCircle, Share2 } from 'lucide-react';
import { defineMessages, useIntl } from '../../i18n';
import { type SharedSessionDetails } from '../../sharedSessions';
import { SessionMessages } from './SessionViewComponents';
import { formatMessageTimestamp } from '../../utils/timeUtils';
import { MainPanelLayout } from '../Layout/MainPanelLayout';
const i18n = defineMessages({
sharedSession: {
id: 'sharedSession.title',
defaultMessage: 'Shared Session',
},
loadingDetails: {
id: 'sharedSession.loading',
defaultMessage: 'Loading session details...',
},
});
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 (
<div className="flex flex-col pb-8 border-b">
<h1 className="text-4xl font-light mb-4 pt-6">{title}</h1>
<div className="flex items-center">{children}</div>
</div>
);
};
const SharedSessionView: React.FC<SharedSessionViewProps> = ({
session,
isLoading,
error,
onRetry,
}) => {
const intl = useIntl();
return (
<MainPanelLayout>
<div className="flex-1 flex flex-col min-h-0 px-8">
<div className="flex items-center py-4 border-b border-border-primary mb-6">
<div className="flex items-center text-text-secondary">
<Share2 className="w-5 h-5 mr-2" />
<span className="text-sm font-medium">{intl.formatMessage(i18n.sharedSession)}</span>
</div>
</div>
<SessionHeader title={session ? session.description : intl.formatMessage(i18n.sharedSession)}>
<div className="flex flex-col">
{!isLoading && session && session.messages.length > 0 ? (
<>
<div className="flex items-center text-text-secondary text-sm space-x-5 font-mono">
<span className="flex items-center">
<Calendar className="w-4 h-4 mr-1" />
{formatMessageTimestamp(session.messages[0]?.created)}
</span>
<span className="flex items-center">
<MessageSquareText className="w-4 h-4 mr-1" />
{session.message_count}
</span>
{session.total_tokens !== null && (
<span className="flex items-center">
<Target className="w-4 h-4 mr-1" />
{session.total_tokens.toLocaleString()}
</span>
)}
</div>
<div className="flex items-center text-text-secondary text-sm mt-1 font-mono">
<span className="flex items-center">
<Folder className="w-4 h-4 mr-1" />
{session.working_dir}
</span>
</div>
</>
) : (
<div className="flex items-center text-text-secondary text-sm">
<LoaderCircle className="w-4 h-4 mr-2 animate-spin" />
<span>{intl.formatMessage(i18n.loadingDetails)}</span>
</div>
)}
</div>
</SessionHeader>
<SessionMessages
messages={session?.messages || []}
isLoading={isLoading}
error={error}
onRetry={onRetry}
/>
</div>
</MainPanelLayout>
);
};
export default SharedSessionView;
@@ -2,7 +2,6 @@ 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 ExternalBackendSection from './app/ExternalBackendSection';
import AppSettingsSection from './app/AppSettingsSection';
import ConfigSettings from './config/ConfigSettings';
@@ -253,7 +252,6 @@ export default function SettingsView({
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
>
<div className="space-y-8 pb-8">
<SessionSharingSection />
<ExternalBackendSection />
</div>
</TabsContent>
@@ -1,321 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Input } from '../../ui/input';
import { Check, Lock, Loader2, AlertCircle } from 'lucide-react';
import { Switch } from '../../ui/switch';
import { Button } from '../../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { trackSettingToggled } from '../../../utils/analytics';
import { defineMessages, useIntl } from '../../../i18n';
const i18n = defineMessages({
title: {
id: 'sessionSharingSection.title',
defaultMessage: 'Session Sharing',
},
descriptionConfigured: {
id: 'sessionSharingSection.descriptionConfigured',
defaultMessage:
'Session sharing is configured but fully opt-in — your sessions are only shared when you explicitly click the share button.',
},
descriptionDefault: {
id: 'sessionSharingSection.descriptionDefault',
defaultMessage: 'You can enable session sharing to share your sessions with others.',
},
alreadyConfigured: {
id: 'sessionSharingSection.alreadyConfigured',
defaultMessage: 'Session sharing has already been configured',
},
enableSharing: {
id: 'sessionSharingSection.enableSharing',
defaultMessage: 'Enable session sharing',
},
baseUrl: {
id: 'sessionSharingSection.baseUrl',
defaultMessage: 'Base URL',
},
urlPlaceholder: {
id: 'sessionSharingSection.urlPlaceholder',
defaultMessage: 'https://example.com/api',
},
invalidUrl: {
id: 'sessionSharingSection.invalidUrl',
defaultMessage:
'Invalid URL format. Please enter a valid URL (e.g. https://example.com/api).',
},
testingConnection: {
id: 'sessionSharingSection.testingConnection',
defaultMessage: 'Testing connection...',
},
connectionSuccess: {
id: 'sessionSharingSection.connectionSuccess',
defaultMessage: 'Connection successful!',
},
serverError: {
id: 'sessionSharingSection.serverError',
defaultMessage:
'Server error: HTTP {status}. The server may not be configured correctly.',
},
connectionFailed: {
id: 'sessionSharingSection.connectionFailed',
defaultMessage: 'Connection failed. ',
},
unreachableServer: {
id: 'sessionSharingSection.unreachableServer',
defaultMessage:
'Unable to reach the server. Please check the URL and your network connection.',
},
connectionTimedOut: {
id: 'sessionSharingSection.connectionTimedOut',
defaultMessage: 'Connection timed out. The server may be slow or unreachable.',
},
unknownError: {
id: 'sessionSharingSection.unknownError',
defaultMessage: 'Unknown error occurred.',
},
testing: {
id: 'sessionSharingSection.testing',
defaultMessage: 'Testing...',
},
testConnection: {
id: 'sessionSharingSection.testConnection',
defaultMessage: 'Test Connection',
},
});
export default function SessionSharingSection() {
const intl = useIntl();
const envBaseUrlShare = window.appConfig.get('GOOSE_BASE_URL_SHARE');
// If env is set, force sharing enabled and set the baseUrl accordingly.
const [sessionSharingConfig, setSessionSharingConfig] = useState({
enabled: envBaseUrlShare ? true : false,
baseUrl: typeof envBaseUrlShare === 'string' ? envBaseUrlShare : '',
});
const [urlError, setUrlError] = useState('');
const [testResult, setTestResult] = useState<{
status: 'success' | 'error' | 'testing' | null;
message: string;
}>({ 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 settings if the env variable is not provided.
useEffect(() => {
if (envBaseUrlShare) {
// If env variable is set, save the forced configuration to settings
const forcedConfig = {
enabled: true,
baseUrl: typeof envBaseUrlShare === 'string' ? envBaseUrlShare : '',
};
window.electron.setSetting('sessionSharing', forcedConfig);
} else {
window.electron.getSetting('sessionSharing').then((config) => {
setSessionSharingConfig(config);
});
}
}, [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 = async () => {
if (envBaseUrlShare) {
return; // Do nothing if the environment variable forces sharing.
}
const updated = { ...sessionSharingConfig, enabled: !sessionSharingConfig.enabled };
setSessionSharingConfig(updated);
await window.electron.setSetting('sessionSharing', updated);
trackSettingToggled('session_sharing', updated.enabled);
};
// Handle changes to the base URL field
const handleBaseUrlChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
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 };
await window.electron.setSetting('sessionSharing', updated);
} else {
setUrlError(intl.formatMessage(i18n.invalidUrl));
}
};
// Test connection to the configured URL
const testConnection = async () => {
const baseUrl = sessionSharingConfig.baseUrl;
if (!baseUrl) return;
setTestResult({ status: 'testing', message: intl.formatMessage(i18n.testingConnection) });
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: intl.formatMessage(i18n.connectionSuccess),
});
} else {
setTestResult({
status: 'error',
message: intl.formatMessage(i18n.serverError, { status: response.status }),
});
}
} catch (error) {
console.error('Connection test failed:', error);
let errorMessage = intl.formatMessage(i18n.connectionFailed);
if (error instanceof TypeError && error.message.includes('fetch')) {
errorMessage += intl.formatMessage(i18n.unreachableServer);
} else if (error instanceof Error) {
if (error.name === 'AbortError') {
errorMessage += intl.formatMessage(i18n.connectionTimedOut);
} else {
errorMessage += error.message;
}
} else {
errorMessage += intl.formatMessage(i18n.unknownError);
}
setTestResult({
status: 'error',
message: errorMessage,
});
}
};
return (
<section id="session-sharing" className="space-y-4 pr-4 mt-1">
<Card className="pb-2">
<CardHeader className="pb-0">
<CardTitle>{intl.formatMessage(i18n.title)}</CardTitle>
<CardDescription>
{(envBaseUrlShare as string)
? intl.formatMessage(i18n.descriptionConfigured)
: intl.formatMessage(i18n.descriptionDefault)}
</CardDescription>
</CardHeader>
<CardContent className="px-4 py-2">
<div className="space-y-4">
{/* Toggle for enabling session sharing */}
<div className="flex items-center gap-3">
<label className="text-sm cursor-pointer">
{(envBaseUrlShare as string)
? intl.formatMessage(i18n.alreadyConfigured)
: intl.formatMessage(i18n.enableSharing)}
</label>
{envBaseUrlShare ? (
<Lock className="w-5 h-5 text-text-secondary" />
) : (
<Switch
checked={sessionSharingConfig.enabled}
disabled={!!envBaseUrlShare}
onCheckedChange={toggleSharing}
variant="mono"
/>
)}
</div>
{/* Base URL field (only visible if enabled) */}
{sessionSharingConfig.enabled && (
<div className="space-y-2 relative">
<div className="flex items-center space-x-2">
<label htmlFor="session-sharing-url" className="text-sm text-text-primary">
{intl.formatMessage(i18n.baseUrl)}
</label>
{isUrlConfigured && <Check className="w-5 h-5 text-green-500" />}
</div>
<div className="flex items-center">
<Input
id="session-sharing-url"
type="url"
placeholder={intl.formatMessage(i18n.urlPlaceholder)}
value={sessionSharingConfig.baseUrl}
disabled={!!envBaseUrlShare}
{...(envBaseUrlShare ? {} : { onChange: handleBaseUrlChange })}
/>
</div>
{urlError && <p className="text-red-500 text-sm">{urlError}</p>}
{(isUrlConfigured || (envBaseUrlShare as string)) && (
<div className="space-y-2">
<Button
variant="outline"
size="sm"
onClick={testConnection}
disabled={testResult.status === 'testing'}
className="flex items-center gap-2"
>
{testResult.status === 'testing' ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
{intl.formatMessage(i18n.testing)}
</>
) : (
intl.formatMessage(i18n.testConnection)
)}
</Button>
{/* Test Results */}
{testResult.status && testResult.status !== 'testing' && (
<div
className={`flex items-start gap-2 p-3 rounded-md text-sm ${
testResult.status === 'success'
? 'bg-green-50 text-green-800 border border-green-200'
: 'bg-red-50 text-red-800 border border-red-200'
}`}
>
{testResult.status === 'success' ? (
<Check className="w-4 h-4 mt-0.5 flex-shrink-0" />
) : (
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
)}
<span>{testResult.message}</span>
</div>
)}
</div>
)}
</div>
)}
</div>
</CardContent>
</Card>
</section>
);
}