fix: keep AI Fix usable in-session and stop silent no-op hand-offs

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-08-14 16:47:49 +02:00
co-authored by Claude Opus 5
parent 9755e1215a
commit c31ec855d2
7 changed files with 114 additions and 72 deletions
+3 -1
View File
@@ -266,7 +266,9 @@
const absolutePathExtraLibs = new Map<string, { dispose: () => 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 | undefined>('aiChatManager')
// let graphqlService: MonacoGraphQLAPI | undefined = undefined
@@ -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 @@
<div class="my-3 p-3 bg-surface-secondary rounded-md relative flex flex-col gap-3">
<div class="flex flex-row gap-2 justify-between items-center">
<h3 class="text-sm font-medium">Fill the inputs with AI</h3>
<!-- Labelled per branch: the hand-off runs the item, only the legacy path
actually fills the form. A plain Button rather than AskAiButton, whose
own session branch would fire here too and open an empty session. -->
<OpenInSessionButton
source={sessionSource}
label="Fill with AI"
label="Run with AI"
btnProps={{ iconOnly: false, startIcon: { icon: WandSparkles } }}
>
{#snippet fallback()}
<AskAiButton label="Fill with AI" onClick={fillFormWithAI} />
<Button
unifiedSize="md"
startIcon={{ icon: WandSparkles }}
btnClasses={AIBtnClasses('default')}
on:click={fillFormWithAI}
>
Fill with AI
</Button>
{/snippet}
</OpenInSessionButton>
</div>
@@ -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 | undefined>('aiChatManager')
</script>
{#if SUPPORTED_LANGUAGES.has(lang)}
<OpenInSessionButton
source={sessionSource}
btnClasses={AIBtnClasses('default')}
label="AI Fix"
btnProps={{ iconOnly: false, startIcon: { icon: WandSparkles } }}
>
{#snippet fallback()}
<Popover
floatingConfig={{
middleware: [
autoPlacement({
allowedPlacements: ['bottom-end', 'top-end']
})
]
}}
displayArrow={true}
>
{#snippet trigger()}
<div class="flex flex-row">
<Button
title="Fix code"
size="xs"
color="light"
spacingSize="xs2"
startIcon={{ icon: WandSparkles }}
on:click={() => {
if ($copilotInfo.enabled) {
aiChatManager.fix()
}
}}
btnClasses="text-ai bg-violet-100 dark:bg-gray-700 min-w-[84px]"
propagateEvent={!$copilotInfo.enabled}
>
AI Fix
</Button>
</div>
{/snippet}
{#snippet content()}
<div class="p-4">
<div class="w-80">
<p class="text-sm"
>Enable Windmill AI in the <a
class="inline-flex flex-row items-center gap-1"
href="{base}/workspace_settings?tab=ai"
target="_blank">workspace settings</a
></p
></div
>
</div>
{/snippet}
</Popover>
{/snippet}
</OpenInSessionButton>
{#if sessionScopedManager}
<Button
title="Fix code"
size="xs"
color="light"
spacingSize="xs2"
startIcon={{ icon: WandSparkles }}
on:click={() => sessionScopedManager.seedComposer(seedPrompt)}
btnClasses={AIBtnClasses('default')}
>
AI Fix
</Button>
{:else}
<OpenInSessionButton
source={sessionSource}
btnClasses={AIBtnClasses('default')}
label="AI Fix"
btnProps={{ iconOnly: false, startIcon: { icon: WandSparkles } }}
>
{#snippet fallback()}
<Popover
floatingConfig={{
middleware: [
autoPlacement({
allowedPlacements: ['bottom-end', 'top-end']
})
]
}}
displayArrow={true}
>
{#snippet trigger()}
<div class="flex flex-row">
<Button
title="Fix code"
size="xs"
color="light"
spacingSize="xs2"
startIcon={{ icon: WandSparkles }}
on:click={() => {
if ($copilotInfo.enabled) {
aiChatManager.fix()
}
}}
btnClasses="text-ai bg-violet-100 dark:bg-gray-700 min-w-[84px]"
propagateEvent={!$copilotInfo.enabled}
>
AI Fix
</Button>
</div>
{/snippet}
{#snippet content()}
<div class="p-4">
<div class="w-80">
<p class="text-sm"
>Enable Windmill AI in the <a
class="inline-flex flex-row items-center gap-1"
href="{base}/workspace_settings?tab=ai"
target="_blank">workspace settings</a
></p
></div
>
</div>
{/snippet}
</Popover>
{/snippet}
</OpenInSessionButton>
{/if}
{/if}
@@ -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()
@@ -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<void> {
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<boolean> {
if (!sessionOpen) return false
await openSourceInSession(sessionOpen, { seedPrompt })
return true
}
// Convert to object format for child components
@@ -75,7 +75,7 @@
togglePanel={open}
btnClasses={btnClasses ?? AIBtnClasses('default')}
{btnProps}
label={label ?? 'Open in AI session'}
{label}
/>
{:else if !inSessionPanel}
{@render fallback?.()}
@@ -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()