feat: re-introduce session sharing (#4370)
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { IpcRendererEvent } from 'electron';
|
import { IpcRendererEvent } from 'electron';
|
||||||
import { HashRouter, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
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 { ErrorUI } from './components/ErrorBoundary';
|
||||||
import { ExtensionInstallModal } from './components/ExtensionInstallModal';
|
import { ExtensionInstallModal } from './components/ExtensionInstallModal';
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
@@ -14,6 +16,7 @@ import Hub from './components/hub';
|
|||||||
import Pair from './components/pair';
|
import Pair from './components/pair';
|
||||||
import SettingsView, { SettingsViewOptions } from './components/settings/SettingsView';
|
import SettingsView, { SettingsViewOptions } from './components/settings/SettingsView';
|
||||||
import SessionsView from './components/sessions/SessionsView';
|
import SessionsView from './components/sessions/SessionsView';
|
||||||
|
import SharedSessionView from './components/sessions/SharedSessionView';
|
||||||
import SchedulesView from './components/schedule/SchedulesView';
|
import SchedulesView from './components/schedule/SchedulesView';
|
||||||
import ProviderSettings from './components/settings/providers/ProviderSettingsPage';
|
import ProviderSettings from './components/settings/providers/ProviderSettingsPage';
|
||||||
import { useChat } from './hooks/useChat';
|
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 (
|
||||||
|
<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 ExtensionsRoute = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -353,6 +398,8 @@ export default function App() {
|
|||||||
const [isLoadingSession, setIsLoadingSession] = useState(false);
|
const [isLoadingSession, setIsLoadingSession] = useState(false);
|
||||||
const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
|
const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
|
||||||
const [agentWaitingMessage, setAgentWaitingMessage] = useState<string | null>(null);
|
const [agentWaitingMessage, setAgentWaitingMessage] = useState<string | null>(null);
|
||||||
|
const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false);
|
||||||
|
const [sharedSessionError, setSharedSessionError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Add separate state for pair chat to maintain its own conversation
|
// Add separate state for pair chat to maintain its own conversation
|
||||||
const [pairChat, setPairChat] = useState<ChatType>({
|
const [pairChat, setPairChat] = useState<ChatType>({
|
||||||
@@ -399,6 +446,9 @@ export default function App() {
|
|||||||
case 'ConfigureProviders':
|
case 'ConfigureProviders':
|
||||||
window.location.hash = '#/configure-providers';
|
window.location.hash = '#/configure-providers';
|
||||||
break;
|
break;
|
||||||
|
case 'sharedSession':
|
||||||
|
window.location.hash = '#/shared-session';
|
||||||
|
break;
|
||||||
case 'recipeEditor':
|
case 'recipeEditor':
|
||||||
window.location.hash = '#/recipe-editor';
|
window.location.hash = '#/recipe-editor';
|
||||||
break;
|
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
|
// Handle recipe decode events from main process
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleLoadRecipeDeeplink = (_event: IpcRendererEvent, ...args: unknown[]) => {
|
const handleLoadRecipeDeeplink = (_event: IpcRendererEvent, ...args: unknown[]) => {
|
||||||
@@ -793,6 +881,18 @@ export default function App() {
|
|||||||
</ProviderGuard>
|
</ProviderGuard>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="shared-session"
|
||||||
|
element={
|
||||||
|
<ProviderGuard>
|
||||||
|
<SharedSessionRouteWrapper
|
||||||
|
isLoadingSharedSession={isLoadingSharedSession}
|
||||||
|
setIsLoadingSharedSession={setIsLoadingSharedSession}
|
||||||
|
sharedSessionError={sharedSessionError}
|
||||||
|
/>
|
||||||
|
</ProviderGuard>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="permission"
|
path="permission"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ const AppLayoutContent: React.FC<AppLayoutProps> = ({ setIsGoosehintsModalOpen }
|
|||||||
case 'ConfigureProviders':
|
case 'ConfigureProviders':
|
||||||
navigate('/configure-providers');
|
navigate('/configure-providers');
|
||||||
break;
|
break;
|
||||||
|
case 'sharedSession':
|
||||||
|
navigate('/shared-session', { state: viewOptions });
|
||||||
|
break;
|
||||||
case 'recipeEditor':
|
case 'recipeEditor':
|
||||||
navigate('/recipe-editor', { state: viewOptions });
|
navigate('/recipe-editor', { state: viewOptions });
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
MessageSquareText,
|
MessageSquareText,
|
||||||
Folder,
|
Folder,
|
||||||
|
Share2,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
Target,
|
Target,
|
||||||
LoaderCircle,
|
LoaderCircle,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
@@ -14,11 +17,21 @@ import { toast } from 'react-toastify';
|
|||||||
import { MainPanelLayout } from '../Layout/MainPanelLayout';
|
import { MainPanelLayout } from '../Layout/MainPanelLayout';
|
||||||
import { ScrollArea } from '../ui/scroll-area';
|
import { ScrollArea } from '../ui/scroll-area';
|
||||||
import { formatMessageTimestamp } from '../../utils/timeUtils';
|
import { formatMessageTimestamp } from '../../utils/timeUtils';
|
||||||
|
import { createSharedSession } from '../../sharedSessions';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '../ui/dialog';
|
||||||
import ProgressiveMessageList from '../ProgressiveMessageList';
|
import ProgressiveMessageList from '../ProgressiveMessageList';
|
||||||
import { SearchView } from '../conversation/SearchView';
|
import { SearchView } from '../conversation/SearchView';
|
||||||
import { ContextManagerProvider } from '../context_management/ContextManager';
|
import { ContextManagerProvider } from '../context_management/ContextManager';
|
||||||
import { Message } from '../../types/message';
|
import { Message } from '../../types/message';
|
||||||
import BackButton from '../ui/BackButton';
|
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)
|
// Helper function to determine if a message is a user message (same as useChatEngine)
|
||||||
const isUserMessage = (message: Message): boolean => {
|
const isUserMessage = (message: Message): boolean => {
|
||||||
@@ -137,6 +150,74 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
|||||||
onRetry,
|
onRetry,
|
||||||
showActionButtons = true,
|
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 = () => {
|
const handleLaunchInNewWindow = () => {
|
||||||
if (session) {
|
if (session) {
|
||||||
console.log('Launching session in new window:', session.session_id);
|
console.log('Launching session in new window:', session.session_id);
|
||||||
@@ -168,63 +249,136 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
|||||||
|
|
||||||
// Define action buttons
|
// Define action buttons
|
||||||
const actionButtons = showActionButtons ? (
|
const actionButtons = showActionButtons ? (
|
||||||
<Button onClick={handleLaunchInNewWindow} size="sm" variant="outline">
|
<>
|
||||||
<Sparkles className="w-4 h-4" />
|
<Tooltip>
|
||||||
Resume
|
<TooltipTrigger asChild>
|
||||||
</Button>
|
<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>
|
||||||
|
</>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainPanelLayout>
|
<>
|
||||||
<div className="flex-1 flex flex-col min-h-0 px-8">
|
<MainPanelLayout>
|
||||||
<SessionHeader
|
<div className="flex-1 flex flex-col min-h-0 px-8">
|
||||||
onBack={onBack}
|
<SessionHeader
|
||||||
title={session.metadata.description || 'Session Details'}
|
onBack={onBack}
|
||||||
actionButtons={!isLoading ? actionButtons : null}
|
title={session.metadata.description || 'Session Details'}
|
||||||
>
|
actionButtons={!isLoading ? actionButtons : null}
|
||||||
<div className="flex flex-col">
|
>
|
||||||
{!isLoading && session.messages.length > 0 ? (
|
<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">
|
<div className="flex items-center text-text-muted text-sm space-x-5 font-mono">
|
||||||
<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">
|
<span className="flex items-center">
|
||||||
<Target className="w-4 h-4 mr-1" />
|
<Calendar className="w-4 h-4 mr-1" />
|
||||||
{session.metadata.total_tokens.toLocaleString()}
|
{formatMessageTimestamp(session.messages[0]?.created)}
|
||||||
</span>
|
</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>
|
||||||
<div className="flex items-center text-text-muted text-sm mt-1 font-mono">
|
)}
|
||||||
<span className="flex items-center">
|
</div>
|
||||||
<Folder className="w-4 h-4 mr-1" />
|
</SessionHeader>
|
||||||
{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
|
<SessionMessages
|
||||||
messages={session.messages}
|
messages={session.messages}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
error={error}
|
error={error}
|
||||||
onRetry={onRetry}
|
onRetry={onRetry}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</MainPanelLayout>
|
</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>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsShareModalOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<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,11 +2,12 @@ import { ScrollArea } from '../ui/scroll-area';
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||||
import { View, ViewOptions } from '../../utils/navigationUtils';
|
import { View, ViewOptions } from '../../utils/navigationUtils';
|
||||||
import ModelsSection from './models/ModelsSection';
|
import ModelsSection from './models/ModelsSection';
|
||||||
|
import SessionSharingSection from './sessions/SessionSharingSection';
|
||||||
import AppSettingsSection from './app/AppSettingsSection';
|
import AppSettingsSection from './app/AppSettingsSection';
|
||||||
import ConfigSettings from './config/ConfigSettings';
|
import ConfigSettings from './config/ConfigSettings';
|
||||||
import { ExtensionConfig } from '../../api';
|
import { ExtensionConfig } from '../../api';
|
||||||
import { MainPanelLayout } from '../Layout/MainPanelLayout';
|
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 { useState, useEffect } from 'react';
|
||||||
import ChatSettingsSection from './chat/ChatSettingsSection';
|
import ChatSettingsSection from './chat/ChatSettingsSection';
|
||||||
import { CONFIGURATION_ENABLED } from '../../updates';
|
import { CONFIGURATION_ENABLED } from '../../updates';
|
||||||
@@ -36,6 +37,7 @@ export default function SettingsView({
|
|||||||
update: 'app',
|
update: 'app',
|
||||||
models: 'models',
|
models: 'models',
|
||||||
modes: 'chat',
|
modes: 'chat',
|
||||||
|
sharing: 'sharing',
|
||||||
styles: 'chat',
|
styles: 'chat',
|
||||||
tools: 'chat',
|
tools: 'chat',
|
||||||
app: 'app',
|
app: 'app',
|
||||||
@@ -91,6 +93,14 @@ export default function SettingsView({
|
|||||||
<MessageSquare className="h-4 w-4" />
|
<MessageSquare className="h-4 w-4" />
|
||||||
Chat
|
Chat
|
||||||
</TabsTrigger>
|
</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">
|
<TabsTrigger value="app" className="flex gap-2" data-testid="settings-app-tab">
|
||||||
<Monitor className="h-4 w-4" />
|
<Monitor className="h-4 w-4" />
|
||||||
App
|
App
|
||||||
@@ -113,6 +123,13 @@ export default function SettingsView({
|
|||||||
<ChatSettingsSection />
|
<ChatSettingsSection />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent
|
||||||
|
value="sharing"
|
||||||
|
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
|
||||||
|
>
|
||||||
|
<SessionSharingSection />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="app"
|
value="app"
|
||||||
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
|
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+17
-2
@@ -297,6 +297,8 @@ async function processProtocolUrl(parsedUrl: URL, window: BrowserWindow) {
|
|||||||
|
|
||||||
if (parsedUrl.hostname === 'extension') {
|
if (parsedUrl.hostname === 'extension') {
|
||||||
window.webContents.send('add-extension', pendingDeepLink);
|
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') {
|
} else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
|
||||||
const recipeDeeplink = parsedUrl.searchParams.get('config');
|
const recipeDeeplink = parsedUrl.searchParams.get('config');
|
||||||
const scheduledJobId = parsedUrl.searchParams.get('scheduledJob');
|
const scheduledJobId = parsedUrl.searchParams.get('scheduledJob');
|
||||||
@@ -359,6 +361,8 @@ app.on('open-url', async (_event, url) => {
|
|||||||
|
|
||||||
if (parsedUrl.hostname === 'extension') {
|
if (parsedUrl.hostname === 'extension') {
|
||||||
firstOpenWindow.webContents.send('add-extension', pendingDeepLink);
|
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 = () => {
|
const getVersion = () => {
|
||||||
|
// checks app env for sharing url
|
||||||
loadShellEnv(app.isPackaged); // will try to take it from the zshrc file
|
loadShellEnv(app.isPackaged); // will try to take it from the zshrc file
|
||||||
// to in the env at bundle time
|
// to in the env at bundle time
|
||||||
return process.env.GOOSE_VERSION;
|
return process.env.GOOSE_VERSION;
|
||||||
@@ -469,6 +482,8 @@ const getVersion = () => {
|
|||||||
|
|
||||||
const [provider, model, predefinedModels] = getGooseProvider();
|
const [provider, model, predefinedModels] = getGooseProvider();
|
||||||
|
|
||||||
|
const sharingUrl = getSharingUrl();
|
||||||
|
|
||||||
const gooseVersion = getVersion();
|
const gooseVersion = getVersion();
|
||||||
|
|
||||||
const SERVER_SECRET = process.env.GOOSE_EXTERNAL_BACKEND
|
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_PORT: port, // Ensure this specific window gets the correct port
|
||||||
GOOSE_WORKING_DIR: working_dir,
|
GOOSE_WORKING_DIR: working_dir,
|
||||||
REQUEST_DIR: dir,
|
REQUEST_DIR: dir,
|
||||||
|
GOOSE_BASE_URL_SHARE: sharingUrl,
|
||||||
GOOSE_VERSION: gooseVersion,
|
GOOSE_VERSION: gooseVersion,
|
||||||
recipe: recipe,
|
recipe: recipe,
|
||||||
}),
|
}),
|
||||||
@@ -648,7 +663,7 @@ const createChat = async (
|
|||||||
GOOSE_PORT: port, // Ensure this specific window's config gets the correct port
|
GOOSE_PORT: port, // Ensure this specific window's config gets the correct port
|
||||||
GOOSE_WORKING_DIR: working_dir,
|
GOOSE_WORKING_DIR: working_dir,
|
||||||
REQUEST_DIR: dir,
|
REQUEST_DIR: dir,
|
||||||
|
GOOSE_BASE_URL_SHARE: sharingUrl,
|
||||||
recipe: recipe,
|
recipe: recipe,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -181,7 +181,7 @@ const handleViewTypeDeepLink = (viewType: string, recipeConfig: unknown) => {
|
|||||||
recipes: '#/recipes',
|
recipes: '#/recipes',
|
||||||
permission: '#/permission',
|
permission: '#/permission',
|
||||||
ConfigureProviders: '#/configure-providers',
|
ConfigureProviders: '#/configure-providers',
|
||||||
|
sharedSession: '#/shared-session',
|
||||||
recipeEditor: '#/recipe-editor',
|
recipeEditor: '#/recipe-editor',
|
||||||
welcome: '#/welcome',
|
welcome: '#/welcome',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type View =
|
|||||||
| 'settingsV2'
|
| 'settingsV2'
|
||||||
| 'sessions'
|
| 'sessions'
|
||||||
| 'schedules'
|
| 'schedules'
|
||||||
|
| 'sharedSession'
|
||||||
| 'loading'
|
| 'loading'
|
||||||
| 'recipeEditor'
|
| 'recipeEditor'
|
||||||
| 'recipes'
|
| 'recipes'
|
||||||
@@ -60,6 +61,9 @@ export const createNavigationHandler = (navigate: NavigateFunction) => {
|
|||||||
case 'ConfigureProviders':
|
case 'ConfigureProviders':
|
||||||
navigate('/configure-providers', { state: options });
|
navigate('/configure-providers', { state: options });
|
||||||
break;
|
break;
|
||||||
|
case 'sharedSession':
|
||||||
|
navigate('/shared-session', { state: options });
|
||||||
|
break;
|
||||||
case 'recipeEditor':
|
case 'recipeEditor':
|
||||||
navigate('/recipe-editor', { state: options });
|
navigate('/recipe-editor', { state: options });
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user