feat: allow code selection to be added as context to the AI Chat

This commit is contained in:
Ruben Fiszel
2025-12-26 14:44:09 +00:00
parent 56b9fd4eb7
commit f8db36fa04
6 changed files with 175 additions and 6 deletions
+1 -1
View File
@@ -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">