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}