From 2a08aa59b4b3d08ed5153eddf84ff0ee206fe8db Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Thu, 9 Jul 2026 14:04:32 +0200 Subject: [PATCH] auto send prompt --- .../src/lib/components/home/HomeAIChat.svelte | 3 ++- .../components/sessions/SessionWrapper.svelte | 26 +++++++++++++++++-- .../sessions/sessionState.svelte.ts | 18 +++++++++++++ .../sessions/sessionSwitch.svelte.ts | 14 +++++----- 4 files changed, 51 insertions(+), 10 deletions(-) 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)}`) }