Initial prompt with goose://new-session (#9427)

Signed-off-by: Douwe M Osinga <douwe@sidewalklabs.com>
Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
Jack Amadeo
2026-06-17 14:00:44 -04:00
committed by GitHub
parent 3d405249a6
commit 177cf3d4b8
5 changed files with 89 additions and 39 deletions
+25 -10
View File
@@ -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';
@@ -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();
@@ -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(() => {
@@ -122,6 +127,7 @@ const PairRouteWrapper = ({
detail: {
sessionId: newSession.id,
initialMessage: sessionInitialMessage,
noAutoSubmit,
},
})
);
@@ -162,11 +168,12 @@ const PairRouteWrapper = ({
detail: {
sessionId: resumeSessionId,
initialMessage: initialMessage,
noAutoSubmit,
},
})
);
}
}, [resumeSessionId, activeSessions, initialMessage]);
}, [resumeSessionId, activeSessions, initialMessage, noAutoSubmit]);
return null;
};
@@ -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;
@@ -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);
@@ -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
@@ -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(() => {
+33 -25
View File
@@ -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';
@@ -76,6 +70,7 @@ interface BaseChatProps {
sessionId: string;
isActiveSession: boolean;
initialMessage?: UserInput;
noAutoSubmit?: boolean;
}
export default function BaseChat({
@@ -85,6 +80,7 @@ export default function BaseChat({
customMainLayoutProps = {},
sessionId,
initialMessage,
noAutoSubmit,
isActiveSession,
}: BaseChatProps) {
const intl = useIntl();
@@ -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,
@@ -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;
}
}
@@ -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 (
@@ -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
@@ -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}
@@ -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}
@@ -8,6 +8,7 @@ interface ChatSessionsContainerProps {
activeSessions: Array<{
sessionId: string;
initialMessage?: UserInput;
noAutoSubmit?: boolean;
}>;
}
@@ -51,6 +52,7 @@ export default function ChatSessionsContainer({
setChat={setChat}
sessionId={session.sessionId}
initialMessage={session.initialMessage}
noAutoSubmit={session.noAutoSubmit}
suppressEmptyState={false}
isActiveSession={isVisible}
/>
@@ -24,6 +24,7 @@ interface AppLayoutContentProps {
activeSessions: Array<{
sessionId: string;
initialMessage?: UserInput;
noAutoSubmit?: boolean;
}>;
}
@@ -116,6 +117,7 @@ interface AppLayoutProps {
activeSessions: Array<{
sessionId: string;
initialMessage?: UserInput;
noAutoSubmit?: boolean;
}>;
}
+27 -4
View File
@@ -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;
}
@@ -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);
@@ -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;
}
@@ -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;
@@ -870,6 +887,7 @@ interface CreateChatOptions {
const createChat = async (app: App, options: CreateChatOptions = {}) => {
const {
initialMessage,
initialMessageNoAutoSubmit,
dir,
resumeSessionId,
viewType,
@@ -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
@@ -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) {