Goose recover (#5450)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -19,7 +19,7 @@ import ChatInput from './ChatInput';
|
|||||||
import { ChatState } from '../types/chatState';
|
import { ChatState } from '../types/chatState';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import { View, ViewOptions } from '../utils/navigationUtils';
|
import { View, ViewOptions } from '../utils/navigationUtils';
|
||||||
import { startAgent } from '../api';
|
import { startNewSession } from '../sessions';
|
||||||
|
|
||||||
export default function Hub({
|
export default function Hub({
|
||||||
setView,
|
setView,
|
||||||
@@ -37,28 +37,7 @@ export default function Hub({
|
|||||||
const combinedTextFromInput = customEvent.detail?.value || '';
|
const combinedTextFromInput = customEvent.detail?.value || '';
|
||||||
|
|
||||||
if (combinedTextFromInput.trim()) {
|
if (combinedTextFromInput.trim()) {
|
||||||
if (process.env.ALPHA) {
|
await startNewSession(combinedTextFromInput, resetChat, setView);
|
||||||
const newAgent = await startAgent({
|
|
||||||
body: {
|
|
||||||
working_dir: window.appConfig.get('GOOSE_WORKING_DIR') as string,
|
|
||||||
},
|
|
||||||
throwOnError: true,
|
|
||||||
});
|
|
||||||
const session = newAgent.data;
|
|
||||||
setView('pair', {
|
|
||||||
disableAnimation: true,
|
|
||||||
initialMessage: combinedTextFromInput,
|
|
||||||
resumeSessionId: session.id,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Navigate to pair page with the message to be submitted
|
|
||||||
// Pair will handle creating the new chat session
|
|
||||||
resetChat();
|
|
||||||
setView('pair', {
|
|
||||||
disableAnimation: true,
|
|
||||||
initialMessage: combinedTextFromInput,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { View, ViewOptions } from '../utils/navigationUtils';
|
|
||||||
import BaseChat from './BaseChat';
|
import BaseChat from './BaseChat';
|
||||||
import { useRecipeManager } from '../hooks/useRecipeManager';
|
import { useRecipeManager } from '../hooks/useRecipeManager';
|
||||||
import { useIsMobile } from '../hooks/use-mobile';
|
import { useIsMobile } from '../hooks/use-mobile';
|
||||||
@@ -10,6 +9,7 @@ import { cn } from '../utils';
|
|||||||
|
|
||||||
import { ChatType } from '../types/chat';
|
import { ChatType } from '../types/chat';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import type { setViewType } from '../hooks/useNavigation';
|
||||||
|
|
||||||
export interface PairRouteState {
|
export interface PairRouteState {
|
||||||
resumeSessionId?: string;
|
resumeSessionId?: string;
|
||||||
@@ -19,7 +19,7 @@ export interface PairRouteState {
|
|||||||
interface PairProps {
|
interface PairProps {
|
||||||
chat: ChatType;
|
chat: ChatType;
|
||||||
setChat: (chat: ChatType) => void;
|
setChat: (chat: ChatType) => void;
|
||||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
setView: setViewType;
|
||||||
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
|
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
|
||||||
setFatalError: (value: ((prevState: string | null) => string | null) | string | null) => void;
|
setFatalError: (value: ((prevState: string | null) => string | null) | string | null) => void;
|
||||||
setAgentWaitingMessage: (msg: string | null) => void;
|
setAgentWaitingMessage: (msg: string | null) => void;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { SearchView } from '../conversation/SearchView';
|
|||||||
import BackButton from '../ui/BackButton';
|
import BackButton from '../ui/BackButton';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip';
|
||||||
import { Message, Session } from '../../api';
|
import { Message, Session } from '../../api';
|
||||||
|
import { useNavigation } from '../../hooks/useNavigation';
|
||||||
|
|
||||||
// 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 => {
|
||||||
@@ -150,6 +151,8 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
|||||||
|
|
||||||
const messages = session.conversation || [];
|
const messages = session.conversation || [];
|
||||||
|
|
||||||
|
const setView = useNavigation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedSessionConfig = localStorage.getItem('session_sharing_config');
|
const savedSessionConfig = localStorage.getItem('session_sharing_config');
|
||||||
if (savedSessionConfig) {
|
if (savedSessionConfig) {
|
||||||
@@ -212,15 +215,14 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLaunchInNewWindow = () => {
|
const handleResumeSession = () => {
|
||||||
try {
|
try {
|
||||||
resumeSession(session);
|
resumeSession(session, setView);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(`Could not launch session: ${error instanceof Error ? error.message : error}`);
|
toast.error(`Could not launch session: ${error instanceof Error ? error.message : error}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Define action buttons
|
|
||||||
const actionButtons = showActionButtons ? (
|
const actionButtons = showActionButtons ? (
|
||||||
<>
|
<>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -254,7 +256,7 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
) : null}
|
) : null}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Button onClick={handleLaunchInNewWindow} size="sm" variant="outline">
|
<Button onClick={handleResumeSession} size="sm" variant="outline">
|
||||||
<Sparkles className="w-4 h-4" />
|
<Sparkles className="w-4 h-4" />
|
||||||
Resume
|
Resume
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
SessionInsights as ApiSessionInsights,
|
SessionInsights as ApiSessionInsights,
|
||||||
} from '../../api';
|
} from '../../api';
|
||||||
import { resumeSession } from '../../sessions';
|
import { resumeSession } from '../../sessions';
|
||||||
|
import { useNavigation } from '../../hooks/useNavigation';
|
||||||
|
|
||||||
export function SessionInsights() {
|
export function SessionInsights() {
|
||||||
const [insights, setInsights] = useState<ApiSessionInsights | null>(null);
|
const [insights, setInsights] = useState<ApiSessionInsights | null>(null);
|
||||||
@@ -21,6 +22,7 @@ export function SessionInsights() {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isLoadingSessions, setIsLoadingSessions] = useState(true);
|
const [isLoadingSessions, setIsLoadingSessions] = useState(true);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const setView = useNavigation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let loadingTimeout: ReturnType<typeof setTimeout>;
|
let loadingTimeout: ReturnType<typeof setTimeout>;
|
||||||
@@ -86,9 +88,7 @@ export function SessionInsights() {
|
|||||||
|
|
||||||
const handleSessionClick = async (session: Session) => {
|
const handleSessionClick = async (session: Session) => {
|
||||||
try {
|
try {
|
||||||
resumeSession(session, (sessionId: string) => {
|
resumeSession(session, setView);
|
||||||
navigate(`/pair?resumeSessionId=${sessionId}`);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to start session:', error);
|
console.error('Failed to start session:', error);
|
||||||
navigate('/sessions', {
|
navigate('/sessions', {
|
||||||
|
|||||||
@@ -32,11 +32,17 @@ export async function addToAgent(
|
|||||||
toastService.dismiss(toastId);
|
toastService.dismiss(toastId);
|
||||||
}
|
}
|
||||||
const errMsg = errorMessage(error);
|
const errMsg = errorMessage(error);
|
||||||
|
const recoverHints =
|
||||||
|
`Explain the following error: ${errMsg}. ` +
|
||||||
|
'This happened while trying to install an extension. Look out for issues that the ' +
|
||||||
|
"extension tried to run something faulty, didn't exist or there was trouble with " +
|
||||||
|
'the network configuration - VPNs like WARP often cause issues.';
|
||||||
const msg = errMsg.length < 70 ? errMsg : `Failed to add extension`;
|
const msg = errMsg.length < 70 ? errMsg : `Failed to add extension`;
|
||||||
toastService.error({
|
toastService.error({
|
||||||
title: extensionName,
|
title: extensionName,
|
||||||
msg: msg,
|
msg: msg,
|
||||||
traceback: errMsg,
|
traceback: errMsg,
|
||||||
|
recoverHints,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ export const useNavigation = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return createNavigationHandler(navigate);
|
return createNavigationHandler(navigate);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type setViewType = ReturnType<typeof useNavigation>;
|
||||||
|
|||||||
+39
-14
@@ -1,19 +1,17 @@
|
|||||||
import { Session } from './api';
|
import { Session, startAgent } from './api';
|
||||||
|
import type { setViewType } from './hooks/useNavigation';
|
||||||
|
|
||||||
export function resumeSession(
|
export function resumeSession(session: Session, setView: setViewType) {
|
||||||
session: Session,
|
if (process.env.ALPHA) {
|
||||||
navigateInSameWindow?: (sessionId: string) => void
|
setView('pair', {
|
||||||
) {
|
disableAnimation: true,
|
||||||
const workingDir = session.working_dir;
|
resumeSessionId: session.id,
|
||||||
if (!workingDir) {
|
});
|
||||||
throw new Error('Cannot resume session: working directory is missing in session');
|
|
||||||
}
|
|
||||||
|
|
||||||
// When ALPHA is true and we have a navigation callback, resume in the same window
|
|
||||||
// Otherwise, open in a new window (old behavior)
|
|
||||||
if (process.env.ALPHA && navigateInSameWindow) {
|
|
||||||
navigateInSameWindow(session.id);
|
|
||||||
} else {
|
} else {
|
||||||
|
const workingDir = session.working_dir;
|
||||||
|
if (!workingDir) {
|
||||||
|
throw new Error('Cannot resume session: working directory is missing in session');
|
||||||
|
}
|
||||||
window.electron.createChatWindow(
|
window.electron.createChatWindow(
|
||||||
undefined, // query
|
undefined, // query
|
||||||
workingDir,
|
workingDir,
|
||||||
@@ -22,3 +20,30 @@ export function resumeSession(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function startNewSession(
|
||||||
|
initialText: string | undefined,
|
||||||
|
resetChat: (() => void) | null,
|
||||||
|
setView: setViewType
|
||||||
|
) {
|
||||||
|
if (!resetChat || process.env.ALPHA) {
|
||||||
|
const newAgent = await startAgent({
|
||||||
|
body: {
|
||||||
|
working_dir: window.appConfig.get('GOOSE_WORKING_DIR') as string,
|
||||||
|
},
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
const session = newAgent.data;
|
||||||
|
setView('pair', {
|
||||||
|
disableAnimation: true,
|
||||||
|
initialMessage: initialText,
|
||||||
|
resumeSessionId: session.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resetChat();
|
||||||
|
setView('pair', {
|
||||||
|
disableAnimation: true,
|
||||||
|
initialMessage: initialText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+35
-32
@@ -1,12 +1,14 @@
|
|||||||
import { toast, ToastOptions } from 'react-toastify';
|
import { toast, ToastOptions } from 'react-toastify';
|
||||||
import { Button } from './components/ui/button';
|
import { Button } from './components/ui/button';
|
||||||
|
import { startNewSession } from './sessions';
|
||||||
|
import { useNavigation } from './hooks/useNavigation';
|
||||||
|
|
||||||
export interface ToastServiceOptions {
|
export interface ToastServiceOptions {
|
||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
shouldThrow?: boolean;
|
shouldThrow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ToastService {
|
class ToastService {
|
||||||
private silent: boolean = false;
|
private silent: boolean = false;
|
||||||
private shouldThrow: boolean = false;
|
private shouldThrow: boolean = false;
|
||||||
|
|
||||||
@@ -30,13 +32,13 @@ export default class ToastService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
error({ title, msg, traceback }: { title: string; msg: string; traceback: string }): void {
|
error(props: ToastErrorProps): void {
|
||||||
if (!this.silent) {
|
if (!this.silent) {
|
||||||
toastError({ title, msg, traceback });
|
toastError(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldThrow) {
|
if (this.shouldThrow) {
|
||||||
throw new Error(msg);
|
throw new Error(props.msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@ export default class ToastService {
|
|||||||
handleError(title: string, message: string, options: ToastServiceOptions = {}): void {
|
handleError(title: string, message: string, options: ToastServiceOptions = {}): void {
|
||||||
this.configure(options);
|
this.configure(options);
|
||||||
this.error({
|
this.error({
|
||||||
title: title || 'Error',
|
title: title,
|
||||||
msg: message,
|
msg: message,
|
||||||
traceback: message,
|
traceback: message,
|
||||||
});
|
});
|
||||||
@@ -88,6 +90,7 @@ const commonToastOptions: ToastOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ToastSuccessProps = { title?: string; msg?: string; toastOptions?: ToastOptions };
|
type ToastSuccessProps = { title?: string; msg?: string; toastOptions?: ToastOptions };
|
||||||
|
|
||||||
export function toastSuccess({ title, msg, toastOptions = {} }: ToastSuccessProps) {
|
export function toastSuccess({ title, msg, toastOptions = {} }: ToastSuccessProps) {
|
||||||
return toast.success(
|
return toast.success(
|
||||||
<div>
|
<div>
|
||||||
@@ -99,26 +102,42 @@ export function toastSuccess({ title, msg, toastOptions = {} }: ToastSuccessProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ToastErrorProps = {
|
type ToastErrorProps = {
|
||||||
title?: string;
|
title: string;
|
||||||
msg?: string;
|
msg: string;
|
||||||
traceback?: string;
|
traceback?: string;
|
||||||
toastOptions?: ToastOptions;
|
recoverHints?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function toastError({ title, msg, traceback, toastOptions }: ToastErrorProps) {
|
function ToastErrorContent({
|
||||||
return toast.error(
|
title,
|
||||||
|
msg,
|
||||||
|
traceback,
|
||||||
|
recoverHints,
|
||||||
|
}: Omit<ToastErrorProps, 'setView'>) {
|
||||||
|
const setView = useNavigation();
|
||||||
|
const showRecovery = recoverHints && setView;
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
{title ? <strong className="font-medium">{title}</strong> : null}
|
{title && <strong className="font-medium">{title}</strong>}
|
||||||
{msg ? <div>{msg}</div> : null}
|
{msg && <div>{msg}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-none flex items-center">
|
<div className="flex-none flex items-center gap-2">
|
||||||
{traceback ? (
|
{showRecovery ? (
|
||||||
|
<Button onClick={() => startNewSession(recoverHints, null, setView)}>Ask goose</Button>
|
||||||
|
) : traceback ? (
|
||||||
<Button onClick={() => navigator.clipboard.writeText(traceback)}>Copy error</Button>
|
<Button onClick={() => navigator.clipboard.writeText(traceback)}>Copy error</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>
|
||||||
{ ...commonToastOptions, autoClose: traceback ? false : 5000, ...toastOptions }
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toastError({ title, msg, traceback, recoverHints }: ToastErrorProps) {
|
||||||
|
return toast.error(
|
||||||
|
<ToastErrorContent title={title} msg={msg} traceback={traceback} recoverHints={recoverHints} />,
|
||||||
|
{ ...commonToastOptions, autoClose: traceback ? false : 5000 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,19 +156,3 @@ export function toastLoading({ title, msg, toastOptions }: ToastLoadingProps) {
|
|||||||
{ ...commonToastOptions, autoClose: false, ...toastOptions }
|
{ ...commonToastOptions, autoClose: false, ...toastOptions }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToastInfoProps = {
|
|
||||||
title?: string;
|
|
||||||
msg?: string;
|
|
||||||
toastOptions?: ToastOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function toastInfo({ title, msg, toastOptions }: ToastInfoProps) {
|
|
||||||
return toast.info(
|
|
||||||
<div>
|
|
||||||
{title ? <strong className="font-medium">{title}</strong> : null}
|
|
||||||
{msg ? <div>{msg}</div> : null}
|
|
||||||
</div>,
|
|
||||||
{ ...commonToastOptions, ...toastOptions }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user