Compare commits
3 Commits
main
...
consolidat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f31c68e8be | ||
|
|
31975aaa55 | ||
|
|
2e0a727553 |
@@ -98,9 +98,7 @@
|
||||
() => aiChatManager.contextManager.getSelectedContext(),
|
||||
(sc) => aiChatManager.contextManager.setSelectedContext(sc)
|
||||
}
|
||||
availableContext={aiChatManager.mode === AIMode.APP
|
||||
? aiChatManager.getAppAvailableContext()
|
||||
: aiChatManager.contextManager.getAvailableContext()}
|
||||
availableContext={aiChatManager.contextManager.getAvailableContext()}
|
||||
messages={aiChatManager.currentReply
|
||||
? [
|
||||
...aiChatManager.displayMessages,
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { type Snippet } from 'svelte'
|
||||
import {
|
||||
CheckIcon,
|
||||
Code2,
|
||||
FileCode,
|
||||
HistoryIcon,
|
||||
Loader2,
|
||||
MousePointer2,
|
||||
@@ -26,7 +24,7 @@
|
||||
import { aiChatManager, AIMode } from './AIChatManager.svelte'
|
||||
import AIChatInput from './AIChatInput.svelte'
|
||||
import { getModifierKey } from '$lib/utils'
|
||||
import type { SelectedContext } from './app/core'
|
||||
import type { AppTransientContext } from './app/core'
|
||||
|
||||
let {
|
||||
messages,
|
||||
@@ -103,11 +101,11 @@
|
||||
)
|
||||
|
||||
// Get app context for display when in APP mode
|
||||
const appContext = $derived.by((): SelectedContext | undefined => {
|
||||
const appContext = $derived.by((): AppTransientContext | undefined => {
|
||||
if (aiChatManager.mode !== AIMode.APP || !aiChatManager.appAiChatHelpers) {
|
||||
return undefined
|
||||
}
|
||||
return aiChatManager.appAiChatHelpers.getSelectedContext()
|
||||
return aiChatManager.appAiChatHelpers.getTransientContext()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -293,38 +291,7 @@
|
||||
{/if}
|
||||
<ProviderModelSelector />
|
||||
|
||||
{#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"
|
||||
title={appContext.frontendPath}
|
||||
>
|
||||
<FileCode class="w-3 h-3" />
|
||||
<span class="truncate max-w-[80px]">{appContext.frontendPath}</span>
|
||||
<button
|
||||
class="hover:bg-blue-200 dark:hover:bg-blue-800/50 rounded p-0.5 -mr-0.5"
|
||||
onclick={() => appContext.toggleSelectionExcluded?.()}
|
||||
title="Exclude from prompt"
|
||||
>
|
||||
<X class="w-2.5 h-2.5" />
|
||||
</button>
|
||||
</div>
|
||||
{:else if appContext.type === 'backend' && appContext.backendKey && !appContext.selectionExcluded}
|
||||
<div
|
||||
class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 text-2xs"
|
||||
title={appContext.backendKey}
|
||||
>
|
||||
<Code2 class="w-3 h-3" />
|
||||
<span class="truncate max-w-[80px]">{appContext.backendKey}</span>
|
||||
<button
|
||||
class="hover:bg-green-200 dark:hover:bg-green-800/50 rounded p-0.5 -mr-0.5"
|
||||
onclick={() => appContext.toggleSelectionExcluded?.()}
|
||||
title="Exclude from prompt"
|
||||
>
|
||||
<X class="w-2.5 h-2.5" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if aiChatManager.mode === AIMode.APP && appContext && (appContext.inspectorElement || appContext.codeSelection)}
|
||||
{#if appContext.inspectorElement}
|
||||
<div
|
||||
class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-2xs"
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
getAppTools,
|
||||
prepareAppSystemMessage,
|
||||
prepareAppUserMessage,
|
||||
type AppAIChatHelpers
|
||||
type AppAIChatHelpers,
|
||||
type AppSelection
|
||||
} from './app/core'
|
||||
import ContextManager from './ContextManager.svelte'
|
||||
import HistoryManager from './HistoryManager.svelte'
|
||||
@@ -445,10 +446,7 @@ class AIChatManager {
|
||||
if (!pendingPrompt) return undefined
|
||||
this.pendingPrompt = ''
|
||||
if (this.mode === AIMode.SCRIPT) {
|
||||
return prepareScriptUserMessage(
|
||||
pendingPrompt,
|
||||
this.contextManager.getSelectedContext()
|
||||
)
|
||||
return prepareScriptUserMessage(pendingPrompt, this.contextManager.getSelectedContext())
|
||||
} else if (this.mode === AIMode.FLOW) {
|
||||
return prepareFlowUserMessage(
|
||||
pendingPrompt,
|
||||
@@ -627,7 +625,7 @@ class AIChatManager {
|
||||
role: 'user',
|
||||
content: this.instructions,
|
||||
contextElements:
|
||||
this.mode === AIMode.SCRIPT || this.mode === AIMode.FLOW
|
||||
this.mode === AIMode.SCRIPT || this.mode === AIMode.FLOW || this.mode === AIMode.APP
|
||||
? oldSelectedContext
|
||||
: undefined,
|
||||
snapshot,
|
||||
@@ -668,7 +666,7 @@ class AIChatManager {
|
||||
case AIMode.APP:
|
||||
userMessage = prepareAppUserMessage(
|
||||
oldInstructions,
|
||||
this.appAiChatHelpers?.getSelectedContext(),
|
||||
this.appAiChatHelpers?.getTransientContext(),
|
||||
oldSelectedContext
|
||||
)
|
||||
break
|
||||
@@ -903,6 +901,11 @@ class AIChatManager {
|
||||
!copilotSessionModel?.model.endsWith('/thinking'),
|
||||
untrack(() => this.contextManager.getSelectedContext())
|
||||
)
|
||||
} else if (this.mode === AIMode.APP) {
|
||||
this.contextManager.updateAvailableContextForApp(
|
||||
this.getAppAvailableContext(),
|
||||
untrack(() => this.contextManager.getSelectedContext())
|
||||
)
|
||||
}
|
||||
|
||||
if (this.scriptEditorOptions) {
|
||||
@@ -910,6 +913,15 @@ class AIChatManager {
|
||||
}
|
||||
}
|
||||
|
||||
syncAppSelection = (selectedContext: AppSelection | undefined) => {
|
||||
const availableContext = this.getAppAvailableContext()
|
||||
this.contextManager.updateAvailableContextForApp(
|
||||
availableContext,
|
||||
untrack(() => this.contextManager.getSelectedContext())
|
||||
)
|
||||
this.contextManager.setSelectedAppContext(selectedContext, availableContext)
|
||||
}
|
||||
|
||||
listenForDbSchemasChanges = (dbSchemas: DBSchemas) => {
|
||||
this.displayMessages = ContextManager.updateDisplayMessages(
|
||||
untrack(() => this.displayMessages),
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { FlowModule } from '$lib/gen'
|
||||
import type { DisplayMessage } from './shared'
|
||||
import { langToExt } from '$lib/editorLangUtils'
|
||||
import type { ExtendedOpenFlow } from '$lib/components/flows/types'
|
||||
import type { AppSelection } from './app/core'
|
||||
|
||||
export interface ScriptOptions {
|
||||
lang: ScriptLang | 'bunnative'
|
||||
@@ -285,6 +286,40 @@ export default class ContextManager {
|
||||
return this.availableContext
|
||||
}
|
||||
|
||||
updateAvailableContextForApp(
|
||||
availableContext: ContextElement[],
|
||||
currentlySelectedContext: ContextElement[]
|
||||
) {
|
||||
const newSelectedContext: ContextElement[] = []
|
||||
|
||||
for (const context of currentlySelectedContext) {
|
||||
if (context.type === 'app_code_selection') {
|
||||
newSelectedContext.push(context)
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
context.type === 'app_frontend_file' ||
|
||||
context.type === 'app_backend_runnable' ||
|
||||
context.type === 'app_datatable'
|
||||
) {
|
||||
const refreshedContext = availableContext.find(
|
||||
(available) => available.type === context.type && available.title === context.title
|
||||
)
|
||||
|
||||
if (refreshedContext) {
|
||||
newSelectedContext.push({
|
||||
...refreshedContext,
|
||||
activeSelection: context.activeSelection
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.availableContext = availableContext
|
||||
this.selectedContext = newSelectedContext
|
||||
}
|
||||
|
||||
setScriptOptions(scriptOptions: ScriptOptions) {
|
||||
this.scriptOptions = scriptOptions
|
||||
}
|
||||
@@ -422,6 +457,47 @@ export default class ContextManager {
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedAppContext(
|
||||
selectedContext: AppSelection | undefined,
|
||||
availableContext: ContextElement[] | undefined
|
||||
) {
|
||||
this.selectedContext = this.selectedContext.filter(
|
||||
(context) =>
|
||||
!(
|
||||
context.activeSelection &&
|
||||
(context.type === 'app_frontend_file' || context.type === 'app_backend_runnable')
|
||||
)
|
||||
)
|
||||
|
||||
if (!availableContext) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectedAppContext =
|
||||
selectedContext?.type === 'frontend' && selectedContext.frontendPath
|
||||
? availableContext.find(
|
||||
(context) =>
|
||||
context.type === 'app_frontend_file' && context.title === selectedContext.frontendPath
|
||||
)
|
||||
: selectedContext?.type === 'backend' && selectedContext.backendKey
|
||||
? availableContext.find(
|
||||
(context) =>
|
||||
context.type === 'app_backend_runnable' &&
|
||||
context.title === selectedContext.backendKey
|
||||
)
|
||||
: undefined
|
||||
|
||||
if (selectedAppContext) {
|
||||
this.selectedContext = [
|
||||
{ ...selectedAppContext, activeSelection: true },
|
||||
...this.selectedContext.filter(
|
||||
(context) =>
|
||||
context.type !== selectedAppContext.type || context.title !== selectedAppContext.title
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
clearContext() {
|
||||
this.selectedContext = []
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
AppFiles,
|
||||
BackendRunnable,
|
||||
LintResult,
|
||||
SelectedContext
|
||||
AppTransientContext
|
||||
} from '../../app/core'
|
||||
|
||||
/**
|
||||
@@ -30,7 +30,10 @@ export function createAppEvalHelpers(
|
||||
let frontend: Record<string, string> = { ...initialFrontend }
|
||||
let backend: Record<string, BackendRunnable> = { ...initialBackend }
|
||||
let snapshotId = 0
|
||||
const snapshots: Map<number, { frontend: Record<string, string>; backend: Record<string, BackendRunnable> }> = new Map()
|
||||
const snapshots: Map<
|
||||
number,
|
||||
{ frontend: Record<string, string>; backend: Record<string, BackendRunnable> }
|
||||
> = new Map()
|
||||
|
||||
const helpers: AppAIChatHelpers = {
|
||||
// Frontend file operations
|
||||
@@ -78,9 +81,7 @@ export function createAppEvalHelpers(
|
||||
backend: { ...backend }
|
||||
}),
|
||||
|
||||
getSelectedContext: (): SelectedContext => ({
|
||||
type: 'none'
|
||||
}),
|
||||
getTransientContext: (): AppTransientContext => ({}),
|
||||
|
||||
// Snapshot management
|
||||
snapshot: () => {
|
||||
@@ -126,11 +127,7 @@ export function createAppEvalHelpers(
|
||||
return { success: true, result: [] }
|
||||
},
|
||||
|
||||
addTableToWhitelist: (
|
||||
_datatableName: string,
|
||||
_schemaName: string,
|
||||
_tableName: string
|
||||
) => {
|
||||
addTableToWhitelist: (_datatableName: string, _schemaName: string, _tableName: string) => {
|
||||
// No-op for eval testing - tables are not tracked in test context
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export async function runAppEval(
|
||||
const model = resolveModel(options?.variant, options?.model)
|
||||
|
||||
// Build user message
|
||||
const userMessage = prepareAppUserMessage(userPrompt, helpers.getSelectedContext())
|
||||
const userMessage = prepareAppUserMessage(userPrompt, helpers.getTransientContext(), [])
|
||||
|
||||
// Run the base evaluation
|
||||
const rawResult = await runEval({
|
||||
|
||||
@@ -82,28 +82,26 @@ export interface InspectorElementInfo {
|
||||
styles: Record<string, string>
|
||||
}
|
||||
|
||||
/** Context about the currently selected file or runnable in the app editor */
|
||||
export interface SelectedContext {
|
||||
/** Type of selection: 'frontend' for frontend files, 'backend' for backend runnables, or 'none' if nothing is selected */
|
||||
type: 'frontend' | 'backend' | 'none'
|
||||
/** The path of the selected frontend file (when type is 'frontend') */
|
||||
frontendPath?: string
|
||||
/** The content of the selected frontend file */
|
||||
frontendContent?: string
|
||||
/** The key of the selected backend runnable (when type is 'backend') */
|
||||
backendKey?: string
|
||||
/** The configuration of the selected backend runnable */
|
||||
backendRunnable?: BackendRunnable
|
||||
/** Current file or runnable selected in the app editor */
|
||||
export type AppSelection =
|
||||
| {
|
||||
type: 'frontend'
|
||||
frontendPath: string
|
||||
}
|
||||
| {
|
||||
type: 'backend'
|
||||
backendKey: string
|
||||
}
|
||||
| {
|
||||
type: 'none'
|
||||
}
|
||||
|
||||
/** Transient app context that does not live in the shared ContextManager selection list */
|
||||
export interface AppTransientContext {
|
||||
/** Inspector-selected element info (when user has used the inspector tool) */
|
||||
inspectorElement?: InspectorElementInfo
|
||||
/** Whether the file/runnable selection is excluded from being sent to the AI prompt */
|
||||
selectionExcluded?: boolean
|
||||
/** Function to toggle whether the selection is excluded from the prompt */
|
||||
toggleSelectionExcluded?: () => void
|
||||
/** Function to clear the inspector selection */
|
||||
clearInspector?: () => void
|
||||
/** Function to clear the runnable selection (go back to frontend view) */
|
||||
clearRunnable?: () => void
|
||||
/** Code selection from the editor (either frontend or backend) */
|
||||
codeSelection?: AppCodeSelectionElement
|
||||
/** Function to clear the code selection */
|
||||
@@ -138,7 +136,7 @@ export interface AppAIChatHelpers {
|
||||
deleteBackendRunnable: (key: string) => void
|
||||
// Combined view
|
||||
getFiles: () => AppFiles
|
||||
getSelectedContext: () => SelectedContext
|
||||
getTransientContext: () => AppTransientContext
|
||||
snapshot: () => number
|
||||
revertToSnapshot: (id: number) => void
|
||||
// Linting
|
||||
@@ -488,12 +486,22 @@ export const getAppTools = memo((): Tool<AppAIChatHelpers>[] => [
|
||||
def: getGetSelectedContextToolDef(),
|
||||
fn: async ({ helpers, toolId, toolCallbacks }) => {
|
||||
toolCallbacks.setToolStatus(toolId, { content: 'Getting selected context...' })
|
||||
const context = helpers.getSelectedContext()
|
||||
const currentSelection = getCurrentAppSelection(
|
||||
aiChatManager.contextManager.getSelectedContext()
|
||||
)
|
||||
const transientContext = helpers.getTransientContext()
|
||||
const context = {
|
||||
...currentSelection,
|
||||
...(transientContext.inspectorElement
|
||||
? { inspectorElement: transientContext.inspectorElement }
|
||||
: {}),
|
||||
...(transientContext.codeSelection ? { codeSelection: transientContext.codeSelection } : {})
|
||||
}
|
||||
const statusMsg =
|
||||
context.type === 'frontend'
|
||||
? `Frontend file selected: ${context.frontendPath}`
|
||||
: context.type === 'backend'
|
||||
? `Backend runnable selected: ${context.backendKey}`
|
||||
currentSelection.type === 'frontend'
|
||||
? `Frontend file selected: ${currentSelection.frontendPath}`
|
||||
: currentSelection.type === 'backend'
|
||||
? `Backend runnable selected: ${currentSelection.backendKey}`
|
||||
: 'No selection'
|
||||
toolCallbacks.setToolStatus(toolId, { content: statusMsg })
|
||||
return JSON.stringify(context, null, 2)
|
||||
@@ -981,73 +989,130 @@ ${policy.schema ? `\n**IMPORTANT**: Always use the schema prefix \`${schemaPrefi
|
||||
|
||||
/** Maximum characters for file content in context */
|
||||
const MAX_CONTEXT_CONTENT_LENGTH = 3000
|
||||
type AppContextElement = (
|
||||
| AppFrontendFileElement
|
||||
| AppBackendRunnableElement
|
||||
| AppDatatableElement
|
||||
) & { activeSelection?: boolean }
|
||||
type ActiveAppContextElement = (AppFrontendFileElement | AppBackendRunnableElement) & {
|
||||
activeSelection?: boolean
|
||||
}
|
||||
|
||||
function truncateContextContent(content: string, maxLength = MAX_CONTEXT_CONTENT_LENGTH): string {
|
||||
return content.length > maxLength ? content.slice(0, maxLength) + '\n... [TRUNCATED]' : content
|
||||
}
|
||||
|
||||
function formatAppContextElement(
|
||||
context: AppContextElement,
|
||||
options: { activeSelection?: boolean } = {}
|
||||
): string {
|
||||
if (context.type === 'app_frontend_file') {
|
||||
const truncatedContent = truncateContextContent(context.content)
|
||||
return options.activeSelection
|
||||
? `The user is currently viewing the frontend file: **${context.path}**\n\n\`\`\`\n${truncatedContent}\n\`\`\`\n`
|
||||
: `\n**Frontend File: ${context.path}**\n\`\`\`\n${truncatedContent}\n\`\`\`\n`
|
||||
}
|
||||
|
||||
if (context.type === 'app_backend_runnable') {
|
||||
const runnable = context.runnable
|
||||
let formattedContext = options.activeSelection
|
||||
? `The user is currently viewing the backend runnable: **${context.key}**\n`
|
||||
: `\n**Backend Runnable: ${context.key}**\n`
|
||||
|
||||
formattedContext += `- **Name**: ${runnable.name}\n`
|
||||
formattedContext += `- **Type**: ${runnable.type}\n`
|
||||
if (runnable.path) {
|
||||
formattedContext += `- **Path**: ${runnable.path}\n`
|
||||
}
|
||||
if (runnable.inlineScript) {
|
||||
formattedContext += `- **Language**: ${runnable.inlineScript.language}\n`
|
||||
formattedContext += `- **Code**:\n\`\`\`${runnable.inlineScript.language === 'bun' ? 'typescript' : 'python'}\n${truncateContextContent(runnable.inlineScript.content)}\n\`\`\`\n`
|
||||
}
|
||||
if (runnable.staticInputs && Object.keys(runnable.staticInputs).length > 0) {
|
||||
formattedContext += `- **Static inputs**: ${JSON.stringify(runnable.staticInputs)}\n`
|
||||
}
|
||||
return formattedContext
|
||||
}
|
||||
|
||||
const tableRef =
|
||||
context.schemaName === 'public'
|
||||
? `${context.datatableName}/${context.tableName}`
|
||||
: `${context.datatableName}/${context.schemaName}:${context.tableName}`
|
||||
return (
|
||||
`\n**Table: ${tableRef}**\n` +
|
||||
`- **Datatable**: ${context.datatableName}\n` +
|
||||
`- **Schema**: ${context.schemaName}\n` +
|
||||
`- **Table**: ${context.tableName}\n` +
|
||||
`- **Columns** (column_name -> type):\n\`\`\`json\n${truncateContextContent(JSON.stringify(context.columns, null, 2))}\n\`\`\`\n`
|
||||
)
|
||||
}
|
||||
|
||||
function getCurrentAppSelection(selectedContext: ContextElement[]): AppSelection {
|
||||
const activeContextElement = selectedContext.find(
|
||||
(context) =>
|
||||
context.activeSelection &&
|
||||
(context.type === 'app_frontend_file' || context.type === 'app_backend_runnable')
|
||||
) as ActiveAppContextElement | undefined
|
||||
|
||||
if (!activeContextElement) {
|
||||
return { type: 'none' }
|
||||
}
|
||||
|
||||
return activeContextElement.type === 'app_frontend_file'
|
||||
? {
|
||||
type: 'frontend',
|
||||
frontendPath: activeContextElement.path
|
||||
}
|
||||
: {
|
||||
type: 'backend',
|
||||
backendKey: activeContextElement.key
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedAppContextElement(
|
||||
selectedContext: ContextElement[]
|
||||
): ActiveAppContextElement | undefined {
|
||||
return selectedContext.find(
|
||||
(context) =>
|
||||
context.activeSelection &&
|
||||
(context.type === 'app_frontend_file' || context.type === 'app_backend_runnable')
|
||||
) as ActiveAppContextElement | undefined
|
||||
}
|
||||
|
||||
function getAdditionalAppContextElements(selectedContext: ContextElement[]): AppContextElement[] {
|
||||
return selectedContext.filter(
|
||||
(context) =>
|
||||
!context.activeSelection &&
|
||||
(context.type === 'app_frontend_file' ||
|
||||
context.type === 'app_backend_runnable' ||
|
||||
context.type === 'app_datatable')
|
||||
) as AppContextElement[]
|
||||
}
|
||||
|
||||
export function prepareAppUserMessage(
|
||||
instructions: string,
|
||||
selectedContext?: SelectedContext,
|
||||
additionalContext?: ContextElement[]
|
||||
transientContext?: AppTransientContext,
|
||||
selectedContext: ContextElement[] = []
|
||||
): ChatCompletionUserMessageParam {
|
||||
let content = ''
|
||||
const activeContextElement = getSelectedAppContextElement(selectedContext)
|
||||
const additionalContextElements = getAdditionalAppContextElements(selectedContext)
|
||||
|
||||
// Check if we have any context to add
|
||||
const hasSelectedContext =
|
||||
selectedContext && (selectedContext.type !== 'none' || selectedContext.inspectorElement)
|
||||
const hasAdditionalContext = additionalContext && additionalContext.length > 0
|
||||
!!activeContextElement ||
|
||||
!!transientContext?.inspectorElement ||
|
||||
!!transientContext?.codeSelection
|
||||
const hasAdditionalContext = additionalContextElements.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
|
||||
) {
|
||||
content += `The user is currently viewing the frontend file: **${selectedContext.frontendPath}**\n`
|
||||
if (selectedContext.frontendContent) {
|
||||
const truncatedContent =
|
||||
selectedContext.frontendContent.length > MAX_CONTEXT_CONTENT_LENGTH
|
||||
? selectedContext.frontendContent.slice(0, MAX_CONTEXT_CONTENT_LENGTH) +
|
||||
'\n... [TRUNCATED]'
|
||||
: selectedContext.frontendContent
|
||||
content += `\n\`\`\`\n${truncatedContent}\n\`\`\`\n`
|
||||
}
|
||||
if (activeContextElement) {
|
||||
content += formatAppContextElement(activeContextElement, { activeSelection: true })
|
||||
}
|
||||
|
||||
// Add backend runnable context with content (unless excluded)
|
||||
if (
|
||||
selectedContext &&
|
||||
selectedContext.type === 'backend' &&
|
||||
selectedContext.backendKey &&
|
||||
!selectedContext.selectionExcluded
|
||||
) {
|
||||
content += `The user is currently viewing the backend runnable: **${selectedContext.backendKey}**\n`
|
||||
if (selectedContext.backendRunnable) {
|
||||
const runnable = selectedContext.backendRunnable
|
||||
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`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add inspector element context if available
|
||||
if (selectedContext?.inspectorElement) {
|
||||
const el = selectedContext.inspectorElement
|
||||
if (transientContext?.inspectorElement) {
|
||||
const el = transientContext.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`
|
||||
content += `- **Selector path**: ${el.path}\n`
|
||||
@@ -1057,77 +1122,24 @@ export function prepareAppUserMessage(
|
||||
el.textContent.length > 100 ? el.textContent.slice(0, 100) + '...' : el.textContent
|
||||
content += `- **Text content**: "${truncatedText}"\n`
|
||||
}
|
||||
// Include HTML (truncated) for more context
|
||||
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
|
||||
if (transientContext?.codeSelection) {
|
||||
const selection = transientContext.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`
|
||||
content += `\`\`\`\n${truncateContextContent(selection.content)}\n\`\`\`\n`
|
||||
}
|
||||
|
||||
// Add additional context from @ mentions
|
||||
if (additionalContext && additionalContext.length > 0) {
|
||||
if (additionalContextElements.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`
|
||||
}
|
||||
for (const ctx of additionalContextElements) {
|
||||
content += formatAppContextElement(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -173,4 +173,5 @@ export type ContextElement = (
|
||||
| WorkspaceFlowElement
|
||||
) & {
|
||||
deletable?: boolean
|
||||
activeSelection?: boolean
|
||||
}
|
||||
|
||||
@@ -18,7 +18,13 @@
|
||||
import { isRunnableByName, isRunnableByPath } from '../apps/inputType'
|
||||
import { aiChatManager, AIMode } from '../copilot/chat/AIChatManager.svelte'
|
||||
import { onMount, untrack } from 'svelte'
|
||||
import type { LintResult, DataTableSchema, InspectorElementInfo } from '../copilot/chat/app/core'
|
||||
import type {
|
||||
LintResult,
|
||||
DataTableSchema,
|
||||
InspectorElementInfo,
|
||||
AppSelection,
|
||||
AppTransientContext
|
||||
} from '../copilot/chat/app/core'
|
||||
import type { AppCodeSelectionElement } from '../copilot/chat/context'
|
||||
import { rawAppLintStore } from './lintStore'
|
||||
import { dbSchemas } from '$lib/stores'
|
||||
@@ -396,40 +402,7 @@
|
||||
backend: aiChatManager.appAiChatHelpers?.getBackendRunnables() ?? {}
|
||||
}
|
||||
},
|
||||
getSelectedContext: () => {
|
||||
const baseContext = {
|
||||
inspectorElement: inspectorElement,
|
||||
selectionExcluded: selectionExcludedFromPrompt,
|
||||
toggleSelectionExcluded: toggleSelectionExcluded,
|
||||
clearInspector: clearInspectorSelection,
|
||||
clearRunnable: handleClearRunnable,
|
||||
codeSelection: codeSelection,
|
||||
clearCodeSelection: () => {
|
||||
codeSelection = undefined
|
||||
}
|
||||
}
|
||||
if (selectedRunnable) {
|
||||
const runnable = convertToBackendRunnable(selectedRunnable, runnables[selectedRunnable])
|
||||
return {
|
||||
type: 'backend' as const,
|
||||
backendKey: selectedRunnable,
|
||||
backendRunnable: runnable,
|
||||
...baseContext
|
||||
}
|
||||
}
|
||||
if (selectedDocument) {
|
||||
return {
|
||||
type: 'frontend' as const,
|
||||
frontendPath: selectedDocument,
|
||||
frontendContent: files?.[selectedDocument],
|
||||
...baseContext
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'none' as const,
|
||||
...baseContext
|
||||
}
|
||||
},
|
||||
getTransientContext: () => getAppTransientContext(),
|
||||
snapshot: () => {
|
||||
// Force create snapshot for AI - it needs a restore point
|
||||
return (
|
||||
@@ -589,15 +562,21 @@
|
||||
let selectedRunnable: string | undefined = $state(undefined)
|
||||
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
|
||||
}
|
||||
|
||||
let modules = $state({}) as Modules
|
||||
|
||||
function getAppTransientContext(): AppTransientContext {
|
||||
return {
|
||||
inspectorElement: inspectorElement,
|
||||
clearInspector: clearInspectorSelection,
|
||||
codeSelection: codeSelection,
|
||||
clearCodeSelection: () => {
|
||||
codeSelection = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize Windows-style path separators to Linux-style
|
||||
function normalizeFilePaths(
|
||||
filesObj: Record<string, string> | undefined
|
||||
@@ -703,28 +682,34 @@
|
||||
)
|
||||
}
|
||||
|
||||
function handleClearRunnable() {
|
||||
selectedRunnable = undefined
|
||||
}
|
||||
|
||||
// Track previous values for change detection
|
||||
let prevSelectedRunnable: string | undefined = undefined
|
||||
let prevSelectedDocument: string | undefined = undefined
|
||||
|
||||
// Clear inspector and reset exclusion when selection changes
|
||||
// Clear inspector when the active file/runnable changes
|
||||
$effect(() => {
|
||||
if (selectedRunnable !== prevSelectedRunnable || selectedDocument !== prevSelectedDocument) {
|
||||
// Only clear if we're actually switching to something different
|
||||
if (prevSelectedRunnable !== undefined || prevSelectedDocument !== undefined) {
|
||||
clearInspectorSelection()
|
||||
}
|
||||
// Reset exclusion when switching files/runnables
|
||||
selectionExcludedFromPrompt = false
|
||||
prevSelectedRunnable = selectedRunnable
|
||||
prevSelectedDocument = selectedDocument
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
const appSelection: AppSelection = selectedRunnable
|
||||
? { type: 'backend' as const, backendKey: selectedRunnable }
|
||||
: selectedDocument
|
||||
? { type: 'frontend' as const, frontendPath: selectedDocument }
|
||||
: { type: 'none' as const }
|
||||
|
||||
untrack(() => {
|
||||
aiChatManager.syncAppSelection(appSelection)
|
||||
})
|
||||
})
|
||||
|
||||
function handleUndo() {
|
||||
// Create a snapshot if we're at the latest position with pending changes
|
||||
if (historyManager.needsSnapshotBeforeNav) {
|
||||
|
||||
Reference in New Issue
Block a user