Files
windmill/frontend/src/lib/components/TemplateEditor.svelte
T
hugocasaandClaude Opus 5 f037c73d10 feat(frontend): group the agent form and edit saved agents as drafts (#10880)
* feat(frontend): group the AI agent step form and edit saved agents in a modal

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: edit a saved AI agent through its own resource draft

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: drop the agent fork-for-edit session now that edits live in a draft

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: edit ai_agent resources from the resources page with the agent editor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: send a standalone agent's brain from the module when testing a step

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: keep the agent draft faithful to the resource it deploys to

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: add the sqlx cache entry for the eval subject rename

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: share the module insert between the graph and the agent editor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): open evals inside the agent editor, actions in its header

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): add tools from the agent editor and lighten its test pane

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): open an ai_agent deep link in the agent editor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): drop the failed result badge on a step that never ran

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): head the agent editor's levels with a back control

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): drop connect and fill inputs from the agent editor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): lighten the agent editor's run panel

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): stop a nested agent tool's config reading as AI-filled

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): offer only AI or static on an agent tool's inputs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): give a saved agent's tool editor a static-only surface

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): open an agent tool in a drawer beside the agent

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(frontend): hide unset agent config in the run form

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(frontend): share the input forms' pickers and s3 lookup

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(frontend): drop a dead agent-editor export and fix two stale comments

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): reach an ai_agent's resource-level settings and copilot

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): open an ai_agent's resource view as JSON, not the generic form

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): address review findings on the agent editor's draft and streaming

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): close the agent editor on a version restore, as the resource editor does

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): stop the provider picker auto-writing a kind, and clear review nits

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(frontend): drop the fork-for-edit leftovers from the agent card

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): mount the agent editor in the dev flow editor and guard the deep-link race

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): deploy the agent config that was submitted, and refuse one no run could use

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(frontend): build the agent editor's rows from the design-system button

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): keep a draft-only agent's draft, and let a blank MCP summary deploy

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): guard read-only agents, incomplete MCP tools and duplicate editor mounts

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: read-only agent editor, linked-card refresh, atomic eval rename

* fix: eval rename needs the privileged pool, per-workspace write access

* fix(frontend): drop the agent editor target when its mount goes away

* refactor: drop the agent rename work from this PR, unban the bindable defaults

* fix(frontend): refuse a renaming deploy and drop the copilot from static-only fields

* fix(frontend): mirror the worker's streaming rule and scope agent writes to their target

* fix(frontend): read runtime streaming as off and reset the drawer's json view

* fix(frontend): read an unsettled output_type as non-streaming too

* fix(frontend): let the showing modal claim an agent opened from inside it

* fix(frontend): keep in-flight edits, tool replacements and every linked step in sync

* fix(frontend): keep attachments in the run form and bind the agent ref to its tools

* fix(frontend): preview the agent as authored and re-evaluate step args on run

* fix(frontend): scope agent-editor ownership to the flow's workspace

* fix(frontend): drop the tool drill-in where there is no graph to select on

* fix(frontend): require a provider kind and keep one resource editor open at a time

* fix(frontend): keep legacy nulls, static-only text literal, and the handover anchor

* test(worker): pin the agent streaming default

* fix(frontend): let an AI-fillable input be switched to static

* fix(frontend): report agent editor background failures instead of floating them

* fix(frontend): keep the version pane's path alive while the editor closes

* fix(frontend): clear the anchor-keep flag at the start of each drawer session

* fix(frontend): preview the agent without its synthetic path, refresh the baseline on external writes

* refactor(frontend): drop the unverifiable baseline refresh, state the synthetic-path rule

* fix(frontend): keep the synthetic path out of agent tool test runs too

* refactor(frontend): host the agent editor under the agent's own path

* fix(frontend): mark an agent editor's host explicitly instead of inferring it from the path

* fix(frontend): discard linked-agent responses from before a deploy

* fix(frontend): keep a flow mount from claiming an agent editor's nested target

* feat(frontend): keep an agent used as a tool inside the agent being edited

