mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 16:03:47 +00:00
fix: preselect first row of AI agent and AI sandbox insert panes (#10937)
* fix: preselect first row of AI agent and AI sandbox insert panes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019vjSnhnewkUbx6mR9iCeK8 * fix: keep Enter for focused controls in the AI insert panes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019vjSnhnewkUbx6mR9iCeK8
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
337154b830
commit
95b6bbd46a
@@ -46,6 +46,10 @@
|
||||
|
||||
let input: TextInput | undefined = $state()
|
||||
|
||||
export function focus() {
|
||||
input?.focus()
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
preFilter &&
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -15,12 +15,11 @@
|
||||
workspaceStore
|
||||
} from '$lib/stores'
|
||||
import type { SupportedLanguage } from '$lib/common'
|
||||
import { createEventDispatcher, getContext, onDestroy, onMount, untrack } from 'svelte'
|
||||
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
||||
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
|
||||
import { type Script, type ScriptLang, type HubScriptKind } from '$lib/gen'
|
||||
import ListFiltersQuick from '$lib/components/home/ListFiltersQuick.svelte'
|
||||
import { ExternalLink, Folder, User, X } from 'lucide-svelte'
|
||||
import type { FlowEditorContext } from '../../flows/types'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { Button } from '$lib/components/common'
|
||||
@@ -87,8 +86,6 @@
|
||||
|
||||
let hubCompletions: HubCompletion[] = $state([])
|
||||
|
||||
const { insertButtonOpen } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let selected: { kind: 'owner' | 'integrations'; name: string | undefined } | undefined =
|
||||
$state(undefined)
|
||||
|
||||
@@ -221,13 +218,6 @@
|
||||
selectedByKeyboard = index
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
$insertButtonOpen = true
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
$insertButtonOpen = false
|
||||
})
|
||||
let langs = $derived(
|
||||
processInlineLangs(undefined, $defaultScripts?.order ?? Object.keys(defaultScriptLanguages))
|
||||
.map((l) => [defaultScriptLanguages[l], l])
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
||||
import { createEventDispatcher, getContext, onDestroy, onMount, untrack } from 'svelte'
|
||||
import StepGenQuick from '$lib/components/copilot/StepGenQuick.svelte'
|
||||
import FlowInputsQuick from '../content/FlowInputsQuick.svelte'
|
||||
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
|
||||
@@ -58,6 +58,15 @@
|
||||
const flowEditorContext = getContext<FlowEditorContext | undefined>('FlowEditorContext')
|
||||
let ws = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
|
||||
|
||||
// Flagged for the whole menu, whichever pane is showing: the flow builder walks node
|
||||
// selection on ArrowUp/ArrowDown while it is unset, which tears the popover down.
|
||||
onMount(() => {
|
||||
flowEditorContext?.insertButtonOpen.set(true)
|
||||
})
|
||||
onDestroy(() => {
|
||||
flowEditorContext?.insertButtonOpen.set(false)
|
||||
})
|
||||
|
||||
let savedAgents = $state<{ path: string; description?: string }[]>([])
|
||||
let savedAgentsLoading = $state(false)
|
||||
let savedAgentsWs: string | undefined = undefined
|
||||
@@ -86,6 +95,72 @@
|
||||
: savedAgents
|
||||
)
|
||||
|
||||
function newBlankAgent() {
|
||||
dispatch('close')
|
||||
dispatch('new', { kind: 'aiagent' })
|
||||
}
|
||||
function newLinkedAgent(agentPath: string) {
|
||||
dispatch('close')
|
||||
logReusableAgentUsage('linked')
|
||||
dispatch('new', { kind: 'aiagent', agentPath })
|
||||
}
|
||||
function newClaudeSandbox() {
|
||||
dispatch('close')
|
||||
dispatch('new', {
|
||||
kind: 'script',
|
||||
inlineScript: {
|
||||
language: 'bun',
|
||||
kind: 'script',
|
||||
subkind: 'claudesandbox'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Rows of the AI panes in render order. FlowInputsQuick owns the keyboard index for the other
|
||||
// panes and is unmounted while these render, so the two never fight over Enter or the arrows.
|
||||
let aiRows = $derived.by((): (() => void)[] => {
|
||||
if (selectedKind === 'aiagent') {
|
||||
const saved = savedAgentsLoading ? [] : filteredAgents
|
||||
return [newBlankAgent, ...saved.map((a) => () => newLinkedAgent(a.path))]
|
||||
}
|
||||
if (selectedKind === 'aisandbox') {
|
||||
return [newClaudeSandbox]
|
||||
}
|
||||
return []
|
||||
})
|
||||
let selectedByKeyboard = $state(0)
|
||||
// Clamped rather than reset when the search filter shrinks the list, so the index never
|
||||
// points past the last rendered row.
|
||||
let aiSelected = $derived(Math.min(selectedByKeyboard, Math.max(aiRows.length - 1, 0)))
|
||||
let aiScrollable: HTMLElement | undefined = $state()
|
||||
let stepGen: StepGenQuick | undefined = $state()
|
||||
function onAiKeyDown(e: KeyboardEvent) {
|
||||
if (aiRows.length === 0) {
|
||||
return
|
||||
}
|
||||
// Enter activates the highlighted row only from the search box (or nothing focused); a
|
||||
// focused button, link or field keeps its own native activation.
|
||||
if (
|
||||
e.key === 'Enter' &&
|
||||
e.target instanceof HTMLElement &&
|
||||
e.target.closest('button, a, select, textarea')
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
selectedByKeyboard = (aiSelected + 1) % aiRows.length
|
||||
aiScrollable?.scrollTo({ top: selectedByKeyboard * 32, behavior: 'smooth' })
|
||||
e.preventDefault()
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
selectedByKeyboard = (aiSelected - 1 + aiRows.length) % aiRows.length
|
||||
aiScrollable?.scrollTo({ top: selectedByKeyboard * 32, behavior: 'smooth' })
|
||||
e.preventDefault()
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
aiRows[aiSelected]()
|
||||
}
|
||||
}
|
||||
|
||||
let height = $state(0)
|
||||
let owners = $state([])
|
||||
// Only the content-sized host (TriggersWrapper) grows past this. The fixed-height hosts top out
|
||||
@@ -94,6 +169,7 @@
|
||||
let displayPath = $derived(height > 480)
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onAiKeyDown} />
|
||||
<div
|
||||
id="flow-editor-insert-module"
|
||||
class="flex flex-col h-full gap-2 max-w-full {small ? 'w-[450px]' : 'w-[650px]'}"
|
||||
@@ -105,6 +181,7 @@
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<StepGenQuick
|
||||
bind:this={stepGen}
|
||||
on:escape={() => dispatch('close')}
|
||||
{disableAi}
|
||||
on:insert
|
||||
@@ -230,7 +307,10 @@
|
||||
selected={selectedKind === 'aiagent'}
|
||||
onSelect={() => {
|
||||
selectedKind = 'aiagent'
|
||||
selectedByKeyboard = 0
|
||||
loadSavedAgents()
|
||||
// Clicking leaves focus on this button, where Enter would only re-select it.
|
||||
stepGen?.focus()
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -240,6 +320,8 @@
|
||||
selected={selectedKind === 'aisandbox'}
|
||||
onSelect={() => {
|
||||
selectedKind = 'aisandbox'
|
||||
selectedByKeyboard = 0
|
||||
stepGen?.focus()
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -248,19 +330,25 @@
|
||||
{/if}
|
||||
|
||||
{#if selectedKind === 'aiagent'}
|
||||
<div class="h-full overflow-auto grow min-w-0 p-2 gap-1 flex flex-col">
|
||||
<div
|
||||
bind:this={aiScrollable}
|
||||
class="h-full overflow-auto grow min-w-0 p-2 gap-1 flex flex-col"
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch('close')
|
||||
dispatch('new', { kind: 'aiagent' })
|
||||
}}
|
||||
onClick={newBlankAgent}
|
||||
onmousemove={() => (selectedByKeyboard = 0)}
|
||||
role="menuitem"
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
btnClasses="justify-start"
|
||||
btnClasses="justify-start {aiSelected === 0
|
||||
? 'bg-surface-hover'
|
||||
: 'hover:bg-transparent'}"
|
||||
>
|
||||
<Plus size={13} class="shrink-0" />
|
||||
<span class="grow truncate text-left">Blank AI agent</span>
|
||||
{#if aiSelected === 0}
|
||||
<kbd class="!text-xs">↵</kbd>
|
||||
{/if}
|
||||
</Button>
|
||||
{#if savedAgentsLoading}
|
||||
<div class="flex items-center gap-2 p-2 text-xs text-tertiary">
|
||||
@@ -268,21 +356,23 @@
|
||||
</div>
|
||||
{:else if filteredAgents.length > 0}
|
||||
<div class="pt-2 pb-0 text-2xs font-normal text-secondary ml-2">Saved agents</div>
|
||||
{#each filteredAgents as agent (agent.path)}
|
||||
{#each filteredAgents as agent, i (agent.path)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch('close')
|
||||
logReusableAgentUsage('linked')
|
||||
dispatch('new', { kind: 'aiagent', agentPath: agent.path })
|
||||
}}
|
||||
onClick={() => newLinkedAgent(agent.path)}
|
||||
onmousemove={() => (selectedByKeyboard = i + 1)}
|
||||
role="menuitem"
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
btnClasses="justify-start"
|
||||
btnClasses="justify-start {aiSelected === i + 1
|
||||
? 'bg-surface-hover'
|
||||
: 'hover:bg-transparent'}"
|
||||
title={agent.description || agent.path}
|
||||
>
|
||||
<BotIcon size={13} class="shrink-0 text-ai" />
|
||||
<span class="grow truncate text-left">{agent.path}</span>
|
||||
{#if aiSelected === i + 1}
|
||||
<kbd class="!text-xs">↵</kbd>
|
||||
{/if}
|
||||
</Button>
|
||||
{/each}
|
||||
{:else}
|
||||
@@ -297,17 +387,11 @@
|
||||
<div class="h-full overflow-auto grow min-w-0 p-2 gap-1 flex flex-col">
|
||||
<TopLevelNode
|
||||
label="Claude Code"
|
||||
onSelect={() => {
|
||||
dispatch('close')
|
||||
dispatch('new', {
|
||||
kind: 'script',
|
||||
inlineScript: {
|
||||
language: 'bun',
|
||||
kind: 'script',
|
||||
subkind: 'claudesandbox'
|
||||
}
|
||||
})
|
||||
}}
|
||||
neutral
|
||||
returnIcon
|
||||
selected={aiSelected === 0}
|
||||
onSelect={newClaudeSandbox}
|
||||
onHover={() => (selectedByKeyboard = 0)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
Reference in New Issue
Block a user