From c31ec855d2e0bee1de80a3a5a05c4346d4c5a242 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Fri, 14 Aug 2026 16:47:49 +0200 Subject: [PATCH] fix: keep AI Fix usable in-session and stop silent no-op hand-offs Co-Authored-By: Claude Opus 5 (1M context) --- frontend/src/lib/components/Editor.svelte | 4 +- .../components/copilot/AIFormAssistant.svelte | 16 +- .../lib/components/copilot/ScriptFix.svelte | 140 ++++++++++-------- .../copilot/chat/AIChatManager.svelte.ts | 8 + .../components/raw_apps/RawAppEditor.svelte | 9 +- .../sessions/OpenInSessionButton.svelte | 2 +- .../apps_raw/edit/[...path]/+page.svelte | 7 +- 7 files changed, 114 insertions(+), 72 deletions(-) diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index 729497ce6e..987aa937eb 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -266,7 +266,9 @@ const absolutePathExtraLibs = new Map void }>() const dispatch = createEventDispatcher() // Set by the sessions pane; undefined everywhere else. ⌘L targets it so the - // shortcut reaches the chat the user is actually looking at. + // shortcut reaches the chat the user is actually looking at. Read directly + // rather than via getAiChatManager(), which collapses "no session" into the + // singleton — the distinction is what tells ⌘L whether a pane exists to open. const sessionScopedChatManager = getContext('aiChatManager') // let graphqlService: MonacoGraphQLAPI | undefined = undefined diff --git a/frontend/src/lib/components/copilot/AIFormAssistant.svelte b/frontend/src/lib/components/copilot/AIFormAssistant.svelte index e4ad9bdfcc..422c093265 100644 --- a/frontend/src/lib/components/copilot/AIFormAssistant.svelte +++ b/frontend/src/lib/components/copilot/AIFormAssistant.svelte @@ -2,8 +2,8 @@ import { Button } from '$lib/components/common' import { Pencil, WandSparkles } from 'lucide-svelte' import { aiChatManager } from './chat/AIChatManager.svelte' - import AskAiButton from './AskAiButton.svelte' import OpenInSessionButton from '$lib/components/sessions/OpenInSessionButton.svelte' + import { AIBtnClasses } from './chat/AIButtonStyle' import { workspaceStore } from '$lib/stores' interface Props { @@ -41,13 +41,23 @@

Fill the inputs with AI

+ {#snippet fallback()} - + {/snippet}
diff --git a/frontend/src/lib/components/copilot/ScriptFix.svelte b/frontend/src/lib/components/copilot/ScriptFix.svelte index bfa4773dbf..5b7f1a01dc 100644 --- a/frontend/src/lib/components/copilot/ScriptFix.svelte +++ b/frontend/src/lib/components/copilot/ScriptFix.svelte @@ -7,11 +7,12 @@ import Popover from '$lib/components/meltComponents/Popover.svelte' import { autoPlacement } from '@floating-ui/core' import { WandSparkles } from 'lucide-svelte' - import { aiChatManager } from './chat/AIChatManager.svelte' + import { aiChatManager, type AIChatManager } from './chat/AIChatManager.svelte' import { copilotInfo } from '$lib/aiStore' import OpenInSessionButton from '$lib/components/sessions/OpenInSessionButton.svelte' import { getOpenInSessionHandoff } from '$lib/components/sessions/openInSessionContext' import { AIBtnClasses } from './chat/AIButtonStyle' + import { getContext } from 'svelte' let { lang, @@ -31,70 +32,87 @@ // standalone script, FlowBuilder for a step), seeded with the error so the // session lands with a ready-to-send prompt the user can still edit. const handoff = getOpenInSessionHandoff() + const seedPrompt = $derived( + error ? `Fix this error:\n\n\`\`\`\n${error}\n\`\`\`` : 'Fix the error from the last run.' + ) const sessionSource = $derived.by(() => { const source = handoff?.source({ moduleId }) - if (!source) return undefined - return { - ...source, - seedPrompt: error - ? `Fix this error:\n\n\`\`\`\n${error}\n\`\`\`` - : 'Fix the error from the last run.' - } + return source ? { ...source, seedPrompt } : undefined }) + + // Inside a session pane the chat is already beside this panel, so there is + // nothing to hand off to: drop the error into that chat's composer instead. + // OpenInSessionButton renders nothing there, which would otherwise leave the + // sessions population with no fix affordance at all. + const sessionScopedManager = getContext('aiChatManager') {#if SUPPORTED_LANGUAGES.has(lang)} - - {#snippet fallback()} - - {#snippet trigger()} -
- -
- {/snippet} - {#snippet content()} -
-
-

Enable Windmill AI in the workspace settings

-
- {/snippet} -
- {/snippet} -
+ {#if sessionScopedManager} + + {:else} + + {#snippet fallback()} + + {#snippet trigger()} +
+ +
+ {/snippet} + {#snippet content()} +
+
+

Enable Windmill AI in the workspace settings

+
+ {/snippet} +
+ {/snippet} +
+ {/if} {/if} diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index ec5115d336..57e0f9e866 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -1707,6 +1707,14 @@ export class AIChatManager { return false } + /** Drop `text` into the composer for the user to review and send. Survives a + * collapsed or not-yet-mounted chat (the queue restores it with the panel), + * so an entry point seeding a prompt never silently loses it. */ + seedComposer(text: string) { + this.restoreToInput(text) + this.focusInput() + } + focusInput() { if (this.aiChatInput) { this.aiChatInput.focusInput() diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index a5e0f7f626..185ea0585d 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -247,10 +247,13 @@ /** Hand this app off to a fresh AI session with `seedPrompt` pre-filled. * Exposed for the template picker's "Start with AI": the route owns the - * prompt, but the draft persistence the preview depends on lives here. */ - export async function openInSession(seedPrompt: string): Promise { - if (!sessionOpen) return + * prompt, but the draft persistence the preview depends on lives here. + * False when there is no path to open yet, so the caller can fall back + * rather than swallow the click. */ + export async function openInSession(seedPrompt: string): Promise { + if (!sessionOpen) return false await openSourceInSession(sessionOpen, { seedPrompt }) + return true } // Convert to object format for child components diff --git a/frontend/src/lib/components/sessions/OpenInSessionButton.svelte b/frontend/src/lib/components/sessions/OpenInSessionButton.svelte index bde6bff076..89a0c3a7bf 100644 --- a/frontend/src/lib/components/sessions/OpenInSessionButton.svelte +++ b/frontend/src/lib/components/sessions/OpenInSessionButton.svelte @@ -75,7 +75,7 @@ togglePanel={open} btnClasses={btnClasses ?? AIBtnClasses('default')} {btnProps} - label={label ?? 'Open in AI session'} + {label} /> {:else if !inSessionPanel} {@render fallback?.()} diff --git a/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte index 781b606eb8..72e4175843 100644 --- a/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte @@ -506,10 +506,11 @@ // The delay lets the remount above settle: the session hand-off persists // the draft the preview loads, and the docked path needs the editor to // have registered its app helpers. - setTimeout(() => { + setTimeout(async () => { + // Falls through when the hand-off has no path to open, so the click + // still reaches the legacy path (or its toast) instead of vanishing. if (prefersSessionHandoff($userStore?.operator)) { - void rawAppEditor?.openInSession(prompt) - return + if (await rawAppEditor?.openInSession(prompt)) return } aiChatManager.changeMode(AIMode.APP) if (!aiChatManager.open) aiChatManager.toggleOpen()