Goose recover (#5450)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2025-10-30 18:12:46 -04:00
committed by GitHub
parent 6ea0594fb4
commit 4201e80e8e
8 changed files with 95 additions and 78 deletions
+2 -23
View File
@@ -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();
} }
}; };
+2 -2
View File
@@ -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;
} }
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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 }
);
}