This guide explains how IDEs can integrate with the Snyk Language Server's configuration dialog to provide a user-friendly interface for managing Snyk settings.
The configuration dialog is an HTML-based interface that allows users to view and modify all Snyk Language Server settings. The dialog is triggered via an LSP command and displayed in the IDE's webview or browser component.
The configuration dialog follows a client-server pattern:
- Language Server: Generates HTML content with current settings, handles configuration updates
- IDE Client: Displays the HTML, injects JavaScript functions for user interactions, applies configuration changes
The dialog uses a modular JavaScript architecture under infrastructure/configuration/template/js/ (loaded via app.js):
| Path | Role |
|---|---|
app.js |
Bootstraps ConfigApp and loads modules |
core/utils.js |
Utilities (debounce, cloning, IE-oriented helpers) |
core/dom.js |
DOM helpers (ConfigApp.dom: get, addEvent, triggerEvent, …) |
state/dirty-tracker.js |
DirtyTracker — dirty state vs last-saved snapshot |
state/form-state.js |
Listeners for dirty + auto-save orchestration |
ui/form-handler.js |
collectData() — builds the JSON object sent to the IDE (pflag-style keys, see below) |
ui/reset-handler.js |
Section reset actions |
ui/tabs.js |
Tab switching, folder dropdown, and no-folder disabled state |
ui/tooltips.js |
Tooltip behavior |
features/auto-save.js |
Validates, stringifies, calls ideBridge.saveConfig |
features/validation.js |
Endpoint, risk score, additional env validation |
features/authentication.js |
Auth UI wiring |
features/auth-field-monitor.js |
Token / auth-sensitive field baseline sync |
features/folders.js |
Folder-specific UI helpers |
ide/bridge.js |
ConfigApp.ideBridge — save, login/logout via executeCommand, dirty notify |
Global instances: window.dirtyTracker, window.ConfigApp (namespace). IDE bridge functions are on window (see below).
The IDE triggers the configuration dialog by executing the LSP command snyk.workspace.configuration.
Command Details:
- Command:
workspace/executeCommand - Command ID:
snyk.workspace.configuration - Arguments:
[](no arguments required)
Response:
"<html>... full HTML content ...</html>"
The command returns the complete HTML content as a string. The IDE can directly display this in a webview without any additional processing.
See Opening Configuration Dialog Sequence for the detailed flow.
The IDE should:
- Execute the command and receive the response
- Use the returned string as the webview HTML (the handler returns the HTML body directly)
- Create a webview or browser component
- Inject IDE-specific JavaScript functions (see Function Injection)
- Load the HTML content and display it
Example (conceptual):
// Execute command and receive HTML
const html = await client.sendRequest('workspace/executeCommand', {
command: 'snyk.workspace.configuration',
arguments: []
});
// Create webview and display
const webview = createWebview();
webview.html = injectFunctions(html);
webview.show();The HTML does not use __ideLogin__ / __ideLogout__ — authentication and logout go through window.__ideExecuteCommand__ (same pattern as the tree view). The IDE must implement:
| Function | Purpose | Required |
|---|---|---|
window.__ideExecuteCommand__(command, args, callback?) |
Forward to workspace/executeCommand |
Yes |
window.__saveIdeConfig__(jsonString) |
Persist settings from the HTML form | Yes |
window.__IS_IDE_AUTOSAVE_ENABLED__ |
If true, form changes trigger save via auto-save.js |
No (default false) |
window.__onFormDirtyChange__(isDirty) |
Dirty-state callback for tab chrome | No |
window.__ideSaveAttemptFinished__(status) |
Save outcome: success, validation_error, bridge_missing, error |
No |
Calls from ide/bridge.js:
- Login:
__ideExecuteCommand__('snyk.login', [authMethod, endpoint, insecure]) - Logout:
__ideExecuteCommand__('snyk.logout', []) - Save:
__saveIdeConfig__(jsonString)wherejsonStringisJSON.stringifyof the object fromform-handler.collectData()(notworkspace/didChangeConfiguration— the IDE maps into LSP config).
IDE-callable helpers (on window):
| Function | Purpose |
|---|---|
window.getAndSaveIdeConfig() |
Run validation + save (same as auto-save path) |
window.__isFormDirty__() |
Whether DirtyTracker sees unsaved edits |
window.setAuthToken(token, apiUrl?) |
After OAuth/token login: set token / api_endpoint fields and sync dirty baseline |
Injection example (conceptual):
webview.window.__ideExecuteCommand__ = (command, args, callback) => {
client.sendRequest('workspace/executeCommand', { command, arguments: args }).then(callback);
};
webview.window.__saveIdeConfig__ = async (jsonString: string) => {
await persistHtmlFormJsonToSnykSettings(jsonString); // IDE maps keys → LspConfigurationParam
};
webview.window.__IS_IDE_AUTOSAVE_ENABLED__ = true;
webview.window.__onFormDirtyChange__ = (isDirty: boolean) => { /* update tab title */ };See Function Injection Flow for the detailed sequence.
On save (or auto-save), features/auto-save.js calls form-handler.collectData(), then JSON.stringifys the result and passes that string to window.__saveIdeConfig__(jsonString). The object uses the same pflag-oriented names as the rest of the LS (snake_case / wire names), not legacy camelCase init-option names.
Global keys (examples) — see infrastructure/configuration/template/js/ui/form-handler.js and template/config.html:
snyk_oss_enabled,snyk_code_enabled,snyk_iac_enabled,snyk_secrets_enabled(booleans)scan_automatic,organization,api_endpoint,token,proxy_insecure,authentication_methodseverity_filter_critical,severity_filter_high,severity_filter_medium,severity_filter_low(booleans)issue_view_open_issues,issue_view_ignored_issues(booleans)risk_score_threshold(number),scan_net_new(boolean — delta / net-new findings)cli_path,automatic_download,binary_base_url,cli_release_channeltrusted_folders: string array (fromtrustedFolder_*inputs)additional_parameters(string, space-separated) — Project Defaults Advanced section. Default CLI parameters for all project scans; stored ascli_additional_oss_parameters([]string) atUserGlobalKeybyapplyCliConfig. These combine with per-projectadditional_parameters: both sets of tokens are used together. Not LDX-Sync routed.additional_environment(string, semicolon-separatedKEY=VALUEpairs) — Project Defaults Advanced section. Default environment variables for all project scans; applied viaos.Setenvand also persisted atUserGlobalKey(additional_environment)for dialog pre-population. Combine with per-project values: per-project overrides win on key conflict. Not LDX-Sync routed.
Project Defaults Advanced section: The {{FolderLabel}} defaults tab contains a collapsible "Advanced" section (id defaults-advanced) with name="additional_parameters" and name="additional_environment" global inputs. A static info-box above the fields explains that values at both levels combine. The exact UX copy is TBD (Tars to finalize, per IDE-2110 comment 2026-06-05).
Per folder: folderConfigs is an array of objects with keys such as folderPath, preferred_org, additional_parameters (array of CLI tokens), additional_environment, org_set_by_user, scan_command_config, plus folder-scope overrides (scan_automatic, severity_filter_critical, severity_filter_high, severity_filter_medium, severity_filter_low, snyk_oss_enabled, …). Folder field name= attributes use the folder_<index>_<setting> convention (e.g. folder_0_severity_filter_critical); form-handler.collectData() parses the index and writes the suffix as a flat key under folderConfigs[i]. Per-folder additional_parameters arrives as a []string (split on whitespace by form-handler.js); per-folder additional_environment arrives as a raw string.
From IDE to LS (protocol v25): the plugin must translate the saved JSON into workspace/didChangeConfiguration using the nested envelope documented in configuration.md and types.DidChangeConfigurationParams — the LSP settings field wraps an LspConfigurationParam (settings map of ConfigSetting, folderConfigs, trustedFolders). Keys in the maps are pflag names (e.g. snyk_oss_enabled, api_endpoint).
Processing (server): UpdateSettings in application/server/configuration.go applies machine and folder maps, respects changed on each ConfigSetting, then persists and notifies (e.g. $/snyk.configuration).
Important notes:
- HTML → IDE is a JSON string; parsing and mapping to LSP are IDE-side.
- Tokens and other write-only settings follow the same resolution rules as in configuration.md.
See Saving Configuration Flow for the sequence diagram.
The per-folder "Reset overrides" button (one per folder tab, top-right in the disclaimer banner) clears all of that folder's user overrides so the effective values fall back to org / LDX-sync / default.
JS side (snyk-ls, already implemented): clicking the button marks the folder by its folderPath (read from the hidden folder_<index>_folderPath input — not the index, which is compacted away during diffing). On save, form-handler.applyFolderResets() emits flat null for each of these 17 folder fields on that folder's entry in folderConfigs:
scan_automatic, scan_net_new,
severity_filter_critical, severity_filter_high, severity_filter_medium, severity_filter_low,
snyk_oss_enabled, snyk_code_enabled, snyk_iac_enabled, snyk_secrets_enabled,
issue_view_open_issues, issue_view_ignored_issues, risk_score_threshold,
preferred_org,
additional_parameters, additional_environment, scan_command_config
A reset is emitted even if the folder has no other edits — a reset-only folder still appears in the outbound folderConfigs keyed by folderPath. The JS deliberately emits flat snake_case null, not a ConfigSetting envelope; building the envelope is the IDE plugin's job (next paragraph).
The IDE plugin MUST map a flat null folder field → {value: null, changed: true} for that folder key when building workspace/didChangeConfiguration. This is the cross-IDE reset contract:
| Saved JSON folder field | IDE maps to ConfigSetting |
LS effect |
|---|---|---|
| absent | omit from the map | no-op |
| present, non-null value | {value: X, changed: true} |
set user:folder:<path>:<key> |
present, null |
{value: null, changed: true} |
Unset the user:folder: override → fall back |
A plugin that treats a flat null as "absent" (e.g. nullable-type ?.let{}, node.isNull() → continue, .HasValue == false) will silently drop the reset and the button will appear to do nothing. The null must survive deserialization (parse the raw JSON tree to distinguish present-with-null from absent when the data model can't) and reach the LS as {value: null, changed: true}. See the reset contract in configuration.md.
The {{FolderLabel}} defaults (Project Defaults) tab has a Reset overrides button (.reset-global-overrides-btn, rendered in the fallbacks-pane info-box of config.html). Clicking it calls formHandler.markGlobalForReset(), which sets ConfigApp.globalReset = true. On the next save, auto-save.js calls formHandler.applyGlobalResets(data), which writes every key in GLOBAL_RESET_FIELDS (the 14 org-scope keys, kept in sync with the Go types.GlobalResettableSettings) as null at the top level of the saved JSON — never inside folderConfigs — and clears the flag. The IDE translates each top-level key: null into a {changed: true, value: null} ConfigSetting, which the LS treats as a global reset: it clears the user:global override and reverts the value to the LDX-Sync / org / flagset default (organization reverts to the web account's preferred org). See Global Reset in configuration.md.
This is distinct from the per-section Reset buttons (.reset-section-btn, handled by reset-handler.js): a section reset only writes hard-coded constant defaults back into the form fields client-side (it does not clear user:global overrides server-side), whereas Reset overrides clears the stored overrides so the effective values fall back through the precedence chain. It also mirrors the per-folder Reset overrides button (.reset-overrides-btn), which clears user:folder overrides for a single folder rather than the machine-wide user:global layer.
The dialog renders the Secure At Inception section only when configuration.INTEGRATION_NAME is
exactly VS_CODE. It serializes two top-level machine-scope values:
auto_configure_mcp_server as a boolean and
secure_at_inception_execution_frequency as one of On Code Generation,
Smart Scan, or Manual. A blank frequency is displayed as Manual without
persisting a value merely by rendering the dialog.
The VS Code bridge maps these LS machine-scope values to window-scoped VS Code
settings; neither value belongs in folderConfigs. The Secure At Inception section Reset action
sets the form values to false and Manual. JetBrains, Eclipse, Visual Studio,
and integrations with a blank name intentionally do not render the Secure At Inception
section because their configuration bridges do not persist these values.
When the user clicks Authenticate, features/authentication.js calls ConfigApp.ideBridge.login(authMethod, endpoint, insecure), which invokes window.__ideExecuteCommand__('snyk.login', [authMethod, endpoint, insecure]).
IDE responsibilities:
- Run the Snyk login flow (OAuth, API token, or PAT per IDE).
- On success, call
window.setAuthToken(token, apiUrl?)so the form fieldstokenand optionallyapi_endpointupdate (and dirty baseline syncs). - Persist auth via your normal path (LS token +
didChangeConfiguration/ internal APIs as your plugin already does).
Do not rely on sending a flat { token } object through didChangeConfiguration unless you map it to ConfigSetting entries with pflag keys (token, api_endpoint, …).
See Authentication Flow for the detailed sequence.
When the user clicks Logout, ideBridge.logout() runs window.__ideExecuteCommand__('snyk.logout', []).
IDE responsibilities: clear credentials, complete server-side logout, refresh UI / webview as needed.
See Logout Flow for the detailed sequence.
The dialog includes a dirty tracking system that monitors form changes and notifies the IDE when there are unsaved changes.
How it Works:
- Initial State Capture: When the dialog loads, the
DirtyTrackercaptures a deep clone of the initial form state - Change Detection: Form inputs are monitored with event listeners - text inputs and textareas use
inputandchangeevents, while select dropdowns and checkboxes usechangeevents - Deep Comparison: Before comparison, values are normalized (empty strings → null, "true"/"false" → booleans, numeric strings → numbers). The tracker then performs deep equality checks between normalized current and original state
- State Transition Events: When dirty state transitions (clean→dirty or dirty→clean),
window.__onFormDirtyChange__(isDirty)is called - Reset After Save: After successful save, the tracker resets with the new saved state as the baseline
IDE Integration:
// Listen for dirty state changes
webview.window.__onFormDirtyChange__ = (isDirty: boolean) => {
if (isDirty) {
// Show unsaved changes indicator
setDocumentIcon("*");
enableSaveButton();
} else {
// Clear indicator
setDocumentIcon("");
disableSaveButton();
}
};
// Check dirty state before closing dialog
function beforeClose() {
if (webview.window.__isFormDirty__()) {
const shouldClose = confirm("You have unsaved changes. Close anyway?");
if (!shouldClose) return false;
}
return true;
}
// After successful save, the HTML calls dirtyTracker.reset(data) internally; do not rely on a separate __resetDirtyState__
async function handleSave(jsonString: string) {
try {
await saveConfiguration(jsonString);
} catch (error) {
showError('Failed to save: ' + error.message);
}
}Features:
- Deep equality comparison handles nested objects and arrays
- Value normalization (empty strings = null, "true"/"false" to booleans)
- Debounced change detection for performance
- Automatic reset after successful save
sequenceDiagram
participant IDE as IDE Client
participant LSP as Language Server
participant Config as Config System
participant HTML as HTML Renderer
IDE->>LSP: workspace/executeCommand<br/>{command: "snyk.workspace.configuration"}
LSP->>Config: Get current configuration
Config-->>LSP: Configuration data
LSP->>Config: Construct Settings from Config
Config-->>LSP: Settings object
LSP->>HTML: Generate HTML with settings
HTML-->>LSP: HTML content
LSP-->>IDE: Command response<br/>"<html>..." (HTML string)
IDE->>IDE: Create webview
IDE->>IDE: Inject IDE functions into HTML
IDE->>IDE: Display HTML in webview
sequenceDiagram
participant IDE as IDE Client
participant Webview as Webview Component
participant HTML as HTML Content
IDE->>IDE: Receive HTML content
IDE->>Webview: Create webview instance
IDE->>Webview: Expose functions on window object:<br/>- window.__ideExecuteCommand__(cmd, args)<br/>- window.__saveIdeConfig__(jsonString)<br/>- window.__onFormDirtyChange__(isDirty)
IDE->>Webview: Load HTML content
Webview-->>IDE: Webview ready
Note over HTML,Webview: User interacts with dialog
HTML->>Webview: User clicks "Authenticate"
Webview->>IDE: Call __ideExecuteCommand__('snyk.login', [...])
IDE->>IDE: Handle authentication
HTML->>Webview: User clicks "Save"
Webview->>Webview: collectData()
Webview->>IDE: Call window.__saveIdeConfig__(jsonString)
IDE->>IDE: Map JSON → didChangeConfiguration / persist
HTML->>Webview: User clicks "Logout"
Webview->>IDE: Call __ideExecuteCommand__('snyk.logout', [])
IDE->>IDE: Handle logout
sequenceDiagram
participant User as User
participant Dialog as Configuration Dialog
participant Webview as Webview
participant IDE as IDE Client
participant LSP as Language Server
participant Config as Config System
User->>Dialog: Modify settings
User->>Dialog: Click "Save Configuration"
Dialog->>Dialog: collectData()<br/>Gather all form values
Dialog->>Webview: Call window.__saveIdeConfig__(jsonString)
Webview->>IDE: Post message with config data
IDE->>IDE: Validate configuration data
IDE->>LSP: workspace/didChangeConfiguration<br/>{settings: data}
LSP->>Config: Update configuration
Config->>Config: Validate settings
Config->>Config: Apply settings
Config->>Config: Persist to storage
Config-->>LSP: Configuration updated
LSP-->>IDE: Acknowledgment
IDE->>Webview: Show success message
Webview->>Dialog: Display "Configuration saved"
Dialog->>User: Visual feedback
sequenceDiagram
participant User as User
participant Dialog as Configuration Dialog
participant IDE as IDE Client
participant Auth as Auth Service
participant LSP as Language Server
participant Snyk as Snyk API
User->>Dialog: Click "Authenticate"
Dialog->>IDE: Call window.ideLogin()
IDE->>Auth: Initiate authentication
alt OAuth Flow
Auth->>Snyk: Request OAuth authorization
Snyk-->>User: Open browser with auth page
User->>Snyk: Approve authorization
Snyk->>Auth: OAuth callback with code
Auth->>Snyk: Exchange code for token
Snyk-->>Auth: Access token
else Token Authentication
Auth->>User: Prompt for API token
User->>Auth: Provide token
end
Auth-->>IDE: Authentication successful<br/>{token: "..."}
IDE->>LSP: workspace/didChangeConfiguration<br/>{settings: {token: "..."}}
LSP->>Snyk: Verify token
Snyk-->>LSP: Token valid
LSP-->>IDE: Configuration updated
IDE->>Dialog: Refresh with authenticated state
Dialog->>User: Show "Authenticated" status
sequenceDiagram
participant User as User
participant Dialog as Configuration Dialog
participant IDE as IDE Client
participant LSP as Language Server
participant Auth as Auth Service
participant Storage as Credential Storage
User->>Dialog: Click "Logout"
Dialog->>IDE: __ideExecuteCommand__('snyk.logout', [])
IDE->>LSP: workspace/executeCommand<br/>{command: "snyk.logout"}
LSP->>Auth: Clear authentication state
Auth->>Storage: Remove credentials
Storage-->>Auth: Credentials cleared
Auth-->>LSP: Logout complete
LSP-->>IDE: Command completed
IDE->>IDE: Clear local token storage
IDE->>Dialog: Refresh with logged-out state
Dialog->>User: Show "Not authenticated" status
- Execute
snyk.workspace.configurationcommand - Use returned string as webview HTML
- Create webview/browser component for display
- Expose
window.__ideExecuteCommand__,window.__saveIdeConfig__, and optional__onFormDirtyChange__/__ideSaveAttemptFinished__ - Display HTML content in webview
- Implement
window.__saveIdeConfig__(jsonString)to receiveJSON.stringifyoutput from the form - Parse JSON and map keys to
ConfigSettingmaps (pflag names); sendworkspace/didChangeConfigurationwith theLspConfigurationParamenvelope (seetypes.DidChangeConfigurationParams) - Validate configuration data before sending
- Handle configuration errors gracefully
- Provide user feedback on save success/failure (
__ideSaveAttemptFinished__/ UI) - Rely on HTML
dirtyTracker.resetafter successful save (no separate__resetDirtyState__)
- Implement
window.__ideExecuteCommand__sosnyk.logincan run with optional[authMethod, endpoint, insecure] - Support OAuth flow (recommended)
- Support PAT (Personal Access Token) authentication
- Support API token authentication (legacy)
- Update language server on successful authentication
- Handle authentication errors
- Optionally refresh dialog after authentication
- Route logout through
__ideExecuteCommand__('snyk.logout', [])(not a separate__ideLogout__on the window) - Execute
snyk.logoutcommand - Clear stored credentials
- Update UI to reflect logged-out state
- Optionally refresh dialog after logout
- Implement
window.__onFormDirtyChange__(isDirty)callback - Show unsaved changes indicator in IDE UI (e.g., "*" in tab title)
- Warn user before closing dialog with unsaved changes using
window.__isFormDirty__() - Disable save button when form is clean
- Enable save button when form is dirty
- Set
window.__IS_IDE_AUTOSAVE_ENABLED__ = truebefore loading HTML - Handle automatic saves triggered by form changes
- Provide feedback for auto-save operations
- Display loading indicators during operations
- Show success/error messages
- Provide validation feedback for form fields
- Support dialog refresh after configuration changes
- Handle dialog close/cancel actions
- Implement beforeunload confirmation for unsaved changes
-
Error Handling: Always wrap LSP calls in try-catch blocks and provide meaningful error messages to users.
-
Validation: Validate configuration data on the IDE side before sending to the language server.
-
Security:
- Never log or expose authentication tokens
- Use secure credential storage
- Clear sensitive data on logout
-
User Feedback: Provide immediate visual feedback for all user actions (save, authenticate, logout).
-
Refresh Strategy: After authentication or logout, consider refreshing the dialog to show the updated state.
-
Webview Isolation: Use proper webview security settings to isolate the dialog from other IDE components.
- Verify the command ID is correct:
snyk.workspace.configuration - Check that the language server is initialized
- Ensure
workspace/executeCommandcapability is supported
- Verify function injection is replacing all placeholders
- Check that webview message passing is configured correctly
- Ensure functions are exposed to the webview's global scope
- Verify the
workspace/didChangeConfigurationnotification format - Check that the language server has
workspace.configurationcapability - Validate configuration data structure
- Ensure network connectivity to Snyk API
- Verify OAuth redirect URLs are configured correctly
- Check that the token is valid and not expired
For issues or questions about the configuration dialog integration:
- Open an issue on GitHub
- Join the discussion in Snyk Community




