diff --git a/frontend/src/lib/components/home/HomeAIChat.svelte b/frontend/src/lib/components/home/HomeAIChat.svelte
index d69cbeb776..e7e45d0aa4 100644
--- a/frontend/src/lib/components/home/HomeAIChat.svelte
+++ b/frontend/src/lib/components/home/HomeAIChat.svelte
@@ -82,10 +82,11 @@
inputProps={{ rows: 4, placeholder, onkeydown: onKeydown }}
/>
diff --git a/frontend/src/lib/components/sessions/SessionWrapper.svelte b/frontend/src/lib/components/sessions/SessionWrapper.svelte
index f5fa09c6d0..cfb8640d20 100644
--- a/frontend/src/lib/components/sessions/SessionWrapper.svelte
+++ b/frontend/src/lib/components/sessions/SessionWrapper.svelte
@@ -40,7 +40,8 @@
selectSession,
sessionState,
setSessionArchived,
- syncWorkspaceTo
+ syncWorkspaceTo,
+ takeAutoSendPrompt
} from './sessionState.svelte'
import { getOrCreateRuntime, removeSession } from './sessionRuntime.svelte'
import { goto } from '$lib/navigation'
@@ -73,6 +74,12 @@
// transient draft slot (script-init: AIChatInput reads it once at mount).
const restoredDraftPrompt = peekTransientDraftPrompt(sessionId)
+ // One-shot: a prompt this session was created to auto-send (home composer).
+ // Read once at init and cleared; the effect below fires it when the chat is
+ // ready. Sent through the manager, so the composer stays empty (no prefill) —
+ // which is why initialInstructions is suppressed when this is set.
+ const autoSendPrompt = takeAutoSendPrompt(sessionId)
+
// The workspace the session acts on, shown in the header "Acting on" strip via the shared
// WorkspaceScopeTrigger chip. `targetId` is also the workspace the chip's ellipsis menu targets.
const acting = $derived.by(() => {
@@ -239,6 +246,21 @@
setTimeout(() => chat.focusInput(), 0)
})
+ // Auto-send the prompt this session was created with, once the chat is ready
+ // (same readiness gate as the focus effect: mounted + copilot loaded). Latched
+ // so it fires exactly once; guarded on an empty conversation so it can never
+ // interleave with a message the user already sent.
+ let autoSent = false
+ $effect(() => {
+ if (!autoSendPrompt || autoSent) return
+ if (sessionState.currentSessionId !== sessionId) return
+ if (!aiChat || !$copilotInfo.enabled) return
+ if (hasFirstUserMessage) return
+ autoSent = true
+ const chat = aiChat
+ setTimeout(() => chat.sendRequest({ instructions: autoSendPrompt }), 0)
+ })
+
// True when the session committed to a workspace that's no longer in
// the user's list (deleted / archived / access revoked). The chat is
// disabled and SessionChangesBar shows a move/discard banner.
@@ -414,7 +436,7 @@
hideHeader
hideModeSelector
wideLayout
- initialInstructions={restoredDraftPrompt}
+ initialInstructions={autoSendPrompt ? undefined : restoredDraftPrompt}
onDraftChange={(text) => queueTransientDraftPrompt(sessionId, text)}
forceDisabled={isUnavailable || !!session.archived}
forceDisabledMessage={isUnavailable
diff --git a/frontend/src/lib/components/sessions/sessionState.svelte.ts b/frontend/src/lib/components/sessions/sessionState.svelte.ts
index 8382c201ce..7476136652 100644
--- a/frontend/src/lib/components/sessions/sessionState.svelte.ts
+++ b/frontend/src/lib/components/sessions/sessionState.svelte.ts
@@ -341,6 +341,24 @@ export function peekTransientDraftPrompt(sessionId: string): string | undefined
return transientPrompt?.sessionId === sessionId ? transientPrompt.text : undefined
}
+// One-shot intent to auto-send a prompt as soon as a freshly-created session's
+// chat is ready (set by startSessionWithPrompt for the home composer). Held in
+// memory only — deliberately NOT persisted, so a reload never re-fires a send
+// the user didn't just trigger. Consumed once via takeAutoSendPrompt.
+let autoSendPrompt: { sessionId: string; text: string } | undefined
+export function queueAutoSendPrompt(sessionId: string, text: string): void {
+ autoSendPrompt = { sessionId, text }
+}
+
+// Take (read-and-clear) the auto-send prompt for a session. One-shot: the second
+// caller — a re-mount, or a different session — gets undefined.
+export function takeAutoSendPrompt(sessionId: string): string | undefined {
+ if (autoSendPrompt?.sessionId !== sessionId) return undefined
+ const text = autoSendPrompt.text
+ autoSendPrompt = undefined
+ return text
+}
+
// Write-behind a single session record. Transient (unsent) sessions are not
// written to IndexedDB — they live in memory plus a single localStorage draft
// slot until materializeTransient() promotes them at first send.
diff --git a/frontend/src/lib/components/sessions/sessionSwitch.svelte.ts b/frontend/src/lib/components/sessions/sessionSwitch.svelte.ts
index c05c7f7175..3915e54a67 100644
--- a/frontend/src/lib/components/sessions/sessionSwitch.svelte.ts
+++ b/frontend/src/lib/components/sessions/sessionSwitch.svelte.ts
@@ -3,7 +3,7 @@ import { goto } from '$lib/navigation'
import { workspaceStore } from '$lib/stores'
import {
createSession,
- queueTransientDraftPrompt,
+ queueAutoSendPrompt,
selectSession,
sessionInCurrentFamily,
sessionState,
@@ -50,15 +50,15 @@ export async function enterSessionMode(opts?: { replace?: boolean }): Promise {
const session = createSession()
const text = prompt.trim()
- if (text) queueTransientDraftPrompt(session.id, text)
+ if (text) queueAutoSendPrompt(session.id, text)
selectSession(session.id)
await goto(`/sessions?session_name=${encodeURIComponent(session.name)}`)
}