From f8db36fa049c96e483a9d41340358eea193a46f8 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 26 Dec 2025 14:44:09 +0000 Subject: [PATCH] feat: allow code selection to be added as context to the AI Chat --- frontend/scripts/untar_ui_builder.js | 2 +- .../copilot/chat/AIChatDisplay.svelte | 21 +++- .../components/raw_apps/RawAppEditor.svelte | 19 +++ .../raw_apps/RawAppInlineScriptEditor.svelte | 115 +++++++++++++++++- .../RawAppInlineScriptRunnable.svelte | 13 +- .../raw_apps/RawAppInlineScriptsPanel.svelte | 11 +- 6 files changed, 175 insertions(+), 6 deletions(-) diff --git a/frontend/scripts/untar_ui_builder.js b/frontend/scripts/untar_ui_builder.js index 91c2ff98c6..cead68dca7 100644 --- a/frontend/scripts/untar_ui_builder.js +++ b/frontend/scripts/untar_ui_builder.js @@ -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/') diff --git a/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte b/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte index 717b3a66eb..ab978778f9 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte +++ b/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte @@ -9,6 +9,7 @@ Loader2, MousePointer2, Plus, + TextSelect, X, XIcon } from 'lucide-svelte' @@ -286,7 +287,7 @@ {/if} - {#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}
{/if} + {#if appContext.codeSelection} +
+ + + L{appContext.codeSelection.startLine}-{appContext.codeSelection.endLine} + + +
+ {/if} {/if} {/if} diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index 0b124492b5..de62e9578e 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -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 + } + } + }} /> {/if} diff --git a/frontend/src/lib/components/raw_apps/RawAppInlineScriptEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppInlineScriptEditor.svelte index 23e8ca5aec..184102a429 100644 --- a/frontend/src/lib/components/raw_apps/RawAppInlineScriptEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppInlineScriptEditor.svelte @@ -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 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(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) + } + }) {#if inlineScript} diff --git a/frontend/src/lib/components/raw_apps/RawAppInlineScriptRunnable.svelte b/frontend/src/lib/components/raw_apps/RawAppInlineScriptRunnable.svelte index 48c9b14d05..c10988f1c4 100644 --- a/frontend/src/lib/components/raw_apps/RawAppInlineScriptRunnable.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppInlineScriptRunnable.svelte @@ -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)} + /** 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() {#if !selectedRunnable} @@ -37,6 +45,7 @@ }} id={selectedRunnable} bind:runnable={runnables[selectedRunnable]} + {onSelectionChange} />{/key} {:else}