mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 16:03:21 +00:00
refactor: clean up app chat selection
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
31975aaa55
commit
f31c68e8be
@@ -24,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,
|
||||
@@ -101,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>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
prepareAppSystemMessage,
|
||||
prepareAppUserMessage,
|
||||
type AppAIChatHelpers,
|
||||
type SelectedContext
|
||||
type AppSelection
|
||||
} from './app/core'
|
||||
import ContextManager from './ContextManager.svelte'
|
||||
import HistoryManager from './HistoryManager.svelte'
|
||||
@@ -446,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,
|
||||
@@ -628,9 +625,7 @@ class AIChatManager {
|
||||
role: 'user',
|
||||
content: this.instructions,
|
||||
contextElements:
|
||||
this.mode === AIMode.SCRIPT ||
|
||||
this.mode === AIMode.FLOW ||
|
||||
this.mode === AIMode.APP
|
||||
this.mode === AIMode.SCRIPT || this.mode === AIMode.FLOW || this.mode === AIMode.APP
|
||||
? oldSelectedContext
|
||||
: undefined,
|
||||
snapshot,
|
||||
@@ -671,7 +666,7 @@ class AIChatManager {
|
||||
case AIMode.APP:
|
||||
userMessage = prepareAppUserMessage(
|
||||
oldInstructions,
|
||||
this.appAiChatHelpers?.getSelectedContext(),
|
||||
this.appAiChatHelpers?.getTransientContext(),
|
||||
oldSelectedContext
|
||||
)
|
||||
break
|
||||
@@ -918,18 +913,13 @@ class AIChatManager {
|
||||
}
|
||||
}
|
||||
|
||||
syncAppSelection = (
|
||||
selectedContext: Pick<SelectedContext, 'type' | 'frontendPath' | 'backendKey'> | undefined
|
||||
) => {
|
||||
syncAppSelection = (selectedContext: AppSelection | undefined) => {
|
||||
const availableContext = this.getAppAvailableContext()
|
||||
this.contextManager.updateAvailableContextForApp(
|
||||
availableContext,
|
||||
untrack(() => this.contextManager.getSelectedContext())
|
||||
)
|
||||
this.contextManager.setSelectedAppContext(
|
||||
selectedContext,
|
||||
availableContext
|
||||
)
|
||||
this.contextManager.setSelectedAppContext(selectedContext, availableContext)
|
||||
}
|
||||
|
||||
listenForDbSchemasChanges = (dbSchemas: DBSchemas) => {
|
||||
|
||||
@@ -8,7 +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 { SelectedContext } from './app/core'
|
||||
import type { AppSelection } from './app/core'
|
||||
|
||||
export interface ScriptOptions {
|
||||
lang: ScriptLang | 'bunnative'
|
||||
@@ -458,7 +458,7 @@ export default class ContextManager {
|
||||
}
|
||||
|
||||
setSelectedAppContext(
|
||||
selectedContext: Pick<SelectedContext, 'type' | 'frontendPath' | 'backendKey'> | undefined,
|
||||
selectedContext: AppSelection | undefined,
|
||||
availableContext: ContextElement[] | undefined
|
||||
) {
|
||||
this.selectedContext = this.selectedContext.filter(
|
||||
@@ -477,15 +477,14 @@ export default class ContextManager {
|
||||
selectedContext?.type === 'frontend' && selectedContext.frontendPath
|
||||
? availableContext.find(
|
||||
(context) =>
|
||||
context.type === 'app_frontend_file' &&
|
||||
context.title === selectedContext.frontendPath
|
||||
)
|
||||
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) {
|
||||
|
||||
@@ -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,18 +82,22 @@ 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
|
||||
/** Function to clear the inspector selection */
|
||||
@@ -132,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
|
||||
@@ -482,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)
|
||||
@@ -1033,53 +1047,72 @@ function formatAppContextElement(
|
||||
)
|
||||
}
|
||||
|
||||
function getAppContextElements(
|
||||
additionalContext: ContextElement[]
|
||||
): {
|
||||
activeContextElements: ActiveAppContextElement[]
|
||||
additionalContextElements: AppContextElement[]
|
||||
} {
|
||||
const activeContextElements = additionalContext.filter(
|
||||
function getCurrentAppSelection(selectedContext: ContextElement[]): AppSelection {
|
||||
const activeContextElement = selectedContext.find(
|
||||
(context) =>
|
||||
context.activeSelection &&
|
||||
(context.type === 'app_frontend_file' || context.type === 'app_backend_runnable')
|
||||
) as ActiveAppContextElement[]
|
||||
const additionalContextElements = additionalContext.filter(
|
||||
) 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[]
|
||||
|
||||
return { activeContextElements, additionalContextElements }
|
||||
}
|
||||
|
||||
export function prepareAppUserMessage(
|
||||
instructions: string,
|
||||
selectedContext?: SelectedContext,
|
||||
additionalContext?: ContextElement[]
|
||||
transientContext?: AppTransientContext,
|
||||
selectedContext: ContextElement[] = []
|
||||
): ChatCompletionUserMessageParam {
|
||||
let content = ''
|
||||
const { activeContextElements, additionalContextElements } = getAppContextElements(
|
||||
additionalContext ?? []
|
||||
)
|
||||
const activeContextElement = getSelectedAppContextElement(selectedContext)
|
||||
const additionalContextElements = getAdditionalAppContextElements(selectedContext)
|
||||
|
||||
// Check if we have any context to add
|
||||
const hasSelectedContext =
|
||||
activeContextElements.length > 0 || !!selectedContext?.inspectorElement || !!selectedContext?.codeSelection
|
||||
!!activeContextElement ||
|
||||
!!transientContext?.inspectorElement ||
|
||||
!!transientContext?.codeSelection
|
||||
const hasAdditionalContext = additionalContextElements.length > 0
|
||||
|
||||
if (hasSelectedContext || hasAdditionalContext) {
|
||||
content += `## SELECTED CONTEXT:\n`
|
||||
|
||||
for (const activeContextElement of activeContextElements) {
|
||||
if (activeContextElement) {
|
||||
content += formatAppContextElement(activeContextElement, { activeSelection: true })
|
||||
}
|
||||
|
||||
// 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`
|
||||
@@ -1089,14 +1122,12 @@ 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`
|
||||
@@ -1104,7 +1135,6 @@ export function prepareAppUserMessage(
|
||||
content += `\`\`\`\n${truncateContextContent(selection.content)}\n\`\`\`\n`
|
||||
}
|
||||
|
||||
// Add additional context from @ mentions
|
||||
if (additionalContextElements.length > 0) {
|
||||
content += `\n### ADDITIONAL CONTEXT (mentioned by user):\n`
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
LintResult,
|
||||
DataTableSchema,
|
||||
InspectorElementInfo,
|
||||
SelectedContext
|
||||
AppSelection,
|
||||
AppTransientContext
|
||||
} from '../copilot/chat/app/core'
|
||||
import type { AppCodeSelectionElement } from '../copilot/chat/context'
|
||||
import { rawAppLintStore } from './lintStore'
|
||||
@@ -401,7 +402,7 @@
|
||||
backend: aiChatManager.appAiChatHelpers?.getBackendRunnables() ?? {}
|
||||
}
|
||||
},
|
||||
getSelectedContext: () => getSelectedAppContext(),
|
||||
getTransientContext: () => getAppTransientContext(),
|
||||
snapshot: () => {
|
||||
// Force create snapshot for AI - it needs a restore point
|
||||
return (
|
||||
@@ -565,8 +566,8 @@
|
||||
|
||||
let modules = $state({}) as Modules
|
||||
|
||||
function getSelectedAppContext(): SelectedContext {
|
||||
const baseContext = {
|
||||
function getAppTransientContext(): AppTransientContext {
|
||||
return {
|
||||
inspectorElement: inspectorElement,
|
||||
clearInspector: clearInspectorSelection,
|
||||
codeSelection: codeSelection,
|
||||
@@ -574,30 +575,6 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize Windows-style path separators to Linux-style
|
||||
@@ -722,7 +699,7 @@
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
const appSelection = selectedRunnable
|
||||
const appSelection: AppSelection = selectedRunnable
|
||||
? { type: 'backend' as const, backendKey: selectedRunnable }
|
||||
: selectedDocument
|
||||
? { type: 'frontend' as const, frontendPath: selectedDocument }
|
||||
|
||||
Reference in New Issue
Block a user