diff --git a/frontend/src/lib/components/copilot/chat/AIChat.svelte b/frontend/src/lib/components/copilot/chat/AIChat.svelte
index 9e41034fbe..ff31146391 100644
--- a/frontend/src/lib/components/copilot/chat/AIChat.svelte
+++ b/frontend/src/lib/components/copilot/chat/AIChat.svelte
@@ -98,7 +98,9 @@
() => aiChatManager.contextManager.getSelectedContext(),
(sc) => aiChatManager.contextManager.setSelectedContext(sc)
}
- availableContext={aiChatManager.contextManager.getAvailableContext()}
+ availableContext={aiChatManager.mode === AIMode.APP
+ ? aiChatManager.getAppAvailableContext()
+ : aiChatManager.contextManager.getAvailableContext()}
messages={aiChatManager.currentReply
? [
...aiChatManager.displayMessages,
diff --git a/frontend/src/lib/components/copilot/chat/AIChatInput.svelte b/frontend/src/lib/components/copilot/chat/AIChatInput.svelte
index cf609a5848..457e315d94 100644
--- a/frontend/src/lib/components/copilot/chat/AIChatInput.svelte
+++ b/frontend/src/lib/components/copilot/chat/AIChatInput.svelte
@@ -1,6 +1,7 @@
@@ -185,6 +401,98 @@
{disabled}
{onKeyDown}
/>
+ {:else if aiChatManager.mode === AIMode.APP}
+ {#if showContext}
+
+ {/if}
+
+
+
+ {#if showAppContextTooltip}
+
+
+
{
+ handleAppContextSelection(element)
+ }}
+ showAllAvailable={true}
+ stringSearch={appContextTooltipWord.slice(1)}
+ onViewChange={(newNumber) => {
+ appTooltipCurrentViewNumber = newNumber
+ }}
+ setShowing={(showing) => {
+ showAppContextTooltip = showing
+ }}
+ />
+
+
+ {/if}
{:else}
{/if}
+ {:else if contextElement.type === 'app_frontend_file'}
+
+
+
+ {:else if contextElement.type === 'app_backend_runnable'}
+
+ {#if contextElement.runnable.inlineScript}
+
+ {:else}
+
+ {/if}
+
+ {:else if contextElement.type === 'app_code_selection'}
+
+
+ {contextElement.source} (L{contextElement.startLine}-L{contextElement.endLine})
+
+
+
+ {:else if contextElement.type === 'app_datatable'}
+
+
+ {contextElement.datatableName}/{contextElement.schemaName === 'public'
+ ? ''
+ : contextElement.schemaName + ':'}{contextElement.tableName}
+
+
+
{/if}
{/snippet}
diff --git a/frontend/src/lib/components/copilot/chat/__tests__/app/appEvalHelpers.ts b/frontend/src/lib/components/copilot/chat/__tests__/app/appEvalHelpers.ts
index daf934ef90..4dbc8d58a0 100644
--- a/frontend/src/lib/components/copilot/chat/__tests__/app/appEvalHelpers.ts
+++ b/frontend/src/lib/components/copilot/chat/__tests__/app/appEvalHelpers.ts
@@ -124,6 +124,14 @@ export function createAppEvalHelpers(
) => {
// Return success with empty result for eval testing
return { success: true, result: [] }
+ },
+
+ addTableToWhitelist: (
+ _datatableName: string,
+ _schemaName: string,
+ _tableName: string
+ ) => {
+ // No-op for eval testing - tables are not tracked in test context
}
}
diff --git a/frontend/src/lib/components/copilot/chat/app/core.ts b/frontend/src/lib/components/copilot/chat/app/core.ts
index 6ff59f7708..6b6f4854e8 100644
--- a/frontend/src/lib/components/copilot/chat/app/core.ts
+++ b/frontend/src/lib/components/copilot/chat/app/core.ts
@@ -8,6 +8,13 @@ import { FlowService, ScriptService, type Flow, type Script } from '$lib/gen'
import uFuzzy from '@leeoniya/ufuzzy'
import { emptyString } from '$lib/utils'
import { aiChatManager } from '../AIChatManager.svelte'
+import type {
+ ContextElement,
+ AppFrontendFileElement,
+ AppBackendRunnableElement,
+ AppCodeSelectionElement,
+ AppDatatableElement
+} from '../context'
// Backend runnable types
export type BackendRunnableType = 'script' | 'flow' | 'hubscript' | 'inline'
@@ -93,8 +100,10 @@ export interface SelectedContext {
clearInspector?: () => void
/** Function to clear the runnable selection (go back to frontend view) */
clearRunnable?: () => void
- // Future: text selection within the file
- // textSelection?: { startLine: number; endLine: number; startColumn: number; endColumn: number }
+ /** Code selection from the editor (either frontend or backend) */
+ codeSelection?: AppCodeSelectionElement
+ /** Function to clear the code selection */
+ clearCodeSelection?: () => void
}
/**
@@ -142,6 +151,8 @@ export interface AppAIChatHelpers {
sql: string,
newTable?: { schema: string; name: string }
) => Promise<{ success: boolean; result?: Record
[]; error?: string }>
+ /** Add a table to the app's whitelisted tables (called when user selects a table via @) */
+ addTableToWhitelist: (datatableName: string, schemaName: string, tableName: string) => void
}
// ============= Utility =============
@@ -1085,15 +1096,22 @@ const MAX_CONTEXT_CONTENT_LENGTH = 3000
export function prepareAppUserMessage(
instructions: string,
- selectedContext?: SelectedContext
+ selectedContext?: SelectedContext,
+ additionalContext?: ContextElement[]
): ChatCompletionUserMessageParam {
let content = ''
- if (selectedContext && (selectedContext.type !== 'none' || selectedContext.inspectorElement)) {
+ // Check if we have any context to add
+ const hasSelectedContext =
+ selectedContext && (selectedContext.type !== 'none' || selectedContext.inspectorElement)
+ const hasAdditionalContext = additionalContext && additionalContext.length > 0
+
+ if (hasSelectedContext || hasAdditionalContext) {
content += `## SELECTED CONTEXT:\n`
// Add frontend file context with content (unless excluded)
if (
+ selectedContext &&
selectedContext.type === 'frontend' &&
selectedContext.frontendPath &&
!selectedContext.selectionExcluded
@@ -1111,6 +1129,7 @@ export function prepareAppUserMessage(
// Add backend runnable context with content (unless excluded)
if (
+ selectedContext &&
selectedContext.type === 'backend' &&
selectedContext.backendKey &&
!selectedContext.selectionExcluded
@@ -1139,7 +1158,7 @@ export function prepareAppUserMessage(
}
// Add inspector element context if available
- if (selectedContext.inspectorElement) {
+ if (selectedContext?.inspectorElement) {
const el = selectedContext.inspectorElement
content += `\nThe user has selected an element in the app preview using the inspector tool:\n`
content += `- **Element**: ${el.tagName}${el.id ? `#${el.id}` : ''}${el.className ? `.${el.className.split(' ').join('.')}` : ''}\n`
@@ -1154,6 +1173,76 @@ export function prepareAppUserMessage(
const truncatedHtml = el.html.length > 500 ? el.html.slice(0, 500) + '...' : el.html
content += `- **HTML**:\n\`\`\`html\n${truncatedHtml}\n\`\`\`\n`
}
+
+ // Add code selection context if available
+ if (selectedContext?.codeSelection) {
+ const selection = selectedContext.codeSelection
+ content += `\n### CODE SELECTION:\n`
+ content += `The user has selected code in the ${selection.sourceType} editor:\n`
+ content += `- **File/Source**: ${selection.source}\n`
+ content += `- **Lines**: ${selection.startLine}-${selection.endLine}\n`
+ const truncatedCode =
+ selection.content.length > MAX_CONTEXT_CONTENT_LENGTH
+ ? selection.content.slice(0, MAX_CONTEXT_CONTENT_LENGTH) + '\n... [TRUNCATED]'
+ : selection.content
+ content += `\`\`\`\n${truncatedCode}\n\`\`\`\n`
+ }
+
+ // Add additional context from @ mentions
+ if (additionalContext && additionalContext.length > 0) {
+ content += `\n### ADDITIONAL CONTEXT (mentioned by user):\n`
+
+ for (const ctx of additionalContext) {
+ if (ctx.type === 'app_frontend_file') {
+ const fileCtx = ctx as AppFrontendFileElement
+ content += `\n**Frontend File: ${fileCtx.path}**\n`
+ const truncatedContent =
+ fileCtx.content.length > MAX_CONTEXT_CONTENT_LENGTH
+ ? fileCtx.content.slice(0, MAX_CONTEXT_CONTENT_LENGTH) + '\n... [TRUNCATED]'
+ : fileCtx.content
+ content += `\`\`\`\n${truncatedContent}\n\`\`\`\n`
+ } else if (ctx.type === 'app_backend_runnable') {
+ const runnableCtx = ctx as AppBackendRunnableElement
+ const runnable = runnableCtx.runnable
+ content += `\n**Backend Runnable: ${runnableCtx.key}**\n`
+ content += `- **Name**: ${runnable.name}\n`
+ content += `- **Type**: ${runnable.type}\n`
+ if (runnable.path) {
+ content += `- **Path**: ${runnable.path}\n`
+ }
+ if (runnable.inlineScript) {
+ const truncatedCode =
+ runnable.inlineScript.content.length > MAX_CONTEXT_CONTENT_LENGTH
+ ? runnable.inlineScript.content.slice(0, MAX_CONTEXT_CONTENT_LENGTH) +
+ '\n... [TRUNCATED]'
+ : runnable.inlineScript.content
+ content += `- **Language**: ${runnable.inlineScript.language}\n`
+ content += `- **Code**:\n\`\`\`${runnable.inlineScript.language === 'bun' ? 'typescript' : 'python'}\n${truncatedCode}\n\`\`\`\n`
+ }
+ if (runnable.staticInputs && Object.keys(runnable.staticInputs).length > 0) {
+ content += `- **Static inputs**: ${JSON.stringify(runnable.staticInputs)}\n`
+ }
+ } else if (ctx.type === 'app_datatable') {
+ const datatableCtx = ctx as AppDatatableElement
+ const tableRef =
+ datatableCtx.schemaName === 'public'
+ ? `${datatableCtx.datatableName}/${datatableCtx.tableName}`
+ : `${datatableCtx.datatableName}/${datatableCtx.schemaName}:${datatableCtx.tableName}`
+ content += `\n**Table: ${tableRef}**\n`
+ content += `- **Datatable**: ${datatableCtx.datatableName}\n`
+ content += `- **Schema**: ${datatableCtx.schemaName}\n`
+ content += `- **Table**: ${datatableCtx.tableName}\n`
+ // Format columns as column_name: type
+ const columnsStr = JSON.stringify(datatableCtx.columns, null, 2)
+ const truncatedColumns =
+ columnsStr.length > MAX_CONTEXT_CONTENT_LENGTH
+ ? columnsStr.slice(0, MAX_CONTEXT_CONTENT_LENGTH) + '\n... [TRUNCATED]'
+ : columnsStr
+ content += `- **Columns** (column_name -> type):\n\`\`\`json\n${truncatedColumns}\n\`\`\`\n`
+ }
+ }
+ }
+
content += '\n'
}
diff --git a/frontend/src/lib/components/copilot/chat/context.ts b/frontend/src/lib/components/copilot/chat/context.ts
index 5c02a84835..f0f9af2310 100644
--- a/frontend/src/lib/components/copilot/chat/context.ts
+++ b/frontend/src/lib/components/copilot/chat/context.ts
@@ -1,14 +1,19 @@
-import { Code, Database, TriangleAlert, Diff } from 'lucide-svelte'
+import { Code, Database, TriangleAlert, Diff, FileCode, Code2, TextSelect, Table2 } from 'lucide-svelte'
import type { ScriptLang } from '$lib/gen/types.gen'
import { type DBSchema } from '$lib/stores'
import { type Change } from 'diff'
+import type { BackendRunnable } from './app/core'
export const ContextIconMap = {
code: Code,
error: TriangleAlert,
db: Database,
diff: Diff,
- code_piece: Code
+ code_piece: Code,
+ app_frontend_file: FileCode,
+ app_backend_runnable: Code2,
+ app_code_selection: TextSelect,
+ app_datatable: Table2
// flow_module type is handled with FlowModuleIcon
}
@@ -67,6 +72,62 @@ export interface FlowModuleCodePieceElement extends Omit compact_type */
+ columns: Record
+}
+
export type ContextElement = (
| CodeElement
| ErrorElement
@@ -75,6 +136,10 @@ export type ContextElement = (
| CodePieceElement
| FlowModuleElement
| FlowModuleCodePieceElement
+ | AppFrontendFileElement
+ | AppBackendRunnableElement
+ | AppCodeSelectionElement
+ | AppDatatableElement
) & {
deletable?: boolean
}
diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte
index 2f2b006fb7..0b124492b5 100644
--- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte
+++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte
@@ -19,6 +19,7 @@
import { aiChatManager, AIMode } from '../copilot/chat/AIChatManager.svelte'
import { onMount } from 'svelte'
import type { LintResult, DataTableSchema, InspectorElementInfo } from '../copilot/chat/app/core'
+ import type { AppCodeSelectionElement } from '../copilot/chat/context'
import { rawAppLintStore } from './lintStore'
import { dbSchemas } from '$lib/stores'
import { runScriptAndPollResult } from '../jobs/utils'
@@ -392,7 +393,11 @@
selectionExcluded: selectionExcludedFromPrompt,
toggleSelectionExcluded: toggleSelectionExcluded,
clearInspector: clearInspectorSelection,
- clearRunnable: handleClearRunnable
+ clearRunnable: handleClearRunnable,
+ codeSelection: codeSelection,
+ clearCodeSelection: () => {
+ codeSelection = undefined
+ }
}
if (selectedRunnable) {
const runnable = convertToBackendRunnable(selectedRunnable, runnables[selectedRunnable])
@@ -440,6 +445,12 @@
// Get unique datatable names from dataTableRefs (the whitelisted tables)
const whitelistedDatatables = new Set(dataTableRefsObjects.map((ref) => ref.datatable))
+ // If no datatables are configured, return all available datatables
+ // This allows users to see all datatables in the @ context menu
+ if (whitelistedDatatables.size === 0) {
+ return allSchemas
+ }
+
// Build a map of whitelisted tables per datatable: datatable -> schema -> Set
const whitelistedTables = new Map>>()
for (const ref of dataTableRefsObjects) {
@@ -550,6 +561,19 @@
const errorMsg = e instanceof Error ? e.message : String(e)
return { success: false, error: errorMsg }
}
+ },
+ addTableToWhitelist: (datatableName: string, schemaName: string, tableName: string) => {
+ // Format the table reference
+ const newRef = formatDataTableRef({
+ datatable: datatableName,
+ schema: schemaName === 'public' ? undefined : schemaName,
+ table: tableName
+ })
+ // Only add if not already present
+ if (!data.tables.includes(newRef)) {
+ data.tables = [...data.tables, newRef]
+ saveFrontendDraft()
+ }
}
})
})
@@ -557,6 +581,7 @@
let selectedDocument: string | undefined = $state(undefined)
let inspectorElement: InspectorElementInfo | undefined = $state(undefined)
let selectionExcludedFromPrompt: boolean = $state(false)
+ let codeSelection: AppCodeSelectionElement | undefined = $state(undefined)
function toggleSelectionExcluded() {
selectionExcludedFromPrompt = !selectionExcludedFromPrompt
@@ -596,6 +621,27 @@
} else if (e.data.type === 'inspectorClear') {
// Clear the inspector element when user dismisses the selection
inspectorElement = undefined
+ } else if (e.data.type === 'editorSelection') {
+ // Handle code selection from the iframe editor
+ const selection = e.data.selection
+ if (selection === null) {
+ // Selection cleared
+ codeSelection = undefined
+ } else {
+ // Normalize path
+ const normalizedPath = selection.path?.replace(/\\/g, '/')
+ codeSelection = {
+ type: 'app_code_selection',
+ source: normalizedPath,
+ sourceType: 'frontend',
+ title: `${normalizedPath}:L${selection.range.startLine}-L${selection.range.endLine}`,
+ content: selection.content,
+ startLine: selection.range.startLine,
+ endLine: selection.range.endLine,
+ startColumn: selection.range.startColumn,
+ endColumn: selection.range.endColumn
+ }
+ }
}
}