mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 00:05:27 +00:00
* draft
* Phase 1: Remove deprecated granular flow AI tools
Simplify AI chat flow mode to use only YAML-based editing:
- Remove all commented-out granular tools (add_step, remove_step, set_code, etc.)
- Clean up FlowAIChatHelpers interface to only essential methods
- Update system prompts to focus on YAML-only workflow
- Remove unused imports and type definitions
This is part of a larger refactoring to simplify the flow editing
experience to a single YAML editing tool with automatic diff visualization.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* use minified json
* use openflow in system prompt
* handle inputs
* cleaning
* cleaning
* diffmode in flowgraph
* remove acceptrejectmodule
* use new diff mode
* cleaning
* better props
* better logic
* cleaning
* accept reject logic
* use get set
* draft manager
* use diff manager
* draft
* Refactor flowDiffManager to be instance-based with auto-computation
- Remove singleton export, making it instantiable per FlowGraphV2
- Add afterFlow state tracking for auto-diff computation
- Add beforeInputSchema/afterInputSchema for schema change tracking
- Add $effect for reactive auto-computation when beforeFlow/afterFlow changes
- Add setAfterFlow() and setInputSchemas() methods
- Simplify accept/reject methods to just mark pending=false
- Add validation to throw error when accepting/rejecting without beforeFlow
- Update setSnapshot to accept undefined for clearing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor FlowGraphV2 to own diffManager instance
- Import and create diffManager instance per FlowGraphV2
- Remove onAcceptModule and onRejectModule props
- Add validation $effect to error if both diffBeforeFlow and moduleActions provided
- Add $effect to sync props (diffBeforeFlow or moduleActions) to diffManager
- Add $effect to watch current flow changes and update afterFlow
- Replace computedDiff with diffManager.moduleActions
- Use raw modules instead of merged flow (diffManager handles merging)
- Expose getDiffManager() and setBeforeFlow() methods
- Pass diffManager to graph context instead of callbacks
- Remove $inspect for removed props
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update FlowModuleSchemaMap to use FlowGraphV2's diffManager
- Remove import of flowDiffManager singleton
- Update setBeforeFlow to call graph.setBeforeFlow()
- Update setModuleActions and getModuleActions to use graph.getDiffManager()
- Add getDiffManager() proxy method
- Simplify handleAcceptModule and handleRejectModule to use new API
- Handle editor state separately from diff operations
- Remove diffBeforeFlow, moduleActions, onAcceptModule, onRejectModule props passed to FlowGraphV2
- Remove onAcceptModule and onRejectModule from Props interface and destructured props
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update FlowAIChat to use flowModuleSchemaMap's diffManager
- Remove import of flowDiffManager singleton
- Update revertToSnapshot to use flowModuleSchemaMap.getDiffManager()
- Add null check for diffManager before using
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Verify FlowGraphDiffViewer compatibility with refactored architecture
FlowGraphDiffViewer already uses the correct prop patterns:
- Before graph: moduleActions prop (display-only mode)
- After graph: diffBeforeFlow prop (full diff mode with auto-computation)
Each FlowGraphV2 instance creates its own diffManager, making the side-by-side
view work correctly with independent diff state per graph.
No code changes required.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update graph components to use diffManager instead of callbacks
- Update graphBuilder.svelte.ts to pass diffManager instead of onAcceptModule/onRejectModule
- Update InputNode and ModuleN type definitions with diffManager
- Update ModuleNode.svelte to pass diffManager to MapItem
- Update MapItem.svelte to pass diffManager to FlowModuleSchemaItem
- Update FlowModuleSchemaItem.svelte to use diffManager directly for accept/reject
- Replace callback-based accept/reject with direct diffManager calls
- Only show accept/reject buttons when beforeFlow exists and action is pending
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix removed modules not showing in diff viewer
Problem: After refactoring, removed modules were no longer appearing in the
diff viewer because we changed effectiveModules from using the merged flow
(which includes removed modules) to using raw modules.
Solution:
- Add mergedFlow state to flowDiffManager to store timeline's merged flow
- Add markRemovedAsShadowed parameter support for side-by-side view
- Store timeline.mergedFlow in auto-computation $effect
- Add getter for mergedFlow and setMarkRemovedAsShadowed method
- Clear mergedFlow in clearSnapshot()
- Update FlowGraphV2 to set markRemovedAsShadowed in diffManager
- Update effectiveModules/FailureModule/PreprocessorModule to use mergedFlow
The merged flow contains all modules including removed ones, enabling:
- Unified view: Removed modules appear in red with "removed" badge
- Side-by-side view: Removed modules show as shadowed in After graph
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Simplify accept/reject logic by removing actions instead of toggling pending state
Previously, accepting or rejecting a module action would set pending to false but keep the action in the moduleActions map. This caused a bug where the $effect would overwrite moduleActions with fresh actions having pending: true, making accept/reject buttons reappear on previously handled modules.
Now, when a user accepts or rejects a module action, we remove it entirely from the moduleActions map. This is simpler and fixes the button reappearing issue.
Changes:
- acceptModule: Remove action from moduleActions instead of setting pending: false
- rejectModule: Remove action from moduleActions instead of setting pending: false
- checkAndClearSnapshot: Check if moduleActions is empty instead of checking pending states
- Fix typo: getModuleFromFrom → getModuleFromFlow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* cleaning
* fix logic
* make diff drawer part of manager
* accept submodules
* fixes
* Phase 4: Add checkAndApplyChanges() helper to flowDiffManager
- Added new checkAndApplyChanges() function to apply mergedFlow to flowStore when all changes are decided
- This replaces the old checkAndClearSnapshot() behavior and ensures flowStore is updated atomically
- Handles both flow structure and input schema updates
* Phase 2: Simplify acceptModule() - only modify mergedFlow
- Remove flowStore mutations from acceptModule()
- For removed modules: just delete the shadowed (__prefix) version from mergedFlow
- For added/modified: no action needed (already correct in mergedFlow)
- Call checkAndApplyChanges() to apply changes when all decided
* Phase 3: Simplify rejectModule() - only modify mergedFlow
- Remove all flowStore mutations from rejectModule()
- For added modules: delete from mergedFlow
- For removed modules: replace shadowed (__) module with original from beforeFlow
- For modified modules: restore old version in mergedFlow
- For Input schema: revert afterInputSchema
- Call checkAndApplyChanges() to apply changes when all decided
* Phase 5: Verify acceptAll/rejectAll work with new architecture
- acceptAll() and rejectAll() already pass options correctly to acceptModule/rejectModule
- They will automatically benefit from checkAndApplyChanges()
- No changes needed for this phase
* Phase 6: Remove FlowGraphV2 reactive effect that updates afterFlow
- Removed the (lines 252-266) that continuously updated afterFlow
- This effect created reactive loops when flowStore changed
- afterFlow should only be set once when AI generates changes via setFlowYaml()
- The initial sync effect (lines 226-250) is kept for prop-driven diff mode
* Phase 7: Update FlowAIChat setFlowYaml to use diffManager
- Changed setFlowYaml() to use diffManager.setAfterFlow() instead of modifying flowStore
- flowStore remains unchanged during AI review phase
- Changes are staged in mergedFlow for user review
- Only applied to flowStore when all changes are accepted/rejected
- Added error handling for missing diffManager
* Fix linter warnings
- Remove unused FlowTimeline type import
- Fix ChangeTracker initialization with proper type parameter
- Keep deleteModuleFromFlow and checkAndClearSnapshot for potential future use
* Update plan document with implementation status
- Mark all phases as complete
- Add commit references
- Update file checklist
- Add implementation summary at top of document
* Add comprehensive implementation summary document
- Detailed overview of architecture changes
- Before/after comparisons for each file
- Complete testing scenarios checklist
- Troubleshooting guide
- Migration notes and backwards compatibility info
* Show pending modules in editor panel
- Pass diffManager from FlowModuleSchemaMap to FlowEditorPanel
- Add effectiveModules derived value that uses mergedFlow when in diff mode
- Update module iteration to use effectiveModules instead of flowStore
- Allows users to view added/modified modules during AI review
- Fixes issue where clicking on pending modules showed nothing
* Add implementation summary for show pending modules feature
* fix
* shorter system prompt
* Fix Input schema diff mode issues
- Add Accept/Reject buttons to Input node (previously only showed Diff button)
- Pass diffManager to FlowInput component
- Add effectiveSchema derived value that uses afterInputSchema when in diff mode
- Add effectiveDisabled to prevent editing Input when reviewing AI changes
- Update FlowInputViewer to show pending schema changes
- Fixes issue where Input schema changes couldn't be accepted/rejected
- Fixes issue where pending Input schema wasn't visible in the panel
* Disable delete and move buttons when in pending mode
- Add effectiveDeletable derived value that checks diffManager.hasPendingChanges
- Replace all instances of deletable with effectiveDeletable in template
- Prevents delete/move operations when AI changes are being reviewed
- Delete and move buttons are hidden when there are pending changes
- Buttons reappear once all changes are accepted or rejected
- Prevents conflicting operations during review phase
* no move or delte when reviewing
* use context
* inline script reduction
* use json
* rollback to direct modif
* fix merge
* cleaning
* fix reject removed
* add set step code tool
* better prompt
* add back relevant tools
* add back accept reject
* use edit mode for pending
* fix input
* remove unneeded effect
* cleaner + bug fix
* fix failure and preprocessor
* fix show diff for failure module
* fix accept reject on failre module
* no auto add module to context
* cleaning
* add back effect
* cleaning
* fix multiple setflowjson
* track effectivemoduleactions for graph rendering
* nit prompt
* styling
* rm md files
* rm flake copy
* cleaning
* fix z index
* fix revert
* only change before after
* use add remove modify tools
* input + failure + preproc tools
* parsing issues
* nit
* use raw schema for tools
* resolve ref for gemini
* fix schema
* show test on graph
* much cleaner logic
* ignore empty assets
* Remove debug console.log statements from production code
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Remove debug $inspect calls from FlowGraphV2
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add error logging to setFlowJson before re-throwing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Standardize null/undefined handling to prefer null
- Use .nullable().optional() instead of .nullish() in Zod schemas
- Simplify addModuleToFlow signature to use string | null
- Coerce undefined to null when extracting parsed args
- Simplify null checks to only check !== null
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Remove debug console.log from AI tool functions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Extract special module IDs to constants
Add SPECIAL_MODULE_IDS constant with INPUT, PREPROCESSOR, and FAILURE
to avoid magic strings throughout the flow AI chat code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Add cleanup for diffDrawer reference on unmount
Prevents potential memory leaks by clearing the diffDrawer reference
when the FlowGraphV2 component is destroyed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Use structuredClone instead of JSON.parse(JSON.stringify())
structuredClone is more efficient and type-safe for deep cloning objects.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Cache module lookups in reconstructMergedFlow
Move getAllModulesMap and getAllModuleIds calls outside the loop to avoid
redundant recomputation. Track merged IDs incrementally as modules are added.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Revert "Use structuredClone instead of JSON.parse(JSON.stringify())"
This reverts commit a62ba5b980.
* cleaning
* allow delete
* better openflow for ai agents + truncate system prompt
* handle ai agent tools
* fix set code for tool
* fix wrong cancel request called
* mark tool calls as canceled
* get lang instructions
* use streamiing args
* give db url to claude
* fix revert
* save and clear when leaving editor
* keep whitespace in user message
* uniformize colors
* fix diff button
* remove db from backend claude
* remove move module tool
* no failure and preprocessor
* fix error given to llm
* fix z index
* fix ts errors
* cleaning
* fix add module logic
* fix(copilot): add 'tools' to branchPath description for aiagent containers
The branchPath parameter description was missing 'tools' option for aiagent
containers and didn't mention branchall support.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(copilot): correct AI agent tool IDs and summaries documentation
Tool summaries CAN contain spaces (they're human-readable descriptions).
Only tool IDs must avoid spaces.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(copilot): remove reference to non-existent set_flow_json tool
The set_module_code tool description referenced set_flow_json which
doesn't exist as an exposed tool (it's an internal helper).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(copilot): clarify inspect_inline_script is read-only
The tool description incorrectly suggested it could modify code.
This tool only inspects - use set_module_code to modify.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(copilot): clarify afterId behavior for AI agent tools
Updated wording to clarify that afterId can be used but is optional
for AI agent tools since tool order doesn't affect execution.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(copilot): remove unused id param from get_instructions_for_code_generation
The id parameter was only used to check for preprocessor, which is no
longer needed. Simplified the tool to only require the language param.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs(copilot): add result format to search_scripts tool description
Helps AI understand what data format to expect from the tool.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* docs(copilot): add result format to resource_type tool description
Helps AI understand what data format to expect from the tool and
provides example resource type names.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* nit
* Add support for adding branches to branchall/branchone via add_module
Previously, add_module could only add modules inside existing branches.
Now, using insideId with branchPath=null will add a NEW branch to a
branchall or branchone container.
API:
- add_module({ insideId: "my_branchall", branchPath: null, value: { summary: "New Branch", skip_failure: false, modules: [] } })
- add_module({ insideId: "my_branchone", branchPath: null, value: { summary: "Condition", expr: "...", modules: [] } })
Changes:
- Extended addModuleToFlow to handle branchPath=null case
- Updated validation to allow branchPath=null when adding branches
- Updated tool descriptions and system prompt documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* nit
* add remove branch tool
* check all ids for duplicates
* no dup
* nit
* cleaning
* fix dup ids
* split core.ts
* only mount diff drawer if useful
* remove wrong logic
* update exprs
* fix
* chore(flow): Add unit tests to flow diff manager (#7291)
* setup
* add basic tests for flowdiff
* add complex tests
* fix branch issue
* more complex tests
* add flow diff manager tests
* add utils
* better handling of moved case
* more tests for move case
* add buggy test case
* rm
* rework config
* cleaning
* fix config
* rm
* fix for reverting type change module
* all good
* rm
* add missing testmode
---------
Co-authored-by: Claude <noreply@anthropic.com>
949 lines
26 KiB
Svelte
949 lines
26 KiB
Svelte
<script lang="ts">
|
|
import { BROWSER } from 'esm-env'
|
|
|
|
import type { Schema, SupportedLanguage } from '$lib/common'
|
|
import { type CompletedJob, type Job, JobService, type Preview, type ScriptLang } from '$lib/gen'
|
|
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
|
import { copyToClipboard, emptySchema, sendUserToast } from '$lib/utils'
|
|
import Editor from './Editor.svelte'
|
|
import { inferArgs, inferAssets, inferAnsibleExecutionMode } from '$lib/infer'
|
|
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
|
import SchemaForm from './SchemaForm.svelte'
|
|
import LogPanel from './scriptEditor/LogPanel.svelte'
|
|
import EditorBar, { EDITOR_BAR_WIDTH_THRESHOLD } from './EditorBar.svelte'
|
|
import JobLoader from './JobLoader.svelte'
|
|
import JobProgressBar from '$lib/components/jobs/JobProgressBar.svelte'
|
|
import { createEventDispatcher, onDestroy, onMount, untrack } from 'svelte'
|
|
import { Button } from './common'
|
|
import SplitPanesWrapper from './splitPanes/SplitPanesWrapper.svelte'
|
|
import WindmillIcon from './icons/WindmillIcon.svelte'
|
|
import * as Y from 'yjs'
|
|
import { scriptLangToEditorLang } from '$lib/scripts'
|
|
import { WebsocketProvider } from 'y-websocket'
|
|
import Modal from './common/modal/Modal.svelte'
|
|
import DiffEditor from './DiffEditor.svelte'
|
|
import {
|
|
Copy,
|
|
CornerDownLeft,
|
|
ExternalLink,
|
|
Github,
|
|
GitBranch,
|
|
Play,
|
|
PlayIcon,
|
|
WandSparkles
|
|
} from 'lucide-svelte'
|
|
import { setLicense } from '$lib/enterpriseUtils'
|
|
import type { ScriptEditorWhitelabelCustomUi } from './custom_ui'
|
|
import Tabs from './common/tabs/Tabs.svelte'
|
|
import Tab from './common/tabs/Tab.svelte'
|
|
import { slide } from 'svelte/transition'
|
|
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
|
|
import CaptureButton from './triggers/CaptureButton.svelte'
|
|
import { setContext } from 'svelte'
|
|
import HideButton from './apps/editor/settingsPanel/HideButton.svelte'
|
|
import { base } from '$lib/base'
|
|
import { SUPPORTED_CHAT_SCRIPT_LANGUAGES } from './copilot/chat/script/core'
|
|
import { getStringError } from './copilot/chat/utils'
|
|
import type { ScriptOptions } from './copilot/chat/ContextManager.svelte'
|
|
import { aiChatManager, AIMode } from './copilot/chat/AIChatManager.svelte'
|
|
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
|
|
import AssetsDropdownButton from './assets/AssetsDropdownButton.svelte'
|
|
import { canHavePreprocessor } from '$lib/script_helpers'
|
|
import { assetEq, type AssetWithAltAccessType } from './assets/lib'
|
|
import { editor as meditor } from 'monaco-editor'
|
|
import type { ReviewChangesOpts } from './copilot/chat/monaco-adapter'
|
|
import GitRepoViewer from './GitRepoViewer.svelte'
|
|
import GitRepoResourcePicker from './GitRepoResourcePicker.svelte'
|
|
import { updateDelegateToGitRepoConfig, insertAdditionalInventories } from '$lib/ansibleUtils'
|
|
import { copilotInfo } from '$lib/aiStore'
|
|
import JsonInputs from '$lib/components/JsonInputs.svelte'
|
|
import Toggle from './Toggle.svelte'
|
|
|
|
interface Props {
|
|
// Exported
|
|
schema?: Schema | any
|
|
code: string
|
|
path: string | undefined
|
|
lang: Preview['language']
|
|
kind?: string | undefined
|
|
template?: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative'
|
|
tag: string | undefined
|
|
initialArgs?: Record<string, any>
|
|
fixedOverflowWidgets?: boolean
|
|
noSyncFromGithub?: boolean
|
|
editor?: Editor | undefined
|
|
diffEditor?: DiffEditor | undefined
|
|
collabMode?: boolean
|
|
edit?: boolean
|
|
noHistory?: boolean
|
|
saveToWorkspace?: boolean
|
|
watchChanges?: boolean
|
|
customUi?: ScriptEditorWhitelabelCustomUi | undefined
|
|
args: Record<string, any>
|
|
selectedTab?: 'main' | 'preprocessor'
|
|
hasPreprocessor?: boolean
|
|
captureTable?: CaptureTable | undefined
|
|
showCaptures?: boolean
|
|
stablePathForCaptures?: string
|
|
lastSavedCode?: string | undefined
|
|
lastDeployedCode?: string | undefined
|
|
disableAi?: boolean
|
|
assets?: AssetWithAltAccessType[]
|
|
editor_bar_right?: import('svelte').Snippet
|
|
enablePreprocessorSnippet?: boolean
|
|
}
|
|
|
|
let {
|
|
schema = $bindable(),
|
|
code = $bindable(),
|
|
path,
|
|
lang,
|
|
kind = undefined,
|
|
template = 'script',
|
|
tag,
|
|
fixedOverflowWidgets = true,
|
|
noSyncFromGithub = false,
|
|
editor = $bindable(undefined),
|
|
diffEditor = $bindable(undefined),
|
|
collabMode = false,
|
|
edit = true,
|
|
noHistory = false,
|
|
saveToWorkspace = false,
|
|
watchChanges = false,
|
|
customUi = undefined,
|
|
args = $bindable(),
|
|
selectedTab = $bindable('main'),
|
|
hasPreprocessor = $bindable(false),
|
|
captureTable = $bindable(undefined),
|
|
showCaptures = true,
|
|
stablePathForCaptures = '',
|
|
lastSavedCode = undefined,
|
|
lastDeployedCode = undefined,
|
|
disableAi = false,
|
|
assets = $bindable(),
|
|
editor_bar_right,
|
|
enablePreprocessorSnippet = false
|
|
}: Props = $props()
|
|
|
|
let initialArgs = structuredClone($state.snapshot(args))
|
|
let jsonView = $state(false)
|
|
let schemaHeight = $state(0)
|
|
|
|
$effect.pre(() => {
|
|
if (schema == undefined) {
|
|
schema = emptySchema()
|
|
}
|
|
})
|
|
let showHistoryDrawer = $state(false)
|
|
|
|
let jobProgressBar: JobProgressBar | undefined = $state(undefined)
|
|
let diffMode = $state(false)
|
|
|
|
let websocketAlive = $state({
|
|
pyright: false,
|
|
deno: false,
|
|
go: false,
|
|
ruff: false,
|
|
shellcheck: false
|
|
})
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
$effect(() => {
|
|
watchChanges &&
|
|
(code != undefined || schema != undefined) &&
|
|
dispatch('change', { code, schema })
|
|
})
|
|
|
|
$effect(() => {
|
|
;[lang, code]
|
|
untrack(() => {
|
|
inferAssets(lang, code).then((newAssets: AssetWithAltAccessType[]) => {
|
|
for (const asset of newAssets) {
|
|
const old = assets?.find((a) => assetEq(a, asset))
|
|
if (old?.alt_access_type) asset.alt_access_type = old.alt_access_type
|
|
}
|
|
assets = newAssets
|
|
})
|
|
|
|
if (lang === 'ansible') {
|
|
inferAnsibleExecutionMode(code).then((v) => {
|
|
if (
|
|
v !== undefined &&
|
|
(v.delegate_to_git_repo_details === null ||
|
|
v.delegate_to_git_repo_details.resource !==
|
|
ansibleAlternativeExecutionMode?.resource ||
|
|
v.delegate_to_git_repo_details.playbook !==
|
|
ansibleAlternativeExecutionMode?.playbook ||
|
|
v.delegate_to_git_repo_details.inventories_location !==
|
|
ansibleAlternativeExecutionMode?.inventories_location ||
|
|
v.delegate_to_git_repo_details.commit !== ansibleAlternativeExecutionMode?.commit ||
|
|
v.git_ssh_identity !== ansibleGitSshIdentity)
|
|
) {
|
|
ansibleAlternativeExecutionMode = v.delegate_to_git_repo_details
|
|
ansibleGitSshIdentity = v.git_ssh_identity
|
|
}
|
|
})
|
|
}
|
|
})
|
|
})
|
|
|
|
let width = $state(1200)
|
|
|
|
let jobLoader: JobLoader | undefined = $state(undefined)
|
|
|
|
let isValid: boolean = $state(true)
|
|
let scriptProgress = $state(undefined)
|
|
|
|
let logPanel: LogPanel | undefined = $state(undefined)
|
|
// Test
|
|
let testIsLoading = $state(false)
|
|
let testJob: Job | undefined = $state()
|
|
let pastPreviews: CompletedJob[] = $state([])
|
|
let validCode = $state(true)
|
|
|
|
let wsProvider: WebsocketProvider | undefined = $state(undefined)
|
|
let yContent: Y.Text | undefined = $state(undefined)
|
|
let peers: { name: string }[] = $state([])
|
|
let showCollabPopup = $state(false)
|
|
|
|
let ansibleAlternativeExecutionMode = $state<
|
|
| { resource?: string; commit?: string; inventories_location?: string; playbook?: string }
|
|
| null
|
|
| undefined
|
|
>()
|
|
let ansibleGitSshIdentity = $state<string[]>([])
|
|
|
|
const url = new URL(window.location.toString())
|
|
let initialCollab = /true|1/i.test(url.searchParams.get('collab') ?? '0')
|
|
|
|
if (initialCollab) {
|
|
setCollaborationMode()
|
|
url.searchParams.delete('collab')
|
|
url.searchParams.delete('path')
|
|
history.replaceState(null, '', url)
|
|
}
|
|
|
|
function onKeyDown(event: KeyboardEvent) {
|
|
if ((event.ctrlKey || event.metaKey) && event.key == 'Enter') {
|
|
event.preventDefault()
|
|
runTest()
|
|
} else if ((event.ctrlKey || event.metaKey) && event.key == 'u') {
|
|
event.preventDefault()
|
|
toggleTestPanel()
|
|
}
|
|
}
|
|
|
|
export function setArgs(nargs: Record<string, any>) {
|
|
args = nargs
|
|
}
|
|
|
|
export async function runTest() {
|
|
// Not defined if JobProgressBar not loaded
|
|
jobProgressBar?.reset()
|
|
//@ts-ignore
|
|
let job = await jobLoader.runPreview(
|
|
path,
|
|
code,
|
|
lang,
|
|
selectedTab === 'preprocessor' || kind === 'preprocessor'
|
|
? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...(args ?? {}) }
|
|
: (args ?? {}),
|
|
tag,
|
|
undefined,
|
|
undefined,
|
|
{
|
|
done(_x) {
|
|
loadPastTests()
|
|
},
|
|
doneError({ error }) {
|
|
console.error(error)
|
|
// sendUserToast('Error running test', true)
|
|
}
|
|
}
|
|
)
|
|
logPanel?.setFocusToLogs()
|
|
return job
|
|
}
|
|
|
|
async function loadPastTests(): Promise<void> {
|
|
pastPreviews = await JobService.listCompletedJobs({
|
|
workspace: $workspaceStore!,
|
|
jobKinds: 'preview',
|
|
createdBy: $userStore?.username,
|
|
scriptPathExact: path
|
|
})
|
|
}
|
|
|
|
export async function inferSchema(
|
|
code: string,
|
|
{
|
|
nlang,
|
|
resetArgs = false,
|
|
applyInitialArgs = false
|
|
}: {
|
|
nlang?: SupportedLanguage
|
|
resetArgs?: boolean
|
|
applyInitialArgs?: boolean
|
|
} = {}
|
|
) {
|
|
let nschema = schema ?? emptySchema()
|
|
|
|
try {
|
|
const result = await inferArgs(
|
|
nlang ?? lang,
|
|
code,
|
|
nschema,
|
|
selectedTab === 'preprocessor' || kind === 'preprocessor' ? 'preprocessor' : undefined
|
|
)
|
|
|
|
if (kind === 'preprocessor') {
|
|
hasPreprocessor = false
|
|
selectedTab = 'main'
|
|
} else {
|
|
hasPreprocessor =
|
|
(selectedTab === 'preprocessor' ? !result?.no_main_func : result?.has_preprocessor) ??
|
|
false
|
|
|
|
if (!hasPreprocessor && selectedTab === 'preprocessor') {
|
|
selectedTab = 'main'
|
|
}
|
|
}
|
|
|
|
validCode = true
|
|
if (resetArgs) {
|
|
args = {}
|
|
}
|
|
if (applyInitialArgs) {
|
|
// we reapply initial args as the schema form might have cleared them between mount and the schema inference
|
|
args = initialArgs
|
|
}
|
|
schema = nschema
|
|
} catch (e) {
|
|
validCode = false
|
|
}
|
|
}
|
|
|
|
let gitRepoResourcePickerOpen = $state(false)
|
|
let commitHashForGitRepo = $derived(ansibleAlternativeExecutionMode?.commit)
|
|
|
|
// Check if delegate_to_git_repo exists in the code
|
|
let hasDelegateToGitRepo = $derived(code && code.includes('delegate_to_git_repo:'))
|
|
|
|
function handleDelegateConfigUpdate(event: {
|
|
detail: { resourcePath: string; playbook?: string; inventoriesLocation?: string }
|
|
}) {
|
|
if (!editor) return
|
|
|
|
const currentCode = editor.getCode()
|
|
const newCode = updateDelegateToGitRepoConfig(currentCode, {
|
|
resource: event.detail.resourcePath,
|
|
playbook: event.detail.playbook,
|
|
inventories_location: event.detail.inventoriesLocation
|
|
})
|
|
editor.setCode(newCode)
|
|
|
|
// Trigger schema inference to update assets
|
|
inferSchema(newCode)
|
|
}
|
|
|
|
function handleAddInventories(event: { detail: { inventoryPaths: string[] } }) {
|
|
if (!editor) return
|
|
|
|
const currentCode = editor.getCode()
|
|
const newCode = insertAdditionalInventories(currentCode, event.detail.inventoryPaths)
|
|
editor.setCode(newCode)
|
|
|
|
// Trigger schema inference to update assets
|
|
inferSchema(newCode)
|
|
}
|
|
|
|
onMount(() => {
|
|
inferSchema(code, { applyInitialArgs: true })
|
|
loadPastTests()
|
|
aiChatManager.saveAndClear()
|
|
aiChatManager.changeMode(AIMode.SCRIPT)
|
|
})
|
|
|
|
setLicense()
|
|
export async function setCollaborationMode() {
|
|
await setLicense()
|
|
if (!$enterpriseLicense) {
|
|
sendUserToast(`Multiplayer is an enterprise feature`, true, [
|
|
{
|
|
label: 'Upgrade',
|
|
callback: () => {
|
|
window.open('https://www.windmill.dev/pricing', '_blank')
|
|
}
|
|
}
|
|
])
|
|
return
|
|
}
|
|
|
|
const ydoc = new Y.Doc()
|
|
if (wsProvider) {
|
|
wsProvider.destroy()
|
|
}
|
|
let yContentInit = ydoc.getText('content')
|
|
|
|
const wsProtocol = BROWSER && window.location.protocol == 'https:' ? 'wss' : 'ws'
|
|
|
|
wsProvider = new WebsocketProvider(
|
|
`${wsProtocol}://${window.location.host}/ws_mp/`,
|
|
$workspaceStore + '/' + (path ?? 'no-room-name'),
|
|
ydoc,
|
|
{ connect: false }
|
|
)
|
|
|
|
wsProvider.on('sync', (isSynced: boolean) => {
|
|
if (isSynced && yContentInit?.toJSON() == '') {
|
|
showCollabPopup = true
|
|
yContentInit?.insert(0, code)
|
|
}
|
|
yContent = yContentInit
|
|
})
|
|
|
|
wsProvider.on('connection-error', (WSErrorEvent) => {
|
|
console.error(WSErrorEvent)
|
|
sendUserToast('Multiplayer server connection had an error', true)
|
|
})
|
|
wsProvider.connect()
|
|
const awareness = wsProvider.awareness
|
|
|
|
awareness.setLocalStateField('user', {
|
|
name: $userStore?.username
|
|
})
|
|
|
|
function setPeers() {
|
|
peers = Array.from(awareness.getStates().values()).map((x) => x?.['user'])
|
|
}
|
|
|
|
setPeers()
|
|
// You can observe when a user updates their awareness information
|
|
awareness.on('change', (changes) => {
|
|
setPeers()
|
|
})
|
|
}
|
|
|
|
export function disableCollaboration() {
|
|
if (!wsProvider?.shouldConnect) return
|
|
peers = []
|
|
console.log('collab mode disabled')
|
|
wsProvider?.disconnect()
|
|
wsProvider.destroy()
|
|
wsProvider = undefined
|
|
}
|
|
|
|
onDestroy(() => {
|
|
disableCollaboration()
|
|
aiChatManager.scriptEditorApplyCode = undefined
|
|
aiChatManager.scriptEditorShowDiffMode = undefined
|
|
aiChatManager.scriptEditorOptions = undefined
|
|
aiChatManager.saveAndClear()
|
|
aiChatManager.changeMode(AIMode.NAVIGATOR)
|
|
})
|
|
|
|
function asKind(str: string | undefined) {
|
|
return str as 'script' | 'approval' | 'trigger' | undefined
|
|
}
|
|
|
|
function collabUrl() {
|
|
let url = new URL(window.location.toString().split('#')[0])
|
|
url.search = ''
|
|
return `${url}?collab=1` + (edit ? '' : `&path=${path}`)
|
|
}
|
|
|
|
let showTabs = $derived(hasPreprocessor)
|
|
$effect(() => {
|
|
!hasPreprocessor && (selectedTab = 'main')
|
|
})
|
|
$effect(() => {
|
|
selectedTab && code && untrack(() => inferSchema(code))
|
|
})
|
|
|
|
let argsRender = $state(0)
|
|
export async function updateArgs(newArgs: Record<string, any>) {
|
|
if (Object.keys(newArgs).length > 0) {
|
|
args = { ...newArgs }
|
|
argsRender++
|
|
}
|
|
}
|
|
|
|
setContext('disableTooltips', customUi?.disableTooltips === true)
|
|
|
|
let codePanelSize = $state(70)
|
|
let testPanelSize = $state(30)
|
|
let storedTestPanelSize = untrack(() => testPanelSize)
|
|
|
|
function toggleTestPanel() {
|
|
if (testPanelSize > 0) {
|
|
storedTestPanelSize = testPanelSize
|
|
codePanelSize += testPanelSize
|
|
testPanelSize = 0
|
|
} else {
|
|
codePanelSize -= storedTestPanelSize
|
|
testPanelSize = storedTestPanelSize
|
|
}
|
|
}
|
|
|
|
function getError(job: Job | undefined) {
|
|
if (job != undefined && job.type === 'CompletedJob' && !job.success) {
|
|
return getStringError(job.result)
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function showDiffMode() {
|
|
const model = editor?.getModel()
|
|
if (model == undefined) return
|
|
diffMode = true
|
|
diffEditor?.showWithModelAndOriginal(lastDeployedCode ?? '', model)
|
|
editor?.hide()
|
|
}
|
|
|
|
function hideDiffMode() {
|
|
diffMode = false
|
|
diffEditor?.hide()
|
|
editor?.show()
|
|
}
|
|
|
|
let error = $derived(getError(testJob))
|
|
|
|
$effect(() => {
|
|
const options: ScriptOptions = {
|
|
code,
|
|
lang: lang as ScriptLang,
|
|
error,
|
|
args: args ?? {},
|
|
path,
|
|
lastSavedCode,
|
|
lastDeployedCode,
|
|
diffMode
|
|
}
|
|
untrack(() => {
|
|
aiChatManager.scriptEditorOptions = options
|
|
aiChatManager.scriptEditorApplyCode = async (code: string, opts?: ReviewChangesOpts) => {
|
|
hideDiffMode()
|
|
await editor?.reviewAndApplyCode(code, opts)
|
|
}
|
|
aiChatManager.scriptEditorShowDiffMode = showDiffMode
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<JobLoader
|
|
noCode={true}
|
|
bind:scriptProgress
|
|
bind:this={jobLoader}
|
|
bind:isLoading={testIsLoading}
|
|
bind:job={testJob}
|
|
/>
|
|
|
|
<svelte:window onkeydown={onKeyDown} />
|
|
|
|
<!-- Standalone triggerable registration for the script editor -->
|
|
<div
|
|
style="display: none"
|
|
use:triggerableByAI={{
|
|
id: 'script-editor',
|
|
description: 'Component to edit a script'
|
|
}}
|
|
></div>
|
|
|
|
<Modal title="Invite others" bind:open={showCollabPopup}>
|
|
<div>Have others join by sharing the following url:</div>
|
|
<div class="flex gap-2 pr-4">
|
|
<input type="text" disabled value={collabUrl()} />
|
|
|
|
<Button
|
|
color="light"
|
|
startIcon={{ icon: Copy }}
|
|
iconOnly
|
|
on:click={() => copyToClipboard(collabUrl())}
|
|
/>
|
|
</div>
|
|
</Modal>
|
|
<div class="border-b shadow-sm px-1 pr-4" bind:clientWidth={width}>
|
|
<div class="flex justify-between space-x-2">
|
|
{#if args}
|
|
<EditorBar
|
|
scriptPath={edit ? path : undefined}
|
|
on:toggleCollabMode={() => {
|
|
if (wsProvider?.shouldConnect) {
|
|
disableCollaboration()
|
|
} else {
|
|
setCollaborationMode()
|
|
}
|
|
}}
|
|
on:showDiffMode={showDiffMode}
|
|
on:hideDiffMode={hideDiffMode}
|
|
customUi={customUi?.editorBar}
|
|
collabLive={wsProvider?.shouldConnect}
|
|
{collabMode}
|
|
{validCode}
|
|
iconOnly={width < EDITOR_BAR_WIDTH_THRESHOLD}
|
|
on:collabPopup={() => (showCollabPopup = true)}
|
|
{editor}
|
|
{lang}
|
|
on:createScriptFromInlineScript
|
|
{websocketAlive}
|
|
collabUsers={peers}
|
|
kind={asKind(kind)}
|
|
{template}
|
|
{args}
|
|
{noHistory}
|
|
{saveToWorkspace}
|
|
lastDeployedCode={lastDeployedCode && lastDeployedCode !== code
|
|
? lastDeployedCode
|
|
: undefined}
|
|
{diffMode}
|
|
bind:showHistoryDrawer
|
|
>
|
|
{#snippet right()}
|
|
{@render editor_bar_right?.()}
|
|
{/snippet}
|
|
</EditorBar>
|
|
{/if}
|
|
{#if !noSyncFromGithub && customUi?.editorBar?.useVsCode != false}
|
|
<div class="py-1">
|
|
<Button
|
|
target="_blank"
|
|
href="https://www.windmill.dev/docs/cli_local_dev/vscode-extension"
|
|
variant="subtle"
|
|
unifiedSize="md"
|
|
btnClasses="hidden lg:flex"
|
|
startIcon={{
|
|
icon: Github
|
|
}}
|
|
>
|
|
VScode
|
|
</Button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<SplitPanesWrapper>
|
|
<Splitpanes class="!overflow-visible">
|
|
<Pane bind:size={codePanelSize} minSize={10} class="!overflow-visible">
|
|
{#if lang === 'ansible' && ansibleAlternativeExecutionMode != null}
|
|
<!-- Vertical split for ansible with assets -->
|
|
<Splitpanes horizontal class="!overflow-visible h-full">
|
|
<Pane size={60} minSize={30} class="!overflow-visible">
|
|
{@render editorContent()}
|
|
</Pane>
|
|
<Pane size={40} minSize={20} class="!overflow-visible">
|
|
<div
|
|
class="h-full flex flex-col bg-surface border-l border-gray-200 dark:border-gray-700"
|
|
>
|
|
<div class="p-3 border-b border-gray-200 dark:border-gray-700">
|
|
<h4 class="text-sm font-semibold text-primary">File Browser</h4>
|
|
</div>
|
|
<GitRepoViewer
|
|
gitRepoResourcePath={ansibleAlternativeExecutionMode?.resource || ''}
|
|
gitSshIdentity={ansibleGitSshIdentity}
|
|
bind:commitHashInput={commitHashForGitRepo}
|
|
/>
|
|
</div>
|
|
</Pane>
|
|
</Splitpanes>
|
|
{:else}
|
|
<!-- Original single editor layout -->
|
|
{@render editorContent()}
|
|
{/if}
|
|
</Pane>
|
|
<Pane bind:size={testPanelSize} minSize={0}>
|
|
<div class="flex flex-col h-full">
|
|
{#if showTabs}
|
|
<div transition:slide={{ duration: 200 }}>
|
|
<Tabs bind:selected={selectedTab}>
|
|
<Tab value="main" label="Main" />
|
|
{#if hasPreprocessor}
|
|
<div transition:slide={{ duration: 200, axis: 'x' }}>
|
|
<Tab value="preprocessor" label="Preprocessor" />
|
|
</div>
|
|
{/if}
|
|
</Tabs>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="flex justify-center pt-1 relative">
|
|
<div class="absolute top-2 left-2">
|
|
<HideButton
|
|
hidden={false}
|
|
direction="right"
|
|
panelName="Test"
|
|
shortcut="U"
|
|
size="md"
|
|
on:click={() => {
|
|
toggleTestPanel()
|
|
}}
|
|
/>
|
|
</div>
|
|
{#if testIsLoading}
|
|
<Button on:click={jobLoader?.cancelJob} btnClasses="w-full" color="red" size="xs">
|
|
<WindmillIcon
|
|
white={true}
|
|
class="mr-2 text-white"
|
|
height="16px"
|
|
width="20px"
|
|
spin="fast"
|
|
/>
|
|
Cancel
|
|
</Button>
|
|
{:else}
|
|
{@const disableTriggerButton = customUi?.previewPanel?.disableTriggerButton === true}
|
|
<div class="flex flex-row divide-x divide-gray-800 dark:divide-gray-300 items-stretch">
|
|
<Button
|
|
on:click={() => runTest()}
|
|
btnClasses="w-full {!disableTriggerButton ? 'rounded-r-none' : ''}"
|
|
size="xs"
|
|
variant="accent-secondary"
|
|
startIcon={{ icon: Play, classes: 'animate-none' }}
|
|
shortCut={{ Icon: CornerDownLeft, hide: testIsLoading }}
|
|
>
|
|
{#if testIsLoading}
|
|
Running
|
|
{:else}
|
|
Test
|
|
{/if}
|
|
</Button>
|
|
{#if !disableTriggerButton}
|
|
<CaptureButton on:openTriggers />
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
<div class="absolute top-2 right-2"
|
|
><Toggle size="2xs" bind:checked={jsonView} options={{ right: 'JSON' }} /></div
|
|
>
|
|
</div>
|
|
<Splitpanes horizontal class="!max-h-[calc(100%-43px)]">
|
|
<Pane size={33}>
|
|
{#if jsonView}
|
|
<div
|
|
class="py-2"
|
|
style="height: {!schemaHeight || schemaHeight < 600 ? 600 : schemaHeight}px"
|
|
data-schema-picker
|
|
>
|
|
<JsonInputs
|
|
on:select={(e) => {
|
|
if (e.detail) {
|
|
args = e.detail
|
|
}
|
|
}}
|
|
updateOnBlur={false}
|
|
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}`}
|
|
/>
|
|
</div>
|
|
{:else}
|
|
<div class="px-4">
|
|
<div class="break-words relative font-sans" bind:clientHeight={schemaHeight}>
|
|
{#key argsRender}
|
|
<SchemaForm
|
|
helperScript={{
|
|
source: 'inline',
|
|
code,
|
|
//@ts-ignore
|
|
lang
|
|
}}
|
|
compact
|
|
{schema}
|
|
bind:args
|
|
bind:isValid
|
|
noVariablePicker={customUi?.previewPanel?.disableVariablePicker === true}
|
|
showSchemaExplorer
|
|
/>
|
|
{/key}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</Pane>
|
|
<Pane size={67} class="relative">
|
|
<LogPanel
|
|
bind:this={logPanel}
|
|
{lang}
|
|
previewJob={testJob}
|
|
{pastPreviews}
|
|
previewIsLoading={testIsLoading}
|
|
{editor}
|
|
{diffEditor}
|
|
{args}
|
|
{showCaptures}
|
|
customUi={customUi?.previewPanel}
|
|
>
|
|
{#if scriptProgress}
|
|
<!-- Put to the slot in logpanel -->
|
|
<JobProgressBar
|
|
job={testJob}
|
|
{scriptProgress}
|
|
bind:this={jobProgressBar}
|
|
compact={true}
|
|
/>
|
|
{/if}
|
|
{#snippet capturesTab()}
|
|
<div class="h-full p-2">
|
|
<CaptureTable
|
|
bind:this={captureTable}
|
|
{hasPreprocessor}
|
|
canHavePreprocessor={canHavePreprocessor(lang)}
|
|
isFlow={false}
|
|
path={stablePathForCaptures}
|
|
canEdit={true}
|
|
on:applyArgs
|
|
on:updateSchema
|
|
on:addPreprocessor
|
|
/>
|
|
</div>
|
|
{/snippet}
|
|
</LogPanel>
|
|
</Pane>
|
|
</Splitpanes>
|
|
</div>
|
|
</Pane>
|
|
</Splitpanes>
|
|
</SplitPanesWrapper>
|
|
|
|
{#snippet editorContent()}
|
|
<div class="h-full !overflow-visible bg-surface dark:bg-[#272D38] relative">
|
|
<div class="absolute top-2 right-4 z-10 flex flex-row gap-2">
|
|
{#if assets?.length}
|
|
<AssetsDropdownButton {assets} />
|
|
{/if}
|
|
{#if lang === 'ansible' && hasDelegateToGitRepo}
|
|
<Button
|
|
variant="default"
|
|
size="xs"
|
|
on:click={() => (gitRepoResourcePickerOpen = true)}
|
|
startIcon={{ icon: GitBranch }}
|
|
btnClasses="bg-surface hover:bg-surface-hover border border-tertiary/30"
|
|
>
|
|
Delegating to git repo
|
|
</Button>
|
|
{/if}
|
|
{#if testPanelSize === 0}
|
|
<HideButton
|
|
hidden={true}
|
|
direction="right"
|
|
size="md"
|
|
panelName="Test"
|
|
shortcut="U"
|
|
customHiddenIcon={{
|
|
icon: PlayIcon
|
|
}}
|
|
on:click={() => {
|
|
toggleTestPanel()
|
|
}}
|
|
btnClasses="bg-marine-400 hover:bg-marine-200 !text-primary-inverse hover:!text-primary-inverse hover:dark:!text-primary-inverse dark:bg-marine-50 dark:hover:bg-marine-50/70"
|
|
color="marine"
|
|
/>
|
|
{/if}
|
|
{#if !aiChatManager.open && !disableAi}
|
|
{#if customUi?.editorBar?.aiGen != false && SUPPORTED_CHAT_SCRIPT_LANGUAGES.includes(lang ?? '')}
|
|
<HideButton
|
|
hidden={true}
|
|
direction="right"
|
|
panelName="AI"
|
|
shortcut="L"
|
|
size="md"
|
|
usePopoverOverride={!$copilotInfo.enabled}
|
|
customHiddenIcon={{
|
|
icon: WandSparkles
|
|
}}
|
|
btnClasses="!text-ai border border-gray-200 dark:border-gray-600 bg-surface"
|
|
on:click={() => {
|
|
if (!aiChatManager.open) {
|
|
aiChatManager.changeMode(AIMode.SCRIPT)
|
|
}
|
|
aiChatManager.toggleOpen()
|
|
}}
|
|
>
|
|
{#snippet popoverOverride()}
|
|
<div class="text-sm">
|
|
Enable Windmill AI in the <a
|
|
href="{base}/workspace_settings?tab=ai"
|
|
target="_blank"
|
|
class="inline-flex flex-row items-center gap-1"
|
|
>
|
|
workspace settings <ExternalLink size={16} />
|
|
</a>
|
|
</div>
|
|
{/snippet}
|
|
</HideButton>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
{#key lang}
|
|
<Editor
|
|
lineNumbersMinChars={4}
|
|
folding
|
|
{path}
|
|
bind:code
|
|
bind:websocketAlive
|
|
bind:this={editor}
|
|
{yContent}
|
|
awareness={wsProvider?.awareness}
|
|
on:change={(e) => {
|
|
inferSchema(e.detail)
|
|
}}
|
|
on:saveDraft
|
|
on:toggleTestPanel={toggleTestPanel}
|
|
cmdEnterAction={async () => {
|
|
await inferSchema(code)
|
|
runTest()
|
|
}}
|
|
formatAction={async () => {
|
|
await inferSchema(code)
|
|
try {
|
|
localStorage.setItem(path ?? 'last_save', code)
|
|
} catch (e) {
|
|
console.error('Could not save last_save to local storage', e)
|
|
}
|
|
dispatch('format')
|
|
}}
|
|
class="flex flex-1 h-full !overflow-visible"
|
|
scriptLang={lang}
|
|
automaticLayout={true}
|
|
{fixedOverflowWidgets}
|
|
{args}
|
|
{enablePreprocessorSnippet}
|
|
/>
|
|
<DiffEditor
|
|
className="h-full"
|
|
bind:this={diffEditor}
|
|
modifiedModel={editor?.getModel() as meditor.ITextModel}
|
|
automaticLayout
|
|
defaultLang={scriptLangToEditorLang(lang)}
|
|
{fixedOverflowWidgets}
|
|
buttons={diffMode
|
|
? [
|
|
{
|
|
text: 'See changes history',
|
|
onClick: () => {
|
|
showHistoryDrawer = true
|
|
}
|
|
},
|
|
{
|
|
text: 'Quit diff mode',
|
|
onClick: () => {
|
|
hideDiffMode()
|
|
},
|
|
color: 'red'
|
|
}
|
|
]
|
|
: []}
|
|
/>
|
|
{/key}
|
|
</div>
|
|
{/snippet}
|
|
|
|
<GitRepoResourcePicker
|
|
bind:open={gitRepoResourcePickerOpen}
|
|
currentResource={ansibleAlternativeExecutionMode?.resource}
|
|
currentCommit={commitHashForGitRepo || ansibleAlternativeExecutionMode?.commit}
|
|
currentInventories={ansibleAlternativeExecutionMode?.inventories_location}
|
|
currentPlaybook={ansibleAlternativeExecutionMode?.playbook}
|
|
gitSshIdentity={ansibleGitSshIdentity}
|
|
on:selected={handleDelegateConfigUpdate}
|
|
on:addInventories={handleAddInventories}
|
|
/>
|