feat(aichat): add get_lint_errors tool for script and flow mode (#7431)

* feat(aichat): add get_lint_errors tool for script and flow mode

This adds a new `get_lint_errors` tool to the AI chat for script and flow modes,
similar to what exists for app mode.

For script mode:
- Added `getLintErrors` function to Editor.svelte that returns lint errors from Monaco
- Added `ScriptLintResult` and `ScriptLintError` interfaces
- Added `get_lint_errors` tool definition and implementation
- Updated system prompt to instruct AI to use the tool after code changes

For flow mode:
- Added `FlowLintResult` interface for flow-level lint results
- Added `get_lint_errors` tool that gets lint errors from the currently selected module
- Updated system prompt to include linting in the tool selection guide

The AI is now instructed to always use `get_lint_errors` after making code changes
and fix any errors before proceeding with testing.

Closes #7430

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* fix for script

* fix for flow

* cleaning

* fix DatatableCreationPolicy

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: centdix <farhadg110@gmail.com>
This commit is contained in:
claude[bot]
2025-12-23 16:41:42 +00:00
committed by GitHub
parent 1c0889b3dc
commit 0a94d852f9
10 changed files with 236 additions and 37 deletions
+19
View File
@@ -93,6 +93,7 @@
import AIChatInlineWidget from './copilot/chat/AIChatInlineWidget.svelte'
import { writable } from 'svelte/store'
import { formatResourceTypes } from './copilot/chat/script/core'
import type { ScriptLintResult } from './copilot/chat/shared'
import FakeMonacoPlaceHolder from './FakeMonacoPlaceHolder.svelte'
import { editorPositionMap } from '$lib/utils'
import { extToLang, langToExt } from '$lib/editorLangUtils'
@@ -464,6 +465,24 @@
return scriptLang
}
/** Get lint errors and warnings from the Monaco editor */
export function getLintErrors(): ScriptLintResult {
if (!model) {
return { errorCount: 0, warningCount: 0, errors: [], warnings: [] }
}
const markers = meditor.getModelMarkers({ resource: model.uri })
const errors = markers.filter((m) => m.severity === MarkerSeverity.Error)
const warnings = markers.filter((m) => m.severity === MarkerSeverity.Warning)
return {
errorCount: errors.length,
warningCount: warnings.length,
errors,
warnings
}
}
let command: IDisposable | undefined = undefined
let sqlTypeCompletor: IDisposable | undefined = $state(undefined)
@@ -446,6 +446,7 @@
disableCollaboration()
aiChatManager.scriptEditorApplyCode = undefined
aiChatManager.scriptEditorShowDiffMode = undefined
aiChatManager.scriptEditorGetLintErrors = undefined
aiChatManager.scriptEditorOptions = undefined
aiChatManager.saveAndClear()
aiChatManager.changeMode(AIMode.NAVIGATOR)
@@ -535,6 +536,9 @@
await editor?.reviewAndApplyCode(code, opts)
}
aiChatManager.scriptEditorShowDiffMode = showDiffMode
aiChatManager.scriptEditorGetLintErrors = () => {
return editor?.getLintErrors() ?? { errorCount: 0, warningCount: 0, errors: [], warnings: [] }
}
})
})
</script>
@@ -262,7 +262,9 @@
{:else}
<div class="flex flex-row gap-2 min-w-0 flex-wrap items-center">
<ChatMode />
<DatatableCreationPolicy />
{#if aiChatManager.mode === AIMode.APP}
<DatatableCreationPolicy />
{/if}
<ProviderModelSelector />
</div>
{/if}
@@ -30,6 +30,7 @@ import {
prepareScriptSystemMessage,
prepareScriptTools
} from './script/core'
import type { ScriptLintResult } from './shared'
import { navigatorTools, prepareNavigatorSystemMessage } from './navigator/core'
import { loadApiTools } from './api/apiTools'
import { prepareScriptUserMessage } from './script/core'
@@ -97,6 +98,7 @@ class AIChatManager {
undefined
)
scriptEditorShowDiffMode = $state<(() => void) | undefined>(undefined)
scriptEditorGetLintErrors = $state<(() => ScriptLintResult) | undefined>(undefined)
flowAiChatHelpers = $state<FlowAIChatHelpers | undefined>(undefined)
appAiChatHelpers = $state<AppAIChatHelpers | undefined>(undefined)
/** Datatable creation policy: enabled flag, datatable name, and optional schema */
@@ -241,6 +243,12 @@ class AIChatManager {
},
applyCode: (code: string, opts?: ReviewChangesOpts) => {
this.scriptEditorApplyCode?.(code, opts)
},
getLintErrors: () => {
if (this.scriptEditorGetLintErrors) {
return this.scriptEditorGetLintErrors()
}
return { errorCount: 0, warningCount: 0, errors: [], warnings: [] }
}
}
if (options?.closeScriptSettings) {
@@ -982,14 +990,22 @@ class AIChatManager {
currentEditor.showDiffMode()
}
}
this.scriptEditorGetLintErrors = () => {
if (currentEditor && currentEditor.type === 'script') {
return currentEditor.editor.getLintErrors()
}
return { errorCount: 0, warningCount: 0, errors: [], warnings: [] }
}
} else {
this.scriptEditorApplyCode = undefined
this.scriptEditorShowDiffMode = undefined
this.scriptEditorGetLintErrors = undefined
}
return () => {
this.scriptEditorApplyCode = undefined
this.scriptEditorShowDiffMode = undefined
this.scriptEditorGetLintErrors = undefined
}
}
@@ -1,7 +1,7 @@
<script lang="ts">
import { AlertTriangle } from 'lucide-svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { aiChatManager, AIMode } from './AIChatManager.svelte'
import { aiChatManager } from './AIChatManager.svelte'
import DefaultDatabaseSelector from '$lib/components/raw_apps/DefaultDatabaseSelector.svelte'
import { workspaceStore } from '$lib/stores'
import { createDatatablesResource } from '$lib/components/raw_apps/datatableUtils.svelte'
@@ -14,6 +14,7 @@
// Auto-select first datatable when datatables load and none is selected
$effect(() => {
if (
datatables.current &&
datatables.current.length > 0 &&
aiChatManager.datatableCreationPolicy.enabled &&
!aiChatManager.datatableCreationPolicy.datatable
@@ -39,35 +40,33 @@
}
</script>
{#if aiChatManager.mode === AIMode.APP}
<div class="min-w-0 flex items-center gap-1 pt-0.5">
{#if hasNoDatatables}
<!-- Warning when no datatables are available -->
<div
class="text-2xs flex flex-row items-center gap-1 text-red-600 dark:text-red-400 px-1"
title="No datatables configured. Add datatables in the Data panel so AI can create tables."
>
<AlertTriangle size={12} class="shrink-0" />
<span class="truncate">No datatables</span>
</div>
{:else}
<!-- Toggle for new tables -->
<div class="flex items-center gap-1">
<Toggle
size="xs"
checked={aiChatManager.datatableCreationPolicy.enabled}
on:change={(e) => handleToggle(e.detail)}
/>
<span class="text-2xs text-secondary whitespace-nowrap">tables creation</span>
</div>
<!-- Settings icon with popover -->
<DefaultDatabaseSelector
datatable={aiChatManager.datatableCreationPolicy.datatable}
schema={aiChatManager.datatableCreationPolicy.schema}
onChange={handleDefaultChange}
description="Set the default datatable and schema for new tables. When table creation is enabled, AI can create tables here if needed."
<div class="min-w-0 flex items-center gap-1 pt-0.5">
{#if hasNoDatatables}
<!-- Warning when no datatables are available -->
<div
class="text-2xs flex flex-row items-center gap-1 text-red-600 dark:text-red-400 px-1"
title="No datatables configured. Add datatables in the Data panel so AI can create tables."
>
<AlertTriangle size={12} class="shrink-0" />
<span class="truncate">No datatables</span>
</div>
{:else}
<!-- Toggle for new tables -->
<div class="flex items-center gap-1">
<Toggle
size="xs"
checked={aiChatManager.datatableCreationPolicy.enabled}
on:change={(e) => handleToggle(e.detail)}
/>
{/if}
</div>
{/if}
<span class="text-2xs text-secondary whitespace-nowrap">tables creation</span>
</div>
<!-- Settings icon with popover -->
<DefaultDatabaseSelector
datatable={aiChatManager.datatableCreationPolicy.datatable}
schema={aiChatManager.datatableCreationPolicy.schema}
onChange={handleDefaultChange}
description="Set the default datatable and schema for new tables. When table creation is enabled, AI can create tables here if needed."
/>
{/if}
</div>
@@ -88,6 +88,11 @@ export function createFlowEvalHelpers(
testFlow: async () => {
// Return mock job ID - we don't actually run flows in eval
return 'mock-job-id-' + Date.now()
},
getLintErrors: async () => {
// Return empty lint result for eval
return { errorCount: 0, warningCount: 0, errors: [], warnings: [] }
}
}
@@ -12,6 +12,7 @@
import { getSubModules } from '$lib/components/flows/flowExplorer'
import { SPECIAL_MODULE_IDS } from '../shared'
import type { FlowCopilotContext } from '../../flow'
import type { ScriptLintResult } from '../shared'
let {
flowModuleSchemaMap,
@@ -170,6 +171,29 @@
return await onTestFlow?.(conversationId)
},
getLintErrors: async (moduleId: string): Promise<ScriptLintResult> => {
// Focus the module first
selectionManager.selectId(moduleId)
// Poll until editor exists
const maxWait = 3000
const pollInterval = 100
let elapsed = 0
while (elapsed < maxWait) {
if ($currentEditor?.type === 'script') {
// Wait 500ms for LSP to analyze the code
await new Promise((resolve) => setTimeout(resolve, 500))
return $currentEditor.editor.getLintErrors()
}
await new Promise((resolve) => setTimeout(resolve, pollInterval))
elapsed += pollInterval
}
return { errorCount: 0, warningCount: 0, errors: [], warnings: [] }
},
setFlowJson: async (
modules: FlowModule[] | undefined,
schema: Record<string, any> | undefined
@@ -29,7 +29,9 @@ import {
buildContextString,
applyCodePiecesToFlowModules,
findModuleById,
SPECIAL_MODULE_IDS
SPECIAL_MODULE_IDS,
formatScriptLintResult,
type ScriptLintResult
} from '../shared'
import type { ContextElement } from '../context'
import type { ExtendedOpenFlow } from '$lib/components/flows/types'
@@ -77,6 +79,9 @@ export interface FlowAIChatHelpers {
/** Run a test of the current flow using the UI's preview mechanism */
testFlow: (args?: Record<string, any>, conversationId?: string) => Promise<string | undefined>
/** Get lint errors from a specific module (focuses it first, waits for Monaco to analyze) */
getLintErrors: (moduleId: string) => Promise<ScriptLintResult>
}
const searchScriptsSchema = z.object({
@@ -224,6 +229,16 @@ const setModuleCodeToolDef = createToolDef(
'Set or modify the code for an existing inline script module. Use this for quick code-only changes. The module must already exist in the flow.'
)
const getLintErrorsSchema = z.object({
module_id: z.string().describe('The ID of the module to get lint errors for.')
})
const getLintErrorsToolDef = createToolDef(
getLintErrorsSchema,
'get_lint_errors',
'Get lint errors and warnings from a rawscript module. Pass module_id to focus a specific module and check its errors. ALWAYS call this for EACH module where you modified inline script code.'
)
const workspaceScriptsSearch = new WorkspaceScriptsSearch()
export const flowTools: Tool<FlowAIChatHelpers>[] = [
@@ -580,6 +595,29 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
})
return `Flow updated`
}
},
{
def: getLintErrorsToolDef,
fn: async ({ args, helpers, toolCallbacks, toolId }) => {
const parsedArgs = getLintErrorsSchema.parse(args)
toolCallbacks.setToolStatus(toolId, {
content: `Getting lint errors for module "${parsedArgs.module_id}"...`
})
const lintResult = await helpers.getLintErrors(parsedArgs.module_id)
const status =
lintResult.errorCount > 0
? `Found ${lintResult.errorCount} error(s)`
: lintResult.warningCount > 0
? `Found ${lintResult.warningCount} warning(s)`
: 'No issues found'
toolCallbacks.setToolStatus(toolId, { content: status })
return formatScriptLintResult(lintResult)
}
}
]
@@ -602,7 +640,11 @@ export function prepareFlowSystemMessage(customPrompt?: string): ChatCompletionS
- **Find workspace scripts** → \`search_scripts\`
- **Find Windmill Hub scripts** → \`search_hub_scripts\`
**Testing:**
**Testing & Linting:**
- **Check for lint errors after writing new code or modifying existing code** → \`get_lint_errors({ module_id: "..." })\`
- ALWAYS call this for EACH rawscript module that you added or modified
- Pass the module_id to get the lint errors for that module
- Example: After modifying modules "a" and "b", call \`get_lint_errors({ module_id: "a" })\` and \`get_lint_errors({ module_id: "b" })\`
- **Test entire flow** → \`test_run_flow\`
- **Test single step** → \`test_run_step\`
@@ -812,6 +854,8 @@ Example: Before writing TypeScript/Bun code, call \`get_instructions_for_code_ge
- **First call \`get_instructions_for_code_generation\` to get the correct code format**
- Always define \`input_transforms\` to connect parameters to flow inputs or previous step results
3. **After making code changes, ALWAYS use \`get_lint_errors\` to check for issues.** Fix any errors before proceeding with testing.
### AI Agent Modules
AI agents can use tools to accomplish tasks. When creating an AI agent module:
@@ -15,7 +15,9 @@ import {
type Tool,
executeTestRun,
buildTestRunArgs,
buildContextString
buildContextString,
type ScriptLintResult,
formatScriptLintResult
} from '../shared'
import { setupTypeAcquisition, type DepsToGet } from '$lib/ata'
import { getModelContextWindow } from '../../lib'
@@ -176,7 +178,7 @@ function buildChatSystemPrompt(currentModel: AIProviderModel) {
- You can also receive a \`DIFF\` of the changes that have been made to the code. You should use this diff to give better answers.
- Before giving your answer, check again that you carefully followed these instructions.
- When asked to create a script that communicates with an external service, you can use the \`search_hub_scripts\` tool to search for relevant scripts in the hub. Make sure the language is the same as what the user is coding in. If you do not find any relevant scripts, you can use the \`search_npm_packages\` tool to search for relevant packages and their documentation. Always give a link to the documentation in your answer if possible.
- After applying code changes with the \`${editToolName}\` tool, ALWAYS use the \`test_run_script\` tool to test the code, and iterate on the code until it works as expected (MAX 3 times). If the user cancels the test run, do not try again and wait for the next user instruction.
- After applying code changes with the \`${editToolName}\` tool, ALWAYS use the \`get_lint_errors\` tool to check for lint errors. If there are errors, fix them before proceeding. Then use the \`test_run_script\` tool to test the code, and iterate on the code until it works as expected (MAX 3 times). If the user cancels the test run, do not try again and wait for the next user instruction.
Important:
${useDiffBasedEdit ? '- Each old_string must match the exact text in the current code, including whitespace and indentation.' : ''}
@@ -326,6 +328,7 @@ export function prepareScriptTools(
tools.push(editCodeTool)
}
tools.push(testRunScriptTool)
tools.push(getLintErrorsTool)
return tools
}
@@ -407,6 +410,8 @@ export interface ScriptChatHelpers {
args: Record<string, any>
}
applyCode: (code: string, opts?: ReviewChangesOpts) => Promise<void>
/** Get lint errors from the Monaco editor */
getLintErrors?: () => ScriptLintResult
}
export const resourceTypeTool: Tool<ScriptChatHelpers> = {
@@ -699,6 +704,22 @@ const TEST_RUN_SCRIPT_TOOL: ChatCompletionFunctionTool = {
}
}
const GET_LINT_ERRORS_TOOL: ChatCompletionFunctionTool = {
type: 'function',
function: {
name: 'get_lint_errors',
description:
'Get lint errors and warnings from the current script in the editor. Use this after making code changes to check for issues.',
parameters: {
type: 'object',
properties: {},
additionalProperties: false,
strict: true,
required: []
}
}
}
export const editCodeToolWithDiff: Tool<ScriptChatHelpers> = {
def: EDIT_CODE_TOOL_WITH_DIFF,
streamArguments: true,
@@ -860,3 +881,31 @@ export const testRunScriptTool: Tool<ScriptChatHelpers> = {
confirmationMessage: 'Run script test',
showDetails: true
}
export const getLintErrorsTool: Tool<ScriptChatHelpers> = {
def: GET_LINT_ERRORS_TOOL,
fn: async function ({ helpers, toolCallbacks, toolId }) {
toolCallbacks.setToolStatus(toolId, { content: 'Getting lint errors...' })
if (!helpers.getLintErrors) {
toolCallbacks.setToolStatus(toolId, {
content: 'Lint errors not available',
error: 'getLintErrors helper is not available in this context'
})
return 'Lint errors are not available in this context. The editor may not support lint error reporting.'
}
const lintResult = helpers.getLintErrors()
const status =
lintResult.errorCount > 0
? `Found ${lintResult.errorCount} error(s)`
: lintResult.warningCount > 0
? `Found ${lintResult.warningCount} warning(s)`
: 'No issues found'
toolCallbacks.setToolStatus(toolId, { content: status })
return formatScriptLintResult(lintResult)
}
}
@@ -24,6 +24,7 @@ import { z } from 'zod'
import { ScriptService, JobService, type CompletedJob, type FlowModule } from '$lib/gen'
import { scriptLangToEditorLang } from '$lib/scripts'
import { getCurrentModel } from '$lib/aiStore'
import { type editor as meditor } from 'monaco-editor'
// Prettify function for code arguments - extracts and formats code from JSON
function prettifyCodeArguments(content: string): string {
@@ -849,3 +850,39 @@ function formatResultSummary(result: unknown, logs: string | undefined, success:
resultSummary += formatLogs(logs) ?? 'No logs available'
return resultSummary
}
// ============= Script/Flow Lint Types =============
/** Result of linting a script */
export interface ScriptLintResult {
errorCount: number
warningCount: number
errors: meditor.IMarker[]
warnings: meditor.IMarker[]
}
/** Format script lint result for display */
export function formatScriptLintResult(lintResult: ScriptLintResult): string {
let response = ''
const hasIssues = lintResult.errorCount > 0 || lintResult.warningCount > 0
if (hasIssues) {
if (lintResult.errorCount > 0) {
response += `❌ **${lintResult.errorCount} error(s)** found that must be fixed:\n`
for (const error of lintResult.errors) {
response += `- Line ${error.startLineNumber}: ${error.message}\n`
}
}
if (lintResult.warningCount > 0) {
response += `\n⚠️ **${lintResult.warningCount} warning(s)** found:\n`
for (const warning of lintResult.warnings) {
response += `- Line ${warning.startLineNumber}: ${warning.message}\n`
}
}
} else {
response = '✅ No lint issues found.'
}
return response
}