Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { UserInput } from './types/message';
interface PairRouteState {
resumeSessionId?: string;
initialMessage?: UserInput;
noAutoSubmit?: boolean;
}
import SettingsView, { SettingsViewOptions } from './components/settings/SettingsView';
import SessionsView from './components/sessions/SessionsView';
Expand Down Expand Up @@ -84,8 +85,11 @@ const PairRouteWrapper = ({
activeSessions: Array<{
sessionId: string;
initialMessage?: UserInput;
noAutoSubmit?: boolean;
}>;
setActiveSessions: (sessions: Array<{ sessionId: string; initialMessage?: UserInput }>) => void;
setActiveSessions: (
sessions: Array<{ sessionId: string; initialMessage?: UserInput; noAutoSubmit?: boolean }>
) => void;
}) => {
const { extensionsList } = useConfig();
const location = useLocation();
Expand All @@ -98,6 +102,7 @@ const PairRouteWrapper = ({
const recipeDeeplinkFromConfig = window.appConfig?.get('recipeDeeplink') as string | undefined;
const recipeIdFromConfig = window.appConfig?.get('recipeId') as string | undefined;
const initialMessage = routeState.initialMessage;
const noAutoSubmit = routeState.noAutoSubmit;

// Create session if we have an initialMessage, recipeDeeplink, or recipeId but no sessionId
useEffect(() => {
Expand All @@ -122,6 +127,7 @@ const PairRouteWrapper = ({
detail: {
sessionId: newSession.id,
initialMessage: sessionInitialMessage,
noAutoSubmit,
},
})
);
Expand Down Expand Up @@ -162,11 +168,12 @@ const PairRouteWrapper = ({
detail: {
sessionId: resumeSessionId,
initialMessage: initialMessage,
noAutoSubmit,
},
})
);
}
}, [resumeSessionId, activeSessions, initialMessage]);
}, [resumeSessionId, activeSessions, initialMessage, noAutoSubmit]);

