chore: removing little-used session sharing feature (#4249)

This commit is contained in:
Alex Hancock
2025-08-22 10:49:18 -04:00
committed by GitHub
parent 69ad4cad63
commit 180661f26f
11 changed files with 55 additions and 890 deletions
-100
View File
@@ -1,8 +1,6 @@
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 { ConfirmationModal } from './components/ui/ConfirmationModal';
import { ToastContainer } from 'react-toastify';
@@ -17,7 +15,6 @@ 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';
@@ -320,48 +317,6 @@ 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 (
<SharedSessionView
session={sessionDetails}
isLoading={isLoadingSharedSession}
error={error}
onRetry={async () => {
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();
@@ -404,8 +359,6 @@ export default function App() {
const [extensionConfirmTitle, setExtensionConfirmTitle] = useState<string>('');
const [isLoadingSession, setIsLoadingSession] = useState(false);
const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false);
const [sharedSessionError, setSharedSessionError] = useState<string | null>(null);
// Add separate state for pair chat to maintain its own conversation
const [pairChat, setPairChat] = useState<ChatType>({
@@ -452,9 +405,6 @@ 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;
@@ -541,44 +491,6 @@ 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[]) => {
@@ -1012,18 +924,6 @@ export default function App() {
</ProviderGuard>
}
/>
<Route
path="shared-session"
element={
<ProviderGuard>
<SharedSessionRouteWrapper
isLoadingSharedSession={isLoadingSharedSession}
setIsLoadingSharedSession={setIsLoadingSharedSession}
sharedSessionError={sharedSessionError}
/>
</ProviderGuard>
}
/>
<Route
path="permission"
element={
@@ -54,9 +54,7 @@ const AppLayoutContent: React.FC<AppLayoutProps> = ({ setIsGoosehintsModalOpen }
case 'ConfigureProviders':
navigate('/configure-providers');
break;
case 'sharedSession':
navigate('/shared-session', { state: viewOptions });
break;
case 'recipeEditor':
navigate('/recipe-editor', { state: viewOptions });
break;
@@ -1,12 +1,9 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import {
Calendar,
MessageSquareText,
Folder,
Share2,
Sparkles,
Copy,
Check,
Target,
LoaderCircle,
AlertCircle,
@@ -17,21 +14,11 @@ 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 { ChatContextManagerProvider } from '../context_management/ChatContextManager';
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 => {
@@ -150,74 +137,6 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
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);
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);
@@ -249,136 +168,63 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
// Define action buttons
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" />
Sharing...
</>
) : (
<>
<Share2 className="w-4 h-4" />
Share
</>
)}
</Button>
</TooltipTrigger>
{!canShare ? (
<TooltipContent>
<p>
To enable session sharing, go to <b>Settings</b> {'>'} <b>Session</b> {'>'}{' '}
<b>Session Sharing</b>.
</p>
</TooltipContent>
) : null}
</Tooltip>
<Button onClick={handleLaunchInNewWindow} size="sm" variant="outline">
<Sparkles className="w-4 h-4" />
Resume
</Button>
</>
<Button onClick={handleLaunchInNewWindow} size="sm" variant="outline">
<Sparkles className="w-4 h-4" />
Resume
</Button>
) : null;
return (
<>
<MainPanelLayout>
<div className="flex-1 flex flex-col min-h-0 px-8">
<SessionHeader
onBack={onBack}
title={session.metadata.description || 'Session Details'}
actionButtons={!isLoading ? actionButtons : null}
>
<div className="flex flex-col">
{!isLoading && session.messages.length > 0 ? (
<>
<div className="flex items-center text-text-muted text-sm space-x-5 font-mono">
<MainPanelLayout>
<div className="flex-1 flex flex-col min-h-0 px-8">
<SessionHeader
onBack={onBack}
title={session.metadata.description || 'Session Details'}
actionButtons={!isLoading ? actionButtons : null}
>
<div className="flex flex-col">
{!isLoading && session.messages.length > 0 ? (
<>
<div className="flex items-center text-text-muted 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.metadata.message_count}
</span>
{session.metadata.total_tokens !== null && (
<span className="flex items-center">
<Calendar className="w-4 h-4 mr-1" />
{formatMessageTimestamp(session.messages[0]?.created)}
<Target className="w-4 h-4 mr-1" />
{session.metadata.total_tokens.toLocaleString()}
</span>
<span className="flex items-center">
<MessageSquareText className="w-4 h-4 mr-1" />
{session.metadata.message_count}
</span>
{session.metadata.total_tokens !== null && (
<span className="flex items-center">
<Target className="w-4 h-4 mr-1" />
{session.metadata.total_tokens.toLocaleString()}
</span>
)}
</div>
<div className="flex items-center text-text-muted text-sm mt-1 font-mono">
<span className="flex items-center">
<Folder className="w-4 h-4 mr-1" />
{session.metadata.working_dir}
</span>
</div>
</>
) : (
<div className="flex items-center text-text-muted text-sm">
<LoaderCircle className="w-4 h-4 mr-2 animate-spin" />
<span>Loading session details...</span>
)}
</div>
)}
</div>
</SessionHeader>
<SessionMessages
messages={session.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-textStandard" />
Share Session (beta)
</DialogTitle>
<DialogDescription>
Share this session link to give others a read only view of your goose chat.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="relative rounded-full border border-borderSubtle px-3 py-2 flex items-center bg-gray-100 dark:bg-gray-600">
<code className="text-sm text-textStandard dark:text-textStandardInverse 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">Copy</span>
</Button>
</div>
<div className="flex items-center text-text-muted text-sm mt-1 font-mono">
<span className="flex items-center">
<Folder className="w-4 h-4 mr-1" />
{session.metadata.working_dir}
</span>
</div>
</>
) : (
<div className="flex items-center text-text-muted text-sm">
<LoaderCircle className="w-4 h-4 mr-2 animate-spin" />
<span>Loading session details...</span>
</div>
)}
</div>
</SessionHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setIsShareModalOpen(false)}>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
<SessionMessages
messages={session.messages}
isLoading={isLoading}
error={error}
onRetry={onRetry}
/>
</div>
</MainPanelLayout>
);
};
@@ -1,91 +0,0 @@
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 (
<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,
}) => {
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-subtle mb-6">
<div className="flex items-center text-text-muted">
<Share2 className="w-5 h-5 mr-2" />
<span className="text-sm font-medium">Shared Session</span>
</div>
</div>
<SessionHeader title={session ? session.description : 'Shared Session'}>
<div className="flex flex-col">
{!isLoading && session && session.messages.length > 0 ? (
<>
<div className="flex items-center text-text-muted 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-muted 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-muted text-sm">
<LoaderCircle className="w-4 h-4 mr-2 animate-spin" />
<span>Loading session details...</span>
</div>
)}
</div>
</SessionHeader>
<SessionMessages
messages={session?.messages || []}
isLoading={isLoading}
error={error}
onRetry={onRetry}
/>
</div>
</MainPanelLayout>
);
};
export default SharedSessionView;
@@ -2,12 +2,11 @@ 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, Share2, Monitor, MessageSquare } from 'lucide-react';
import { Bot, Monitor, MessageSquare } from 'lucide-react';
import { useState, useEffect } from 'react';
import ChatSettingsSection from './chat/ChatSettingsSection';
import { CONFIGURATION_ENABLED } from '../../updates';
@@ -37,7 +36,6 @@ export default function SettingsView({
update: 'app',
models: 'models',
modes: 'chat',
sharing: 'sharing',
styles: 'chat',
tools: 'chat',
app: 'app',
@@ -93,14 +91,6 @@ export default function SettingsView({
<MessageSquare className="h-4 w-4" />
Chat
</TabsTrigger>
<TabsTrigger
value="sharing"
className="flex gap-2"
data-testid="settings-sharing-tab"
>
<Share2 className="h-4 w-4" />
Session
</TabsTrigger>
<TabsTrigger value="app" className="flex gap-2" data-testid="settings-app-tab">
<Monitor className="h-4 w-4" />
App
@@ -123,13 +113,6 @@ export default function SettingsView({
<ChatSettingsSection />
</TabsContent>
<TabsContent
value="sharing"
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
>
<SessionSharingSection />
</TabsContent>
<TabsContent
value="app"
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
@@ -1,252 +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';
export default function SessionSharingSection() {
const envBaseUrlShare = window.appConfig.get('GOOSE_BASE_URL_SHARE');
console.log('envBaseUrlShare', envBaseUrlShare);
// 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 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<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 };
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 (
<section id="session-sharing" className="space-y-4 pr-4 mt-1">
<Card className="pb-2">
<CardHeader className="pb-0">
<CardTitle>Session Sharing</CardTitle>
<CardDescription>
{(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.'}
</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)
? 'Session sharing has already been configured'
: 'Enable session sharing'}
</label>
{envBaseUrlShare ? (
<Lock className="w-5 h-5 text-text-muted" />
) : (
<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-default">
Base URL
</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="https://example.com/api"
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" />
Testing...
</>
) : (
'Test Connection'
)}
</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>
);
}
+2 -17
View File
@@ -297,8 +297,6 @@ 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');
@@ -361,8 +359,6 @@ 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);
}
}
});
@@ -465,16 +461,7 @@ 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;
@@ -482,8 +469,6 @@ const getVersion = () => {
const [provider, model, predefinedModels] = getGooseProvider();
const sharingUrl = getSharingUrl();
const gooseVersion = getVersion();
const SERVER_SECRET = process.env.GOOSE_EXTERNAL_BACKEND
@@ -608,7 +593,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,
}),
@@ -663,7 +648,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,
};
-86
View File
@@ -1,86 +0,0 @@
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<SharedSessionDetails | null> {
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;
}
}
-114
View File
@@ -1,114 +0,0 @@
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<SharedSessionDetails> {
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<SharedSessionDetails>(
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<string> {
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;
}
}
+1 -1
View File
@@ -178,7 +178,7 @@ const handleViewTypeDeepLink = (viewType: string, recipeConfig: unknown) => {
recipes: '#/recipes',
permission: '#/permission',
ConfigureProviders: '#/configure-providers',
sharedSession: '#/shared-session',
recipeEditor: '#/recipe-editor',
welcome: '#/welcome',
};
-4
View File
@@ -13,7 +13,6 @@ export type View =
| 'settingsV2'
| 'sessions'
| 'schedules'
| 'sharedSession'
| 'loading'
| 'recipeEditor'
| 'recipes'
@@ -61,9 +60,6 @@ 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;