Compare commits

...

3 Commits

Author SHA1 Message Date
centdix
f31c68e8be refactor: clean up app chat selection
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-13 12:32:08 +02:00
centdix
31975aaa55 refactor: remove app chat fallback
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-03 17:50:42 +02:00
centdix
2e0a727553 fix: use shared app chat context
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-03 17:44:02 +02:00
9 changed files with 290 additions and 242 deletions

View File

@@ -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,

View File

@@ -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"

View File

@@ -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),

View File

@@ -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 = []
}

View File

@@ -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
}
}

View File

@@ -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({

View File

@@ -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)
}
}

View File

@@ -173,4 +173,5 @@ export type ContextElement = (
| WorkspaceFlowElement
) & {
deletable?: boolean
activeSelection?: boolean
}

View File

@@ -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) {