mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 00:03:08 +00:00
feat: allow code selection to be added as context to the AI Chat
This commit is contained in:
@@ -20,7 +20,7 @@ console.log('Running postinstall for root project');
|
||||
|
||||
import { x } from 'tar'
|
||||
|
||||
const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-4b1b0a0.tar.gz'
|
||||
const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-9e76079.tar.gz'
|
||||
const outputTarPath = path.join(process.cwd(), 'ui_builder.tar.gz')
|
||||
const extractTo = path.join(process.cwd(), 'static/ui_builder/')
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
Loader2,
|
||||
MousePointer2,
|
||||
Plus,
|
||||
TextSelect,
|
||||
X,
|
||||
XIcon
|
||||
} from 'lucide-svelte'
|
||||
@@ -286,7 +287,7 @@
|
||||
{/if}
|
||||
<ProviderModelSelector />
|
||||
|
||||
{#if aiChatManager.mode === AIMode.APP && appContext && (appContext.type !== 'none' || appContext.inspectorElement)}
|
||||
{#if aiChatManager.mode === AIMode.APP && appContext && (appContext.type !== 'none' || appContext.inspectorElement || appContext.codeSelection)}
|
||||
{#if appContext.type === 'frontend' && appContext.frontendPath && !appContext.selectionExcluded}
|
||||
<div
|
||||
class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-2xs"
|
||||
@@ -341,6 +342,24 @@
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if appContext.codeSelection}
|
||||
<div
|
||||
class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 text-2xs"
|
||||
title={`${appContext.codeSelection.source}: lines ${appContext.codeSelection.startLine}-${appContext.codeSelection.endLine}`}
|
||||
>
|
||||
<TextSelect class="w-3 h-3" />
|
||||
<span class="truncate max-w-[80px]">
|
||||
L{appContext.codeSelection.startLine}-{appContext.codeSelection.endLine}
|
||||
</span>
|
||||
<button
|
||||
class="hover:bg-amber-200 dark:hover:bg-amber-800/50 rounded p-0.5 -mr-0.5"
|
||||
onclick={() => appContext.clearCodeSelection?.()}
|
||||
title="Clear code selection"
|
||||
>
|
||||
<X class="w-2.5 h-2.5" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -879,6 +879,25 @@
|
||||
{selectedRunnable}
|
||||
{initRunnablesContent}
|
||||
{runnables}
|
||||
onSelectionChange={(selection) => {
|
||||
console.log('handle selection', selection)
|
||||
|
||||
if (selection === null) {
|
||||
codeSelection = undefined
|
||||
} else if (selectedRunnable) {
|
||||
codeSelection = {
|
||||
type: 'app_code_selection',
|
||||
source: selectedRunnable,
|
||||
sourceType: 'backend',
|
||||
title: `${selectedRunnable}:L${selection.startLine}-L${selection.endLine}`,
|
||||
content: selection.content,
|
||||
startLine: selection.startLine,
|
||||
endLine: selection.endLine,
|
||||
startColumn: selection.startColumn,
|
||||
endColumn: selection.endColumn
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
const bubble = createBubbler()
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import type { Preview, ScriptLang } from '$lib/gen'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { createEventDispatcher, onMount, untrack } from 'svelte'
|
||||
import { Trash2 } from 'lucide-svelte'
|
||||
import { inferArgs, inferAssets } from '$lib/infer'
|
||||
import type { Schema } from '$lib/common'
|
||||
@@ -35,6 +35,16 @@
|
||||
onCancel: () => Promise<void>
|
||||
editor?: Editor | undefined
|
||||
lastDeployedCode?: string | undefined
|
||||
/** Called when code is selected in the editor */
|
||||
onSelectionChange?: (
|
||||
selection: {
|
||||
content: string
|
||||
startLine: number
|
||||
endLine: number
|
||||
startColumn: number
|
||||
endColumn: number
|
||||
} | null
|
||||
) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -47,7 +57,8 @@
|
||||
onRun,
|
||||
onCancel,
|
||||
editor = $bindable(undefined),
|
||||
lastDeployedCode
|
||||
lastDeployedCode,
|
||||
onSelectionChange
|
||||
}: Props = $props()
|
||||
let diffEditor = $state() as DiffEditor | undefined
|
||||
let validCode = $state(true)
|
||||
@@ -123,6 +134,106 @@
|
||||
$effect(() => {
|
||||
if (inlineScript && inferAssetsRes.current) inlineScript.assets = inferAssetsRes.current?.assets
|
||||
})
|
||||
|
||||
// Track last selection to avoid duplicate events
|
||||
let lastSelectionKey = $state<string | null>(null)
|
||||
// Track pending selection during mouse drag
|
||||
let pendingSelection: {
|
||||
startLineNumber: number
|
||||
startColumn: number
|
||||
endLineNumber: number
|
||||
endColumn: number
|
||||
} | null = null
|
||||
let isMouseDown = false
|
||||
|
||||
function emitSelection(editorInstance: Editor): void {
|
||||
if (!onSelectionChange) return
|
||||
|
||||
const selection = pendingSelection
|
||||
if (!selection) {
|
||||
// No selection - only emit null if we previously had a selection
|
||||
if (lastSelectionKey !== null) {
|
||||
lastSelectionKey = null
|
||||
onSelectionChange(null)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there's an actual selection (not just cursor position)
|
||||
const hasSelection =
|
||||
selection.startLineNumber !== selection.endLineNumber ||
|
||||
selection.startColumn !== selection.endColumn
|
||||
|
||||
if (!hasSelection) {
|
||||
// No selection - only emit null if we previously had a selection
|
||||
if (lastSelectionKey !== null) {
|
||||
lastSelectionKey = null
|
||||
onSelectionChange(null)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get the selected content from the editor
|
||||
const model = editorInstance.getModel?.()
|
||||
if (!model || !('getValueInRange' in model)) return
|
||||
|
||||
const content = (model as any).getValueInRange({
|
||||
startLineNumber: selection.startLineNumber,
|
||||
startColumn: selection.startColumn,
|
||||
endLineNumber: selection.endLineNumber,
|
||||
endColumn: selection.endColumn
|
||||
})
|
||||
|
||||
// Create a key to deduplicate identical selections
|
||||
const selectionKey = `${selection.startLineNumber}:${selection.startColumn}:${selection.endLineNumber}:${selection.endColumn}`
|
||||
if (selectionKey === lastSelectionKey) return
|
||||
lastSelectionKey = selectionKey
|
||||
|
||||
onSelectionChange({
|
||||
content,
|
||||
startLine: selection.startLineNumber,
|
||||
endLine: selection.endLineNumber,
|
||||
startColumn: selection.startColumn,
|
||||
endColumn: selection.endColumn
|
||||
})
|
||||
}
|
||||
|
||||
// Listen for editor selection changes - wait for mouseup before emitting
|
||||
$effect(() => {
|
||||
if (!editor || !onSelectionChange) return
|
||||
|
||||
const editorInstance = editor
|
||||
|
||||
// Track selection changes but don't emit until mouseup
|
||||
const selectionDisposable = editorInstance.onDidChangeCursorSelection?.((e) => {
|
||||
pendingSelection = e.selection
|
||||
// If not mouse-driven (e.g., keyboard selection), emit immediately
|
||||
if (!isMouseDown) {
|
||||
untrack(() => emitSelection(editorInstance))
|
||||
}
|
||||
})
|
||||
|
||||
// Track mouse state
|
||||
const handleMouseDown = () => {
|
||||
isMouseDown = true
|
||||
}
|
||||
const handleMouseUp = () => {
|
||||
if (isMouseDown) {
|
||||
isMouseDown = false
|
||||
untrack(() => emitSelection(editorInstance))
|
||||
}
|
||||
}
|
||||
|
||||
// Add mouse listeners to the document to catch mouseup even outside editor
|
||||
document.addEventListener('mousedown', handleMouseDown)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
|
||||
return () => {
|
||||
selectionDisposable?.dispose()
|
||||
document.removeEventListener('mousedown', handleMouseDown)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if inlineScript}
|
||||
|
||||
@@ -30,9 +30,19 @@
|
||||
id: string
|
||||
appPath: string
|
||||
lastDeployedCode?: string | undefined
|
||||
/** Called when code is selected in the editor */
|
||||
onSelectionChange?: (
|
||||
selection: {
|
||||
content: string
|
||||
startLine: number
|
||||
endLine: number
|
||||
startColumn: number
|
||||
endColumn: number
|
||||
} | null
|
||||
) => void
|
||||
}
|
||||
|
||||
let { runnable = $bindable(), id, appPath }: Props = $props()
|
||||
let { runnable = $bindable(), id, appPath, onSelectionChange }: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -131,6 +141,7 @@
|
||||
}}
|
||||
on:delete
|
||||
path={appPath}
|
||||
{onSelectionChange}
|
||||
/>
|
||||
{:else if isRunnableByPath(runnable)}
|
||||
<InlineScriptRunnableByPath
|
||||
|
||||
@@ -8,9 +8,17 @@
|
||||
selectedRunnable: string | undefined
|
||||
appPath: string
|
||||
initRunnablesContent: Record<string, string>
|
||||
/** Called when code is selected in the editor */
|
||||
onSelectionChange?: (selection: {
|
||||
content: string
|
||||
startLine: number
|
||||
endLine: number
|
||||
startColumn: number
|
||||
endColumn: number
|
||||
} | null) => void
|
||||
}
|
||||
|
||||
let { runnables, selectedRunnable = $bindable(), appPath }: Props = $props()
|
||||
let { runnables, selectedRunnable = $bindable(), appPath, onSelectionChange }: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if !selectedRunnable}
|
||||
@@ -37,6 +45,7 @@
|
||||
}}
|
||||
id={selectedRunnable}
|
||||
bind:runnable={runnables[selectedRunnable]}
|
||||
{onSelectionChange}
|
||||
/>{/key}
|
||||
{:else}
|
||||
<div class="text-sm text-primary text-center py-8 px-2">
|
||||
|
||||
Reference in New Issue
Block a user