mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-13 08:05:23 +00:00
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:
co-authored by
Claude Opus 5
parent
9755e1215a
commit
c31ec855d2
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user