* fix(frontend): reserve the agent editor's root module id

* docs(frontend): record why the agent editor previews under the agent's path

* fix(frontend): refuse to open or deploy a resource that is not an agent

* docs(frontend): put the scope-migration comment on the function it describes

* fix(frontend): refuse an agent path whose resource type is not proven

* fix(frontend): recheck the resource type before deploying, and keep expressions off static-only inputs

* fix(frontend): lazy-load the agent editor and slide its levels like the evals pane

* refactor: drop unreachable non-list tools check from agent deploy

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): clear text-only agent fields on image output, reserve the root id

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): keep the agent editor usable for a non-list tools value

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): stop the parked eval run list from taking arrow keys

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): report a non-list tools value on deploy instead of throwing

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): keep temperature editable for image output

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): skip non-object tool entries when rendering an agent

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): guard tool entry reads instead of copying the tool array

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): key tool rows by position so duplicate ids render

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:32:17 +02:00

763 lines
18 KiB
Svelte

<script module>
import '@codingame/monaco-vscode-standalone-typescript-language-features'
import {
javascriptDefaults,
getJavaScriptWorker
} from '@codingame/monaco-vscode-standalone-typescript-language-features'
</script>
<script lang="ts">
import { BROWSER } from 'esm-env'
import {
convertKind,
createDocumentationString,
displayPartsToString,
editorConfig,
registerWebviewPaste,
updateOptions
} from '$lib/editorUtils'
import { editorFontSize } from '$lib/editorFontSize.svelte'
import { createHash } from '$lib/editorLangUtils'
import libStdContent from '$lib/es6.d.ts.txt?raw'
import { editor as meditor, Uri as mUri, languages, Range, KeyMod, KeyCode } from 'monaco-editor'
import { createEventDispatcher, getContext, onDestroy, onMount, tick, untrack } from 'svelte'
import type { AppViewerContext } from './apps/types'
import { writable } from 'svelte/store'
// import '@codingame/monaco-vscode-standalone-languages'
// import '@codingame/monaco-vscode-standalone-typescript-language-features'
import { initializeVscode, MONACO_Y_PADDING } from './vscode'
import EditorTheme from './EditorTheme.svelte'
import FakeMonacoPlaceHolder from './FakeMonacoPlaceHolder.svelte'
import { setMonacoJsonOptions } from './monacoLanguagesOptions'
import { inputBorderClass } from './text_input/TextInput.svelte'
import { twMerge } from 'tailwind-merge'
export const conf = {
wordPattern:
/(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
comments: {
lineComment: '//',
blockComment: ['/*', '*/'] as [string, string]
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')']
] as [string, string][],
onEnterRules: [],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"', notIn: ['string'] },
{ open: "'", close: "'", notIn: ['string', 'comment'] },
{ open: '`', close: '`', notIn: ['string', 'comment'] }
],
folding: {
markers: {
start: new RegExp('^\\s*//\\s*#?region\\b'),
end: new RegExp('^\\s*//\\s*#?endregion\\b')
}
}
}
export const language = {
// Set defaultToken to invalid to see what you do not tokenize yet
defaultToken: 'invalid',
tokenPostfix: '.ts',
keywords: [
// Should match the keys of textToKeywordObj in
// https://github.com/microsoft/TypeScript/blob/master/src/compiler/scanner.ts
'abstract',
'any',
'as',
'asserts',
'bigint',
'boolean',
'break',
'case',
'catch',
'class',
'continue',
'const',
'constructor',
'debugger',
'declare',
'default',
'delete',
'do',
'else',
'enum',
'export',
'extends',
'false',
'finally',
'for',
'from',
'function',
'get',
'if',
'implements',
'import',
'in',
'infer',
'instanceof',
'interface',
'is',
'keyof',
'let',
'module',
'namespace',
'never',
'new',
'null',
'number',
'object',
'out',
'package',
'private',
'protected',
'public',
'override',
'readonly',
'require',
'global',
'return',
'set',
'static',
'string',
'super',
'switch',
'symbol',
'this',
'throw',
'true',
'try',
'type',
'typeof',
'undefined',
'unique',
'unknown',
'var',
'void',
'while',
'with',
'yield',
'async',
'await',
'of'
],
operators: [
'<=',
'>=',
'==',
'!=',
'===',
'!==',
'=>',
'+',
'-',
'**',
'*',
'/',
'%',
'++',
'--',
'<<',
'</',
'>>',
'>>>',
'&',
'|',
'^',
'!',
'~',
'&&',
'||',
'??',
'?',
':',
'=',
'+=',
'-=',
'*=',
'**=',
'/=',
'%=',
'<<=',
'>>=',
'>>>=',
'&=',
'|=',
'^=',
'@'
],
// we include these common regular expressions
symbols: /[=><!~?:&|+\-*\/\^%]+/,
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
digits: /\d+(_+\d+)*/,
octaldigits: /[0-7]+(_+[0-7]+)*/,
binarydigits: /[0-1]+(_+[0-1]+)*/,
hexdigits: /[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/,
regexpctl: /[(){}\[\]\$\^|\-*+?\.]/,
regexpesc: /\\(?:[bBdDfnrstvwWn0\\\/]|@regexpctl|c[A-Z]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4})/,
// The main tokenizer for our languages
tokenizer: {
root: [
[/\$\{/, { token: 'delimiter.bracket', next: '@bracketCounting' }],
[/[^\\`$]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid']
],
common: [
// identifiers and keywords
[
/[a-z_$][\w$]*/,
{
cases: {
'@keywords': 'keyword',
'@default': 'identifier'
}
}
],
[/[A-Z][\w\$]*/, 'type.identifier'], // to show class names nicely
// [/[A-Z][\w\$]*/, 'identifier'],
// whitespace
{ include: '@whitespace' },
// regular expression: ensure it is terminated before beginning (otherwise it is an opeator)
[
/\/(?=([^\\\/]|\\.)+\/([dgimsuy]*)(\s*)(\.|;|,|\)|\]|\}|$))/,
{ token: 'regexp', bracket: '@open', next: '@regexp' }
],
// delimiters and operators
[/[()\[\]]/, '@brackets'],
[/[<>](?!@symbols)/, '@brackets'],
[/!(?=([^=]|$))/, 'delimiter'],
[
/@symbols/,
{
cases: {
'@operators': 'delimiter',
'@default': ''
}
}
],
// numbers
[/(@digits)[eE]([\-+]?(@digits))?/, 'number.float'],
[/(@digits)\.(@digits)([eE][\-+]?(@digits))?/, 'number.float'],
[/0[xX](@hexdigits)n?/, 'number.hex'],
[/0[oO]?(@octaldigits)n?/, 'number.octal'],
[/0[bB](@binarydigits)n?/, 'number.binary'],
[/(@digits)n?/, 'number'],
// delimiter: after number because of .\d floats
[/[;,.]/, 'delimiter'],
// strings
[/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string
[/'([^'\\]|\\.)*$/, 'string.invalid'], // non-teminated string
[/"/, 'string', '@string_double'],
[/'/, 'string', '@string_single'],
[/`/, 'string', '@string_backtick']
],
whitespace: [
[/[ \t\r\n]+/, ''],
[/\/\*\*(?!\/)/, 'comment.doc', '@jsdoc'],
[/\/\*/, 'comment', '@comment'],
[/\/\/.*$/, 'comment']
],
comment: [
[/[^\/*]+/, 'comment'],
[/\*\//, 'comment', '@pop'],
[/[\/*]/, 'comment']
],
jsdoc: [
[/[^\/*]+/, 'comment.doc'],
[/\*\//, 'comment.doc', '@pop'],
[/[\/*]/, 'comment.doc']
],
// We match regular expression quite precisely
regexp: [
[
/(\{)(\d+(?:,\d*)?)(\})/,
['regexp.escape.control', 'regexp.escape.control', 'regexp.escape.control']
],
[
/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/,
['regexp.escape.control', { token: 'regexp.escape.control', next: '@regexrange' }]
],
[/(\()(\?:|\?=|\?!)/, ['regexp.escape.control', 'regexp.escape.control']],
[/[()]/, 'regexp.escape.control'],
[/@regexpctl/, 'regexp.escape.control'],
[/[^\\\/]/, 'regexp'],
[/@regexpesc/, 'regexp.escape'],
[/\\\./, 'regexp.invalid'],
[
/(\/)([dgimsuy]*)/,
[{ token: 'regexp', bracket: '@close', next: '@pop' }, 'keyword.other']
]
],
regexrange: [
[/-/, 'regexp.escape.control'],
[/\^/, 'regexp.invalid'],
[/@regexpesc/, 'regexp.escape'],
[/[^\]]/, 'regexp'],
[
/\]/,
{
token: 'regexp.escape.control',
next: '@pop',
bracket: '@close'
}
]
],
string_double: [
[/[^\\"]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/"/, 'string', '@pop']
],
string_single: [
[/[^\\']+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/'/, 'string', '@pop']
],
string_backtick: [
[/\$\{/, { token: 'delimiter.bracket', next: '@bracketCounting' }],
[/[^\\`$]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/`/, 'string', '@pop']
],
bracketCounting: [
[/\{/, 'delimiter.bracket', '@bracketCounting'],
[/\}/, 'delimiter.bracket', '@pop'],
{ include: 'common' }
]
}
}
let divEl: HTMLDivElement | null = $state(null)
let editor: meditor.IStandaloneCodeEditor | undefined = $state(undefined)
let pasteCleanup: (() => void) | undefined = undefined
let model: meditor.ITextModel
const { componentControl, selectedComponent } = getContext<AppViewerContext>(
'AppViewerContext'
) || { componentControl: writable({}), selectedComponent: writable([]) }
if ($selectedComponent) {
$componentControl[$selectedComponent[0]] = {
...$componentControl[$selectedComponent[0]],
setCode: (value: string) => {
setCode(value)
}
}
}
interface Props {
code?: string
hash?: string
automaticLayout?: boolean
extraLib?: string
autoHeight?: boolean
fixedOverflowWidgets?: boolean
fontSize?: number
loadAsync?: boolean
class?: string | undefined
/** Height the editor opens at, in lines, for a field that expects more than a phrase. */
minRows?: number
/** Shown over the empty editor. Monaco has no placeholder of its own. */
placeholder?: string
}
let {
code = $bindable(),
hash = createHash(),
automaticLayout = true,
extraLib = '',
autoHeight = true,
fixedOverflowWidgets = true,
fontSize,
loadAsync = false,
class: clazz = '',
minRows = undefined,
placeholder = undefined
}: Props = $props()
let effectiveFontSize = $derived(fontSize ?? editorFontSize.regular)
let yPadding = MONACO_Y_PADDING
// Monaco derives an unset line height as 1.5x the font size, rounded. Reproducing it here lets
// the box stand at its final height before the editor exists, so opening a field does not jump.
let minHeightPx = $derived(
minRows ? minRows * Math.round(1.5 * effectiveFontSize) + yPadding * 2 : 0
)
/** Whether the live model is empty, for the placeholder. Tracked apart from `code`, which the
* editor only writes back on a debounce. */
let editorEmpty = $state(true)
if (typeof code != 'string') {
code = ''
}
const lang = 'template'
const dispatch = createEventDispatcher()
const uri = `file:///${untrack(() => hash)}.ts`
export function insertAtCursor(code: string): void {
if (editor) {
editor.trigger('keyboard', 'type', { text: code })
}
}
export function setCode(ncode: string): void {
if (code != ncode) {
code = ncode
}
editor?.setValue(ncode)
}
let valueAfterDispose: string | undefined = undefined
export function getCode(): string {
if (valueAfterDispose != undefined) {
return valueAfterDispose
}
return editor?.getValue() ?? ''
}
let cip
let extraModel
// let widgets: HTMLElement | undefined = document.getElementById('monaco-widgets-root') ?? undefined
let initialized = $state(false)
let jsLoader: number | undefined = undefined
let timeoutModel: number | undefined = undefined
async function loadMonaco() {
setMonacoJsonOptions()
await initializeVscode('templateEditor')
initialized = true
languages.register({ id: 'template' })
// Register a tokens provider for the language
languages.registerTokensProviderFactory('template', {
create: () => language as languages.IMonarchLanguage
})
languages.setLanguageConfiguration('template', conf)
model = meditor.createModel(code ?? '', lang, mUri.parse(uri))
model.updateOptions(updateOptions)
try {
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code ?? '', lang, automaticLayout, fixedOverflowWidgets, false),
model,
// overflowWidgetsDomNode: widgets,
// lineNumbers: 'on',
lineDecorationsWidth: 0,
lineNumbersMinChars: 2,
fontSize: effectiveFontSize,
suggestOnTriggerCharacters: true,
renderLineHighlight: 'none',
lineNumbers: 'off',
...(yPadding !== undefined ? { padding: { bottom: yPadding, top: yPadding } } : {})
})
} catch (e) {
console.error('Error loading monaco:', e)
return
}
// In VSCode webview (iframe), clipboard operations need special handling
// because the webview has restricted clipboard API access
if (window.parent !== window) {
editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyC, function () {
document.execCommand('copy')
})
editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyX, function () {
document.execCommand('cut')
})
// Paste is scoped to this editor's container instead of a global
// Ctrl+V keybinding, which would leak across editor instances.
pasteCleanup?.()
pasteCleanup = registerWebviewPaste(divEl, () => editor)
}
editor.onDidFocusEditorText(() => {
dispatch('focus')
editor?.addCommand(KeyMod.CtrlCmd | KeyCode.KeyS, function () {
updateCode()
// See Editor.svelte — re-broadcast the swallowed shortcut for
// page-level draft-flush handlers, after `tick()` so they see
// the value `updateCode()` just materialized.
void tick().then(() => window.dispatchEvent(new CustomEvent('wm-monaco-save-shortcut')))
})
editor?.addCommand(KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Digit7, function () {})
})
function updateCode() {
const ncode = getCode()
if (code == ncode) {
return
}
code = ncode
dispatch('change', { code: ncode })
}
editor.onDidChangeModelContent((event) => {
// Undebounced, unlike `code`: the placeholder sits over the editor, so waiting would leave
// it covering the first characters typed into an empty field.
editorEmpty = (editor?.getModel()?.getValue() ?? '') === ''
timeoutModel && clearTimeout(timeoutModel)
timeoutModel = setTimeout(() => {
updateCode()
}, 200)
})
extraModel = meditor.createModel('`' + model.getValue() + '`', 'javascript')
if (autoHeight) {
const updateHeight = () => {
const contentHeight = Math.min(1000, editor?.getContentHeight() ?? 0)
if (divEl) {
divEl.style.height = `${Math.max(contentHeight, minHeightPx)}px`
}
}
editor.onDidContentSizeChange(updateHeight)
updateHeight()
}
editor.onDidFocusEditorText(() => {
dispatch('focus')
isFocus = true
})
editor.onDidBlurEditorText(() => {
dispatch('blur')
isFocus = false
updateCode()
})
jsLoader = setTimeout(async () => {
jsLoader = undefined
try {
const worker = await getJavaScriptWorker()
const client = await worker(extraModel.uri)
cip = languages.registerCompletionItemProvider('template', {
triggerCharacters: ['.'],
provideCompletionItems: async (model, position) => {
extraModel.setValue('`' + model.getValue() + '`')
const offset = model.getOffsetAt(position) + 1
const info = await client.getCompletionsAtPosition(extraModel.uri.toString(), offset)
if (!info) {
return { suggestions: [] }
}
const wordInfo = model.getWordUntilPosition(position)
const wordRange = new Range(
position.lineNumber,
wordInfo.startColumn,
position.lineNumber,
wordInfo.endColumn
)
const suggestions = info.entries
.filter((x) => x.kind != 'keyword' && x.kind != 'var')
.map((entry) => {
let range = wordRange
if (entry.replacementSpan) {
const p1 = model.getPositionAt(entry.replacementSpan.start)
const p2 = model.getPositionAt(
entry.replacementSpan.start + entry.replacementSpan.length
)
range = new Range(p1.lineNumber, p1.column, p2.lineNumber, p2.column)
}
const tags: languages.CompletionItemTag[] = []
if (entry.kindModifiers?.indexOf('deprecated') !== -1) {
tags.push(languages.CompletionItemTag.Deprecated)
}
return {
uri: model.uri,
position: position,
offset: offset,
range: range,
label: entry.name,
insertText: entry.name,
sortText: entry.sortText,
kind: convertKind(entry.kind),
tags
}
})
return { suggestions }
},
resolveCompletionItem: async (item: languages.CompletionItem, token: any) => {
extraModel.setValue('`' + model.getValue() + '`')
const myItem = item as any
const position = myItem.position
const offset = myItem.offset
const details = await client.getCompletionEntryDetails(
extraModel.uri.toString(),
offset,
myItem.label
)
if (!details) {
return myItem
}
return {
uri: model.uri,
position: position,
label: details.name,
kind: convertKind(details.kind),
detail: displayPartsToString(details.displayParts),
documentation: {
value: createDocumentationString(details)
}
} as any
}
})
} catch (e) {
console.error('Error loading javascipt worker:', e)
}
}, 300)
}
export function focus() {
editor?.focus()
}
let isFocus = $state(false)
let mounted = $state(false)
let loadTimeout: number | undefined = undefined
onMount(async () => {
try {
if (BROWSER) {
if (loadAsync) {
loadTimeout = setTimeout(async () => {
await loadMonaco()
mounted = true
}, 0)
} else {
await loadMonaco()
mounted = true
}
}
} catch (e) {
console.error('Error loading monaco:', e)
}
})
function loadExtraLib() {
const stdLib = { content: libStdContent, filePath: 'es6.d.ts' }
const libs = [stdLib]
if (extraLib != '') {
libs.push({
content: extraLib,
filePath: 'windmill.d.ts'
})
}
javascriptDefaults.setExtraLibs(libs)
}
onDestroy(() => {
try {
valueAfterDispose = getCode()
pasteCleanup?.()
jsLoader && clearTimeout(jsLoader)
timeoutModel && clearTimeout(timeoutModel)
loadTimeout && clearTimeout(loadTimeout)
model && model.dispose()
editor && editor.dispose()
cip && cip.dispose()
extraModel && extraModel.dispose()
} catch (err) {}
})
$effect(() => {
mounted && extraLib && initialized && untrack(() => loadExtraLib())
})
$effect(() => {
const next = effectiveFontSize
if (editor) {
editor.updateOptions({ fontSize: next })
}
})
</script>
<EditorTheme />
<div
class={twMerge(
inputBorderClass({ forceFocus: isFocus }),
'relative rounded-md overflow-auto pl-2',
clazz
)}
style={minHeightPx ? `min-height: ${minHeightPx}px` : undefined}
>
{#if !editor}
<FakeMonacoPlaceHolder autoheight showNumbers={false} {code} fontSize={effectiveFontSize} />
{/if}
<div
bind:this={divEl}
style="height: {Math.max(18, minHeightPx)}px;"
class="template nonmain-editor rounded-md overflow-clip {!editor ? 'hidden' : ''}"
></div>
{#if placeholder && !code && (!editor || editorEmpty)}
<div
class="absolute inset-0 px-2 pointer-events-none whitespace-pre-wrap text-hint font-mono"
style="font-size: {effectiveFontSize}px; line-height: {Math.round(
1.5 * effectiveFontSize
)}px; padding-top: {yPadding}px;"
>
{placeholder}
</div>
{/if}
</div>
<style>
:global(.template .mtk20) {
color: black !important;
}
</style>