feat: add wac ai context for frontend chat (#9021)

* feat: add wac ai context

* fix: limit wac context languages

* fix: pass wac auto kind in flow script drawer
This commit is contained in:
centdix
2026-05-05 17:00:06 +02:00
committed by GitHub
parent 4ba46c0e1c
commit 0d0557fc9d
19 changed files with 203 additions and 41 deletions
+3 -3
View File
@@ -6165,12 +6165,12 @@ Use this guide when writing or modifying Windmill Workflow-as-Code (WAC) scripts
WAC is authored as a Windmill script and deployed with the normal script workflow. It is not an OpenFlow YAML flow.
Supported WAC authoring targets:
- TypeScript scripts that import from \`windmill-client\`
- Bun TypeScript scripts that import from \`windmill-client\`
- Python 3 scripts that import from \`wmill\`
## File Shape
TypeScript:
Bun TypeScript:
\`\`\`typescript
import {
@@ -6212,7 +6212,7 @@ async def main(x: str):
Rules:
- Do not call \`main\`.
- TypeScript should export the workflow entrypoint, preferably \`export const main = workflow(async (...) => { ... })\`.
- Bun TypeScript should export the workflow entrypoint, preferably \`export const main = workflow(async (...) => { ... })\`.
- Python must use \`@workflow\` on an async top-level function, usually \`main\`.
- Define task functions and \`taskScript\`/\`task_script\` or \`taskFlow\`/\`task_flow\` assignments at module top level with stable names.
- Use the exact SDK names. Do not alias \`workflow\`, \`task\`, \`taskScript\`, \`taskFlow\`, \`step\`, \`sleep\`, \`waitForApproval\`, \`task_script\`, \`task_flow\`, or \`wait_for_approval\`; the WAC parser recognizes these names directly.
+7 -3
View File
@@ -129,6 +129,7 @@
useWebsockets?: boolean
small?: boolean
scriptLang: Preview['language'] | 'bunnative' | 'tsx' | 'jsx' | 'json' | undefined
workflowAsCode?: boolean
disabled?: boolean
lineNumbersMinChars?: number
files?: Record<string, { code: string; readonly?: boolean }> | undefined
@@ -163,6 +164,7 @@
useWebsockets = true,
small = false,
scriptLang,
workflowAsCode = false,
disabled = false,
lineNumbersMinChars = 3,
files = {},
@@ -819,12 +821,13 @@
function addAutoCompletor(
editor: meditor.IStandaloneCodeEditor,
scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json',
workflowAsCode: boolean
) {
if (autocompletor) {
autocompletor.dispose()
}
autocompletor = new Autocompletor(editor, scriptLang)
autocompletor = new Autocompletor(editor, scriptLang, { workflowAsCode })
}
const outputChannel = {
@@ -1875,13 +1878,14 @@
;(!dbSchema || lang !== 'graphql') && untrack(() => disposeGaphqlService())
})
$effect(() => {
const currentWorkflowAsCode = workflowAsCode
$copilotInfo.enabled &&
$codeCompletionSessionEnabled &&
Autocompletor.isProviderModelSupported($copilotInfo.codeCompletionModel) &&
initialized &&
editor &&
scriptLang &&
untrack(() => editor && addAutoCompletor(editor, scriptLang))
untrack(() => editor && addAutoCompletor(editor, scriptLang, currentWorkflowAsCode))
})
$effect(() => {
$copilotInfo.enabled && initialized && editor && untrack(() => editor && addChatHandler(editor))
@@ -2117,6 +2117,7 @@
bind:code={script.content}
lang={script.language}
kind={script.kind}
autoKind={script.auto_kind}
{template}
tag={script.tag}
lastSavedCode={savedScript?.draft?.content}
@@ -116,6 +116,7 @@
path: string | undefined
lang: Preview['language']
kind?: string | undefined
autoKind?: string | undefined
template?:
| 'pgsql'
| 'mysql'
@@ -161,6 +162,7 @@
path,
lang,
kind = undefined,
autoKind = undefined,
template = 'script',
tag,
fixedOverflowWidgets = true,
@@ -1234,7 +1236,16 @@
)
}
let isWac = $derived(code && lang ? isWorkflowAsCode(code, lang) : false)
const WAC_CONTEXT_LANGUAGES = ['python3', 'bun']
let isWac = $derived(
template === 'wac_python' ||
template === 'wac_typescript' ||
autoKind === 'wac' ||
(code && lang ? isWorkflowAsCode(code, lang) : false)
)
let workflowAsCodeAiContext = $derived(
activeModuleTab === null && isWac && WAC_CONTEXT_LANGUAGES.includes(lang ?? '')
)
let showTabs = $derived(hasPreprocessor || isWac)
$effect(() => {
!hasPreprocessor && (selectedTab = 'main')
@@ -1304,7 +1315,8 @@
path,
lastSavedCode,
lastDeployedCode,
diffMode
diffMode,
workflowAsCode: workflowAsCodeAiContext
}
untrack(() => {
aiChatManager.scriptEditorOptions = options
@@ -2075,6 +2087,7 @@
automaticLayout={true}
{fixedOverflowWidgets}
{args}
workflowAsCode={workflowAsCodeAiContext}
{enablePreprocessorSnippet}
preparedAssetsSqlQueries={preparedSqlQueries.current}
customTag={tag}
@@ -66,6 +66,7 @@ export class Autocompletor {
max: 10
})
#scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
#workflowAsCode: boolean
#abortController: AbortController = new AbortController()
#completionDisposable: IDisposable
#cursorDisposable: IDisposable
@@ -76,7 +77,8 @@ export class Autocompletor {
constructor(
editor: meditor.IStandaloneCodeEditor,
scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json',
options: { workflowAsCode?: boolean } = {}
) {
setGlobalCSS(
'ai-chat-autocomplete',
@@ -87,6 +89,7 @@ export class Autocompletor {
`
)
this.#scriptLang = scriptLang
this.#workflowAsCode = options.workflowAsCode ?? false
const deletionsCues = editor.createDecorationsCollection()
@@ -520,7 +523,8 @@ export class Autocompletor {
suffix,
scriptLang: this.#scriptLang,
markers: markersAtCursor,
libraries: librariesCompletions
libraries: librariesCompletions,
workflowAsCode: this.#workflowAsCode
},
this.#abortController
)
@@ -20,6 +20,7 @@ export async function autocompleteRequest(
scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json'
markers: editor.IMarker[]
libraries: string
workflowAsCode?: boolean
},
abortController: AbortController
) {
@@ -38,7 +39,10 @@ export async function autocompleteRequest(
'You are a code completion assistant. You are given three important contexts (<LANGUAGE CONTEXT>, <DIAGNOSTICS>, <LIBRARY METHODS>) to help you complete the code.\n'
)
contextLines += comment(commentSymbol, 'LANGUAGE CONTEXT:\n')
contextLines += comment(commentSymbol, getLangContext(context.scriptLang) + '\n')
contextLines += comment(
commentSymbol,
getLangContext(context.scriptLang, { workflowAsCode: context.workflowAsCode }) + '\n'
)
contextLines += comment(commentSymbol, 'DIAGNOSTICS:\n')
contextLines += comment(commentSymbol, context.markers.map((m) => m.message).join('\n') + '\n')
contextLines += comment(commentSymbol, 'LIBRARY METHODS:\n')
@@ -256,6 +256,8 @@ class AIChatManager {
options?: {
closeScriptSettings?: boolean
lang?: ScriptLang | 'bunnative'
isPreprocessor?: boolean
workflowAsCode?: boolean
}
) {
if (mode === AIMode.SCRIPT && !tryGetCurrentModel()) return
@@ -265,8 +267,16 @@ class AIChatManager {
const currentModel = getCurrentModel()
const customPrompt = getCombinedCustomPrompt(mode)
const lang = options?.lang ?? this.scriptEditorOptions?.lang ?? 'bun'
const workflowAsCode =
options?.workflowAsCode ??
(options?.lang ? false : (this.scriptEditorOptions?.workflowAsCode ?? false))
const context = this.contextManager.getSelectedContext()
this.systemMessage = prepareScriptSystemMessage(currentModel, lang, {}, customPrompt)
this.systemMessage = prepareScriptSystemMessage(
currentModel,
lang,
{ isPreprocessor: options?.isPreprocessor, workflowAsCode },
customPrompt
)
this.systemMessage.content = this.systemMessage.content
this.tools = [...prepareScriptTools(currentModel, lang, context)]
this.helpers = {
@@ -533,7 +543,9 @@ class AIChatManager {
const systemMessage: ChatCompletionSystemMessageParam = {
role: 'system',
content: prepareInlineChatSystemPrompt(lang)
content: prepareInlineChatSystemPrompt(lang, {
workflowAsCode: this.scriptEditorOptions?.workflowAsCode ?? false
})
}
let reply = ''
@@ -606,9 +618,15 @@ class AIChatManager {
} = {}
) => {
if (options.mode) {
this.changeMode(options.mode, undefined, { lang: options.lang })
this.changeMode(options.mode, undefined, {
lang: options.lang,
isPreprocessor: options.isPreprocessor
})
} else {
this.changeMode(this.mode, undefined, { lang: options.lang })
this.changeMode(this.mode, undefined, {
lang: options.lang,
isPreprocessor: options.isPreprocessor
})
}
if (options.instructions) {
this.instructions = options.instructions
@@ -18,6 +18,7 @@ export interface ScriptOptions {
lastSavedCode?: string
lastDeployedCode?: string
diffMode: boolean
workflowAsCode?: boolean
}
export interface FlowOptions {
@@ -27,7 +27,7 @@ import { getModelContextWindow } from '../../lib'
import type { ReviewChangesOpts } from '../monaco-adapter'
import { getCurrentModel } from '$lib/aiStore'
import { getDbSchemas } from '$lib/components/apps/components/display/dbtable/metadata'
import { getScriptPrompt } from '$system_prompts'
import { getScriptPrompt, getWorkflowAsCodePrompt } from '$system_prompts'
// Score threshold for npm packages search filtering
const SCORE_THRESHOLD = 1000
@@ -104,11 +104,22 @@ export function getLangContext(
lang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json',
{
allowResourcesFetch = false,
isPreprocessor = false
}: { allowResourcesFetch?: boolean; isPreprocessor?: boolean; isFailure?: boolean } = {}
isPreprocessor = false,
workflowAsCode = false
}: {
allowResourcesFetch?: boolean
isPreprocessor?: boolean
isFailure?: boolean
workflowAsCode?: boolean
} = {}
): string {
// Get base language context from centralized prompts
let context = getScriptPrompt(lang)
let context = workflowAsCode ? getWorkflowAsCodePrompt(lang) : getScriptPrompt(lang)
// Fallback to the regular script prompt if WAC context is requested for an unsupported language.
if (!context) {
context = getScriptPrompt(lang)
}
// Add tool usage instructions for applicable languages
if (['python3', 'php', 'bun', 'deno', 'nativets', 'bunnative'].includes(lang)) {
@@ -117,8 +128,8 @@ export function getLangContext(
}
}
// Note preprocessor function naming if applicable
if (isPreprocessor) {
// Note preprocessor function naming if applicable. WAC scripts are not preprocessors.
if (isPreprocessor && !workflowAsCode) {
context +=
'\n\nThe main function for this script should be named `preprocessor` instead of `main`.'
}
@@ -277,8 +288,15 @@ export async function main() {
\`\`\`
`
export function prepareInlineChatSystemPrompt(lang: ScriptLang | 'bunnative') {
return INLINE_CHAT_SYSTEM_PROMPT + getLangContext(lang, { allowResourcesFetch: true })
export function prepareInlineChatSystemPrompt(
lang: ScriptLang | 'bunnative',
options: { workflowAsCode?: boolean } = {}
) {
return (
INLINE_CHAT_SYSTEM_PROMPT +
'\n\n' +
getLangContext(lang, { allowResourcesFetch: true, workflowAsCode: options.workflowAsCode })
)
}
export const CHAT_USER_PROMPT = `
@@ -290,7 +308,7 @@ INSTRUCTIONS:
export function prepareScriptSystemMessage(
currentModel: AIProviderModel,
language: ScriptLang | 'bunnative',
options: { isPreprocessor?: boolean; allowResourcesFetch?: boolean } = {},
options: { isPreprocessor?: boolean; allowResourcesFetch?: boolean; workflowAsCode?: boolean } = {},
customPrompt?: string
): ChatCompletionSystemMessageParam {
let content = buildChatSystemPrompt(currentModel)
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'
import { getScriptPrompt, getWorkflowAsCodePrompt } from '$system_prompts'
describe('Workflow-as-Code prompt helpers', () => {
it('injects only the TypeScript WAC SDK for TypeScript runtimes', () => {
const prompt = getWorkflowAsCodePrompt('bun')
expect(prompt).toContain('Windmill Workflow-as-Code Writing Guide')
expect(prompt).toContain('## TypeScript Workflow-as-Code API')
expect(prompt).not.toContain('## Python Workflow-as-Code API')
})
it('injects only the Python WAC SDK for Python runtimes', () => {
const prompt = getWorkflowAsCodePrompt('python3')
expect(prompt).toContain('Windmill Workflow-as-Code Writing Guide')
expect(prompt).toContain('## Python Workflow-as-Code API')
expect(prompt).not.toContain('## TypeScript Workflow-as-Code API')
})
it('does not support non-Bun TypeScript runtimes as WAC targets', () => {
expect(getWorkflowAsCodePrompt('deno')).toBe('')
expect(getWorkflowAsCodePrompt('nativets')).toBe('')
expect(getWorkflowAsCodePrompt('bunnative')).toBe('')
})
it('does not change normal script prompts', () => {
expect(getWorkflowAsCodePrompt('go')).toBe('')
expect(getScriptPrompt('bun')).not.toContain('Windmill Workflow-as-Code Writing Guide')
})
})
@@ -207,6 +207,7 @@
noSyncFromGithub
lang={script.language}
path={script.path}
autoKind={script.auto_kind}
tag={script.tag}
fixedOverflowWidgets={false}
bind:code={script.content}
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest'
import { isWorkflowAsCode } from './wacToFlow'
const tsWac = `
import { workflow, task } from "windmill-client"
const process = task(async () => "ok")
export const main = workflow(async () => await process())
`
describe('isWorkflowAsCode', () => {
it('detects WAC only for Bun TypeScript and Python', () => {
expect(isWorkflowAsCode(tsWac, 'bun')).toBe(true)
expect(isWorkflowAsCode(tsWac, 'deno')).toBe(false)
expect(isWorkflowAsCode(tsWac, 'nativets')).toBe(false)
expect(isWorkflowAsCode('@workflow\nasync def main():\n return None\n', 'python3')).toBe(true)
})
})
@@ -5,7 +5,7 @@ export function isWorkflowAsCode(code: string, language: string): boolean {
if (language === 'python3') {
return /^\s*@workflow\s*$/m.test(code) || /from\s+wmill\s+import.*workflow/.test(code)
}
if (language === 'bun' || language === 'deno') {
if (language === 'bun') {
return (
/workflow\s*\(/.test(code) &&
/task\s*\(/.test(code) &&
+1 -1
View File
@@ -2,4 +2,4 @@ export * from './prompts';
export declare function getScriptPrompt(language: string): string;
export declare function getFlowPrompt(): string;
export declare function getDatatableSdkReference(): string;
export declare function getWorkflowAsCodePrompt(): string;
export declare function getWorkflowAsCodePrompt(language?: string): string;
+23 -3
View File
@@ -10,6 +10,12 @@ const TS_SDK_LANGUAGES = ['bun', 'deno', 'nativets', 'bunnative'];
// Languages that use the Python SDK
const PY_SDK_LANGUAGES = ['python3'];
// Languages that use the TypeScript Workflow-as-Code SDK
const WAC_TS_SDK_LANGUAGES = ['bun'];
// Languages that use the Python Workflow-as-Code SDK
const WAC_PY_SDK_LANGUAGES = PY_SDK_LANGUAGES;
// Helper to combine prompts for scripts
export function getScriptPrompt(language: string): string {
const langKey = `LANG_${language.toUpperCase()}` as keyof typeof prompts;
@@ -47,10 +53,24 @@ export function getDatatableSdkReference(): string {
}
// Helper to combine prompts for Workflow-as-Code scripts
export function getWorkflowAsCodePrompt(): string {
export function getWorkflowAsCodePrompt(language?: string): string {
let sdkPrompt = '';
if (language == null) {
sdkPrompt = [
prompts.WAC_SDK_TYPESCRIPT,
prompts.WAC_SDK_PYTHON
].filter(Boolean).join('\n\n');
} else if (WAC_TS_SDK_LANGUAGES.includes(language)) {
sdkPrompt = prompts.WAC_SDK_TYPESCRIPT;
} else if (WAC_PY_SDK_LANGUAGES.includes(language)) {
sdkPrompt = prompts.WAC_SDK_PYTHON;
} else {
return '';
}
return [
prompts.WORKFLOW_AS_CODE_BASE,
prompts.WAC_SDK_TYPESCRIPT,
prompts.WAC_SDK_PYTHON
sdkPrompt
].filter(Boolean).join('\n\n');
}
+3 -3
View File
@@ -332,12 +332,12 @@ Use this guide when writing or modifying Windmill Workflow-as-Code (WAC) scripts
WAC is authored as a Windmill script and deployed with the normal script workflow. It is not an OpenFlow YAML flow.
Supported WAC authoring targets:
- TypeScript scripts that import from \`windmill-client\`
- Bun TypeScript scripts that import from \`windmill-client\`
- Python 3 scripts that import from \`wmill\`
## File Shape
TypeScript:
Bun TypeScript:
\`\`\`typescript
import {
@@ -379,7 +379,7 @@ async def main(x: str):
Rules:
- Do not call \`main\`.
- TypeScript should export the workflow entrypoint, preferably \`export const main = workflow(async (...) => { ... })\`.
- Bun TypeScript should export the workflow entrypoint, preferably \`export const main = workflow(async (...) => { ... })\`.
- Python must use \`@workflow\` on an async top-level function, usually \`main\`.
- Define task functions and \`taskScript\`/\`task_script\` or \`taskFlow\`/\`task_flow\` assignments at module top level with stable names.
- Use the exact SDK names. Do not alias \`workflow\`, \`task\`, \`taskScript\`, \`taskFlow\`, \`step\`, \`sleep\`, \`waitForApproval\`, \`task_script\`, \`task_flow\`, or \`wait_for_approval\`; the WAC parser recognizes these names directly.
@@ -48,12 +48,12 @@ Use this guide when writing or modifying Windmill Workflow-as-Code (WAC) scripts
WAC is authored as a Windmill script and deployed with the normal script workflow. It is not an OpenFlow YAML flow.
Supported WAC authoring targets:
- TypeScript scripts that import from `windmill-client`
- Bun TypeScript scripts that import from `windmill-client`
- Python 3 scripts that import from `wmill`
## File Shape
TypeScript:
Bun TypeScript:
```typescript
import {
@@ -95,7 +95,7 @@ async def main(x: str):
Rules:
- Do not call `main`.
- TypeScript should export the workflow entrypoint, preferably `export const main = workflow(async (...) => { ... })`.
- Bun TypeScript should export the workflow entrypoint, preferably `export const main = workflow(async (...) => { ... })`.
- Python must use `@workflow` on an async top-level function, usually `main`.
- Define task functions and `taskScript`/`task_script` or `taskFlow`/`task_flow` assignments at module top level with stable names.
- Use the exact SDK names. Do not alias `workflow`, `task`, `taskScript`, `taskFlow`, `step`, `sleep`, `waitForApproval`, `task_script`, `task_flow`, or `wait_for_approval`; the WAC parser recognizes these names directly.
+3 -3
View File
@@ -6,12 +6,12 @@ Use this guide when writing or modifying Windmill Workflow-as-Code (WAC) scripts
WAC is authored as a Windmill script and deployed with the normal script workflow. It is not an OpenFlow YAML flow.
Supported WAC authoring targets:
- TypeScript scripts that import from `windmill-client`
- Bun TypeScript scripts that import from `windmill-client`
- Python 3 scripts that import from `wmill`
## File Shape
TypeScript:
Bun TypeScript:
```typescript
import {
@@ -53,7 +53,7 @@ async def main(x: str):
Rules:
- Do not call `main`.
- TypeScript should export the workflow entrypoint, preferably `export const main = workflow(async (...) => { ... })`.
- Bun TypeScript should export the workflow entrypoint, preferably `export const main = workflow(async (...) => { ... })`.
- Python must use `@workflow` on an async top-level function, usually `main`.
- Define task functions and `taskScript`/`task_script` or `taskFlow`/`task_flow` assignments at module top level with stable names.
- Use the exact SDK names. Do not alias `workflow`, `task`, `taskScript`, `taskFlow`, `step`, `sleep`, `waitForApproval`, `task_script`, `task_flow`, or `wait_for_approval`; the WAC parser recognizes these names directly.
+31 -3
View File
@@ -1967,6 +1967,12 @@ const TS_SDK_LANGUAGES = ['bun', 'deno', 'nativets', 'bunnative'];
// Languages that use the Python SDK
const PY_SDK_LANGUAGES = ['python3'];
// Languages that use the TypeScript Workflow-as-Code SDK
const WAC_TS_SDK_LANGUAGES = ['bun'];
// Languages that use the Python Workflow-as-Code SDK
const WAC_PY_SDK_LANGUAGES = PY_SDK_LANGUAGES;
// Helper to combine prompts for scripts
export function getScriptPrompt(language: string): string {
const langKey = `LANG_${language.toUpperCase()}` as keyof typeof prompts;
@@ -2004,16 +2010,38 @@ export function getDatatableSdkReference(): string {
}
// Helper to combine prompts for Workflow-as-Code scripts
export function getWorkflowAsCodePrompt(): string {
export function getWorkflowAsCodePrompt(language?: string): string {
let sdkPrompt = '';
if (language == null) {
sdkPrompt = [
prompts.WAC_SDK_TYPESCRIPT,
prompts.WAC_SDK_PYTHON
].filter(Boolean).join('\\n\\n');
} else if (WAC_TS_SDK_LANGUAGES.includes(language)) {
sdkPrompt = prompts.WAC_SDK_TYPESCRIPT;
} else if (WAC_PY_SDK_LANGUAGES.includes(language)) {
sdkPrompt = prompts.WAC_SDK_PYTHON;
} else {
return '';
}
return [
prompts.WORKFLOW_AS_CODE_BASE,
prompts.WAC_SDK_TYPESCRIPT,
prompts.WAC_SDK_PYTHON
sdkPrompt
].filter(Boolean).join('\\n\\n');
}
"""
(OUTPUT_GENERATED_DIR / "index.ts").write_text(index_content)
index_dts_content = """export * from './prompts';
export declare function getScriptPrompt(language: string): string;
export declare function getFlowPrompt(): string;
export declare function getDatatableSdkReference(): string;
export declare function getWorkflowAsCodePrompt(language?: string): string;
"""
(OUTPUT_GENERATED_DIR / "index.d.ts").write_text(index_dts_content)
# Generate skill files for Claude Code
CLI_GUIDANCE_DIR.mkdir(parents=True, exist_ok=True)
skills = generate_skills(