return null;
};
Expand Down Expand Up @@ -358,15 +365,16 @@ export function AppInner() {
const MAX_ACTIVE_SESSIONS = 10;

const [activeSessions, setActiveSessions] = useState<
Array<{ sessionId: string; initialMessage?: UserInput }>
Array<{ sessionId: string; initialMessage?: UserInput; noAutoSubmit?: boolean }>
>([]);

useEffect(() => {
const handleAddActiveSession = (event: Event) => {
const { sessionId, initialMessage } = (
const { sessionId, initialMessage, noAutoSubmit } = (
event as CustomEvent<{
sessionId: string;
initialMessage?: UserInput;
noAutoSubmit?: boolean;
}>
).detail;

Expand All @@ -380,7 +388,7 @@ export function AppInner() {
}

// New session - add to end with LRU eviction if needed
const newSession = { sessionId, initialMessage };
const newSession = { sessionId, initialMessage, noAutoSubmit };
const updated = [...prev, newSession];
if (updated.length > MAX_ACTIVE_SESSIONS) {
return updated.slice(updated.length - MAX_ACTIVE_SESSIONS);
Expand Down Expand Up @@ -496,13 +504,18 @@ export function AppInner() {
// Show a toast if mesh is the configured provider but isn't running.
useEffect(() => {
const handler = () => {
toast.warn('Inference Mesh is set as your provider but isn\'t running. Open Settings → Mesh to start it. Keep goose running to stay connected.', {
autoClose: false,
toastId: 'mesh-not-running',
});
toast.warn(
"Inference Mesh is set as your provider but isn't running. Open Settings → Mesh to start it. Keep goose running to stay connected.",
{
autoClose: false,
toastId: 'mesh-not-running',
}
);
};
window.electron.on('mesh-not-running', handler);
return () => { window.electron.off('mesh-not-running', handler); };
return () => {
window.electron.off('mesh-not-running', handler);
};
}, []);

// Prevent default drag and drop behavior globally to avoid opening files in new windows
Expand Down Expand Up @@ -606,12 +619,14 @@ export function AppInner() {
useEffect(() => {
const handleSetInitialMessage = async (_event: IpcRendererEvent, ...args: unknown[]) => {
const initialMessage = args[0] as string;
const options = (args[1] as { noAutoSubmit?: boolean } | undefined) || {};

if (initialMessage && !isProcessingRef.current) {
isProcessingRef.current = true;
navigate('/pair', {
state: {
initialMessage: { msg: initialMessage, images: [] },
noAutoSubmit: options.noAutoSubmit,
},
});
setTimeout(() => {
Expand Down
58 changes: 33 additions & 25 deletions ui/desktop/src/components/BaseChat.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { AppEvents } from '../constants/events';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { defineMessages, useIntl } from '../i18n';
import { useLocation, useNavigate } from 'react-router-dom';
import { SearchView } from './conversation/SearchView';
Expand Down Expand Up @@ -76,6 +70,7 @@ interface BaseChatProps {
sessionId: string;
isActiveSession: boolean;
initialMessage?: UserInput;
noAutoSubmit?: boolean;
}

export default function BaseChat({
Expand All @@ -85,6 +80,7 @@ export default function BaseChat({
customMainLayoutProps = {},
sessionId,
initialMessage,
noAutoSubmit,
isActiveSession,
}: BaseChatProps) {
const intl = useIntl();
Expand Down Expand Up @@ -136,7 +132,13 @@ export default function BaseChat({
return initialMessage;
}, [initialMessage, recipe?.prompt, session?.user_recipe_values]);

const canAutoSubmit = session?.session_type === 'scheduled' || !recipe || hasNotAcceptedRecipe === false;
// noAutoSubmit only suppresses auto-submitting the initial prompt of a fresh session
// (goose://new-session?prompt=...). Once the conversation has messages, later flows
// such as forks or resumes should auto-submit normally.
const suppressInitialAutoSubmit = noAutoSubmit && messages.length === 0;
const canAutoSubmit =
!suppressInitialAutoSubmit &&
(session?.session_type === 'scheduled' || !recipe || hasNotAcceptedRecipe === false);

useAutoSubmit({
sessionId,
Expand Down Expand Up @@ -201,7 +203,11 @@ export default function BaseChat({
const latestInference = useMemo(() => {
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message.role === 'assistant' && message.metadata.userVisible && message.metadata.inference) {
if (
message.role === 'assistant' &&
message.metadata.userVisible &&
message.metadata.inference
) {
return message.metadata.inference;
}
}
Expand Down Expand Up @@ -360,7 +366,10 @@ export default function BaseChat({
: recipe.prompt;
}

const initialPrompt = recipePrompt;
const initialPrompt =
noAutoSubmit && messages.length === 0 && resolvedInitialMessage?.msg
? resolvedInitialMessage.msg
: recipePrompt;

if (sessionLoadError) {
return (
Expand All @@ -375,7 +384,9 @@ export default function BaseChat({
<div className="flex-1 flex items-center justify-center">
<div className="flex flex-col items-center justify-center p-8">
<div className="text-red-700 dark:text-red-300 bg-red-400/50 p-4 rounded-lg mb-4 max-w-md">
<h3 className="font-semibold mb-2">{intl.formatMessage(i18n.failedToLoadSession)}</h3>
<h3 className="font-semibold mb-2">
{intl.formatMessage(i18n.failedToLoadSession)}
</h3>
<p className="text-sm">{sessionLoadError}</p>
</div>
<button
Expand Down Expand Up @@ -509,14 +520,11 @@ export default function BaseChat({
accumulatedOutputTokens={
tokenState?.accumulatedOutputTokens ?? session?.accumulated_output_tokens ?? undefined
}
accumulatedCost={
tokenState?.accumulatedCost ?? session?.accumulated_cost ?? undefined
}
accumulatedCost={tokenState?.accumulatedCost ?? session?.accumulated_cost ?? undefined}
droppedFiles={droppedFiles}
onFilesProcessed={() => setDroppedFiles([])} // Clear dropped files after processing
messages={messages}
disableAnimation={disableAnimation}

recipe={recipe}
recipeAccepted={!hasNotAcceptedRecipe}
initialPrompt={initialPrompt}
Expand Down Expand Up @@ -548,16 +556,16 @@ export default function BaseChat({
recipe.parameters.length > 0 &&
!session?.user_recipe_values &&
session?.session_type !== 'scheduled' && (
<ParameterInputModal
parameters={recipe.parameters}
onSubmit={setRecipeUserParams}
onClose={() => setView('chat')}
initialValues={
(window.appConfig?.get('recipeParameters') as Record<string, string> | undefined) ||
undefined
}
/>
)}
<ParameterInputModal
parameters={recipe.parameters}
onSubmit={setRecipeUserParams}
onClose={() => setView('chat')}
initialValues={
(window.appConfig?.get('recipeParameters') as Record<string, string> | undefined) ||
undefined
}
/>
)}

<CreateRecipeFromSessionModal
isOpen={isCreateRecipeModalOpen}
Expand Down
2 changes: 2 additions & 0 deletions ui/desktop/src/components/ChatSessionsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface ChatSessionsContainerProps {
activeSessions: Array<{
sessionId: string;
initialMessage?: UserInput;
noAutoSubmit?: boolean;
}>;
}

Expand Down Expand Up @@ -51,6 +52,7 @@ export default function ChatSessionsContainer({
setChat={setChat}
sessionId={session.sessionId}
initialMessage={session.initialMessage}
noAutoSubmit={session.noAutoSubmit}
suppressEmptyState={false}
isActiveSession={isVisible}
/>
Expand Down
2 changes: 2 additions & 0 deletions ui/desktop/src/components/Layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface AppLayoutContentProps {
activeSessions: Array<{
sessionId: string;
initialMessage?: UserInput;
noAutoSubmit?: boolean;
}>;
}

Expand Down Expand Up @@ -116,6 +117,7 @@ interface AppLayoutProps {
activeSessions: Array<{
sessionId: string;
initialMessage?: UserInput;
noAutoSubmit?: boolean;
}>;
}

Expand Down
31 changes: 27 additions & 4 deletions ui/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,12 @@ if (process.platform !== 'darwin') {
app.whenReady().then(async () => {
const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
await createChat(app, { dir: openDir || undefined });
const prompt = parsedUrl.searchParams.get('prompt') || undefined;
await createChat(app, {
dir: openDir || undefined,
initialMessage: prompt,
initialMessageNoAutoSubmit: prompt !== undefined,
});
});
return;
}
Expand Down Expand Up @@ -514,7 +519,12 @@ async function handleProtocolUrl(url: string, parsedUrl: URL) {
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;

if (parsedUrl.hostname === 'new-session') {
await createChat(app, { dir: openDir || undefined });
const prompt = parsedUrl.searchParams.get('prompt') || undefined;
await createChat(app, {
dir: openDir || undefined,
initialMessage: prompt,
initialMessageNoAutoSubmit: prompt !== undefined,
});
return;
} else if (parsedUrl.hostname === 'resume') {
await createResumeChatWindow(parsedUrl, openDir || undefined);
Expand Down Expand Up @@ -588,7 +598,12 @@ app.on('open-url', async (_event, url) => {
if (parsedUrl.hostname === 'new-session') {
log.info('[Main] Detected new-session URL, creating new chat window');
openUrlHandledLaunch = true;
await createChat(app, { dir: openDir || undefined });
const prompt = parsedUrl.searchParams.get('prompt') || undefined;
await createChat(app, {
dir: openDir || undefined,
initialMessage: prompt,
initialMessageNoAutoSubmit: prompt !== undefined,
});
return;
}

Expand Down Expand Up @@ -855,9 +870,11 @@ const releaseWindowGoosedLease = async (windowId: number) => {
const windowPowerSaveBlockers = new Map<number, number>(); // windowId -> blockerId
// Track pending initial messages per window
const pendingInitialMessages = new Map<number, string>(); // windowId -> initialMessage
const pendingInitialMessageNoAutoSubmit = new Set<number>(); // windowIds whose initialMessage should NOT auto-submit

interface CreateChatOptions {
initialMessage?: string;
initialMessageNoAutoSubmit?: boolean;
dir?: string;
resumeSessionId?: string;
viewType?: string;
Expand All @@ -870,6 +887,7 @@ interface CreateChatOptions {
const createChat = async (app: App, options: CreateChatOptions = {}) => {
const {
initialMessage,
initialMessageNoAutoSubmit,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve noAutoSubmit during backend retry

When a goose://new-session?prompt=... window is opened while an external backend is enabled but unreachable, choosing “Disable External Backend & Retry” later recurses with createChat(app, { initialMessage, dir }) and drops this new flag. The retried window then sends set-initial-message without noAutoSubmit, so the deeplink prompt is auto-submitted even though this path was explicitly meant to prefill without running; please forward initialMessageNoAutoSubmit through that retry path.

Useful? React with 👍 / 👎.

dir,
resumeSessionId,
viewType,
Expand Down Expand Up @@ -1226,6 +1244,9 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => {
// If we have an initial message, store it to send after React is ready
if (initialMessage) {
pendingInitialMessages.set(mainWindow.id, initialMessage);
if (initialMessageNoAutoSubmit) {
pendingInitialMessageNoAutoSubmit.add(mainWindow.id);
}
}

// Set up local keyboard shortcuts that only work when the window is focused
Expand Down Expand Up @@ -1636,9 +1657,11 @@ ipcMain.on('react-ready', (event) => {
// Send any pending initial message for this window
if (windowId && pendingInitialMessages.has(windowId)) {
const initialMessage = pendingInitialMessages.get(windowId)!;
const noAutoSubmit = pendingInitialMessageNoAutoSubmit.has(windowId);
log.info('Sending pending initial message to window:', initialMessage);
window.webContents.send('set-initial-message', initialMessage);
window.webContents.send('set-initial-message', initialMessage, { noAutoSubmit });
pendingInitialMessages.delete(windowId);
pendingInitialMessageNoAutoSubmit.delete(windowId);
}

if (windowId && pendingDeepLinks.has(windowId) && window) {
Expand Down
Loading