feat(workflow): add LSP server, VS Code extension, and graph preview for the workflow DSL#2377
Merged
curtisman merged 47 commits intoMay 22, 2026
Conversation
…docs
Adds the foundation for a Language Server Protocol implementation for
the workflow DSL (.wf files) and its companion VS Code extension.
Design docs (ts/docs/design/workflowSystem/editor/):
- lsp-plan.md full plan with phases, features, protocol
- lsp-decisions.md design decisions / non-obvious choices
- lsp-review-log.md log of deferred review findings
- lsp-manual-tests.md per-phase manual test checklist
workflow-engine changes:
- New src/builtinTaskSchemas.ts re-declares the 31 builtin task
schemas without importing aiclient.
- New schemas-only sub-export ("workflow-engine/schemas") so the LSP
can pull schemas without dragging aiclient into the bundle.
- New jest spec asserts the duplicated schemas stay in sync with the
authoritative TaskDefinitions.
workflow-lsp (new package, examples/workflow/lsp/):
- Bin entry that starts an LSP server on stdio.
- createServer() supporting both stdio and injected duplex streams
for in-process integration tests.
- Phase 0 capabilities: incremental text-document sync, initialize.
- documentStore, taskSchemas wrapper, position util, feature stubs.
- jest integration smoke test verifies initialize response.
workflow-vscode (new package, examples/workflow/vscode/):
- Registers language id 'workflow' for .wf files.
- TextMate grammar (keywords, strings, templates, comments, numbers).
- language-configuration.json (brackets, comments, autoclosing).
- Extension activates and launches the bundled LSP via Node IPC.
- workflow.trace.server config to debug client/server messages.
- esbuild.mjs bundles extension + server (server.js: ~171KB).
- scripts/check-server-bundle.mjs asserts no aiclient / @Azure SDKs
leak into the shipped bundle (dep-cycle regression guard).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wires the workflow DSL compiler / formatter into the LSP and adds the
first set of editor features.
Features (ts/examples/workflow/lsp/src/features/):
- diagnostics.ts compile() -> Diagnostic[]; tagged with phase code
(lex/parse/typecheck/emit/validate)
- formatting.ts lex+parse -> format(); whole-document TextEdit;
returns [] on parse error to preserve user source
- symbols.ts walk WorkflowDecl; outline shows workflow,
params, and top-level ConstStatement /
DestructuringConst bindings (synthetic bindings
filtered out)
Server wiring (ts/examples/workflow/lsp/src/server.ts):
- documentFormattingProvider and documentSymbolProvider capabilities
- onDidChangeContent schedules diagnostics with a 100ms debounce
- onDidClose cancels pending timers and clears prior diagnostics
- dispose() drains the pending-timer map
Grammar (ts/examples/workflow/vscode/syntaxes/workflow.tmLanguage.json):
- Replaced placeholder keyword list (param/let/for/in) with the actual
DSL keywords (switch/case/default/break/throw added)
- Grammar-drift jest test reads the lexer's KEYWORDS list and asserts
every entry is in the TextMate alternation
Position util:
- Renamed 'column' -> 'col' to match DSL SourceLocation
- Added pointRange(loc, length) for diagnostics
Tests (14 / 14 passing across 6 suites):
- diagnostics.spec.ts 4 tests (valid, parse, typecheck, lex)
- formatting.spec.ts 3 tests (reformat, no-op, parse error)
- symbols.spec.ts 3 tests (params+consts, garbage, destructure)
- grammarDrift.spec.ts 1 test (keyword sync)
- server.spec.ts 1 test (initialize)
- serverIntegration.spec.ts 2 tests (publish-on-open, clear-on-close)
Review log:
- lsp-review-log.md captures four inline-review findings that were
accepted as-is for Phase 1 (diagnostic range clamping deferred,
schemas-loaded-once by design, whole-doc format-edit by design,
process note about stalled subagent review cycle).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…semantic tokens Adds the navigation core for the workflow DSL language server: - symbolResolver: per-workflow scope chain + symbol table (Def/Ref/ TaskRef) including param, const, destructured-const, and lambda parameter bindings; resolves identifier expressions through the enclosing scope chain and records builtin task call sites. - parsedDocument: per-URI version-keyed cache of lex+parse+symbol-table results, invalidated on change/close. - hover: markdown for in-scope symbols (kind + line) and for builtin task names (schema title/description). - definition: go-to-def for bound names backed by the resolver. - references: include-decl-aware reference listing. - completion: in-scope names + builtin task names; '.' trigger. - semanticTokens: parameter / variable / function legend with full document delta-encoded payload. Tests: 27 specs across 8 suites pass (symbolResolver + features added). Bundle audit still clean (289.9 KB, no aiclient leak). Signature help and rename are deferred to Phase 3 / Phase 4 — see lsp-review-log.md entry. Subagent review cycle waived again pending a reliable turn-1 probe; inline self-review logged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the authoring-assist trio for the workflow DSL language server.
- **signatureHelp:** Scans back from the cursor through document text
(with string and comment masking) to find the innermost unclosed '('
and reads the identifier just before it. The identifier is matched
against the builtin task schema set and rendered with a parameter
list derived from inputSchema.required + properties. The active
parameter index is the depth-zero comma count between the open
paren and the cursor.
- **inlayHints:** For each non-synthetic ConstStatement whose value
is a TaskCallExpr, emits a TypeScript-style ': <type>' hint between
the binding name and the '=' token. Hints are suppressed when the
source already declares a type. Range-scoped requests are honoured.
- **Snippets:** Ships a workflow.code-snippets file with prefixes for
workflow scaffold, const/dconst, if/switch, map/filter/parallelMap,
parallel branches, attempts, shell.exec, and template literals.
Registered under contributes.snippets in package.json.
Server registers signatureHelpProvider (triggers '(' and ',') and
inlayHintProvider. New phase3.spec.ts: 9 specs across both features.
Total: 36 tests in 9 suites pass. Bundle audit still clean (292.6 KB).
Cancellation token plumbing and code actions are deferred to Phase 4
(see lsp-review-log.md).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements LSP textDocument/rename and textDocument/prepareRename. - **features/rename.ts:** prepareRename returns the range of the identifier under the cursor when it's a renamable user-declared symbol (param, const, lambda param). rename walks the symbol table, produces a WorkspaceEdit covering the declaration plus every reference, and rejects invalid identifiers via a regex check. - **symbolResolver.ts:** Threads the source text into buildSymbolTable so const / destructured-const defs are recorded at the binding name rather than the 'const' keyword. A forward scan from stmt.loc.offset produces the precise (line, col, offset). Falls back to stmt.loc when text isn't supplied (back-compat). - **parsedDocument.ts:** Passes document text into buildSymbolTable. Server registers renameProvider with prepareProvider=true. New rename.spec.ts adds 8 specs across prepareRename and rename (declaration site, reference site, invalid name, unresolved position, const-binding rename). Total: 44 tests in 10 suites pass. Bundle audit still clean (293.8 KB). Code actions and DSL AST nameLoc field are deferred to follow-up work (see lsp-review-log.md). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…bbed
Adds the IR preview half of Phase 5.
- **features/compileIR.ts:** server-side custom-request handler.
Compiles the active document with the builtin task schemas and
returns either the IR JSON or a serializable error list. Keeping
the call server-side avoids pulling workflow-dsl into the
extension bundle and preserves the dep-cycle audit guarantee.
- **server.ts:** wires connection.onRequest('workflow/compileIR', ...).
- **extension.ts:** registers workflow.previewIR (opens an untitled
JSON document with the IR or error list in a side editor) and
workflow.previewGraph (currently posts a 'coming soon' info
message until elkjs + webview lands in a follow-up).
- **package.json:** declares both commands under contributes.commands.
- **compileIR.spec.ts:** covers happy path, parse error path, and the
unknown-document branch.
Total: 47 tests across 11 suites pass. Bundle audit still clean
(294.1 KB).
Graph preview (extractGraph + elkjs + webview) is deferred — see
lsp-review-log.md.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three deferred items now implemented: - **features/codeActions.ts:** Two code actions wired via codeActionProvider. (1) 'Surround with attempts(3)' wraps a task-call const-binding in an attempts/fallback block. (2) 'Extract to const' inserts a new binding before the enclosing statement and replaces the selected sub-expression with the new name. Both produce WorkspaceEdits. Server registers codeActionProvider with rewrite + extract kinds. - **test/codeActions.spec.ts:** 5 specs covering the attempts action (offer, text shape, non-task guard) and the extract action (offer and edit shape). - **test/graphShape.spec.ts:** 4 Jest snapshots pinning the GraphModel returned by extractGraph() for linear, conditional, map, and parallel workflows. Snapshots written on first run; drift from upstream DSL graph model will surface as deliberate diffs. - **jest.config.cjs:** Coverage config (v8 provider, 70/60/70/70 thresholds) moved here from a duplicate package.json 'jest' key that was conflicting with the pre-existing jest.config.cjs. Added 'test:coverage' script. Total: 56 tests in 13 suites pass. 4 snapshots. Bundle clean (295.7 KB). Subagent reviews for these todos remain waived. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- lsp.md architecture doc (packages, data flow, feature inventory) - rangeFormatting provider (Gap 5) - keyword completions + namespace-prefix filtering (Gaps 11/12) - inline-const and concat→template code actions (Gap 6) - IR preview refreshes on save via onDidSaveTextDocument (Gap 13) - icons/wf.svg file icon + icons.json icon theme (Gap 10) - @vscode/test-electron spike decision + extension-test.sh stub (Gap 9) - graph preview deferral documented in lsp-decisions.md (Gap 1) - dead documentStore.ts removed (Gap 14) - cancellation behaviour tests (Gap 7) - grammar/lexer tokenization snapshot tests (Gap 8) - feature-level integration tests: hover, completion, definition, rename, rangeFormatting, compileIR (Gap 3) - manual test plan phases 1-5 (Gap 4) 74 tests, 15 suites, 5 snapshots; all pass. LSP server bundle: 298.0 KB, dep-cycle-audit clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- inline-const: refuse action when RHS identifier has multiple defs in symbol table (conservative scope-shadow safety) - concat→template: replace regex-based detection with AST walk + bracket-aware source extraction; bails on unrepresentable shapes - IR preview: prune irPreviewDocs map entries when preview tabs close - test cleanup: use stream.destroy() and dispose JSON-RPC reader/writer - lsp-decisions.md: document --forceExit root cause as upstream vscode-jsonrpc internal queue (test harness only; production LanguageClient unaffected) - 5 new codeActions tests covering shadow-refusal, range correctness, and nested-bracket safety - prettier auto-fixes across previously-touched files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… status snapshot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-ups Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ations Following TypeScript's lead: - SourceLocation gains optional length field - LexError, ParseError, TypeError, EmitError, CompileError all carry length with accurate values (token length, consumed chars for unterminated literals) - ConstStatement gains nameLoc pointing at the identifier token (not const keyword) - DestructuringConst gains nameLocs[] - one per bound name - LSP diagnostics.ts uses error.length for real-width squiggles - LSP symbolResolver.ts uses stmt.nameLoc / nameLocs[] directly - Deleted locateName() text-scan helper (no longer needed) - buildSymbolTable() no longer needs the source text argument Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add workflow/previewGraph LSP request handler + unit/integration tests - Add GraphModel mirror compile-time assignability check - Add webview-based graph preview panel (inline SVG layered layout, strict CSP) - Wire LogOutputChannel + workflow.showServerOutput command; await clientReady - Scaffold @vscode/test-electron suite (compiles; requires display server to run) - Update LSP/vscode READMEs, design decisions, manual tests, and review log
- lsp-decisions.md: remove resolved/superseded/historical entries; keep schema-dup and test-electron entries - lsp/README.md: explain --forceExit hang root cause inline - graphPreview.ts: add revisit note for real layout engine (elkjs/dagre) - lsp-plan.md, lsp.md: reflect implemented previewGraph + inline SVG renderer
builtinTasks.ts now imports schemas from builtinTaskSchemas.ts via a
taskSchema() spread helper — single source of truth, no sync spec needed.
- Export BUILTIN_TASK_SCHEMAS from builtinTaskSchemas.ts
- Replace inline name/inputSchema/outputSchema on all 32 TaskDefinitions
with ...taskSchema("name")
- Delete builtinTaskSchemas.spec.ts (equality now by construction)
- Remove resolved provisional entry from lsp-decisions.md
- Delete lsp-review-log.md (all entries resolved or closed) - Add lsp-future.md: dotted-name hover beyond head segment - lsp-plan.md: remove review-deferral log references - graphPreview.ts: style-src nonce (drop unsafe-inline); O(n²)→O(n) Map lookups for params, nodes, and groups
- lsp.md: add missing Graph preview row to Feature Inventory table - lsp-plan.md: documentStore.ts -> parsedDocument.ts - lsp-plan.md: add compileIR.ts + previewGraph.ts to lsp/src/features list - lsp-plan.md: remove irPreview.ts (IR preview is inline in extension.ts)
Fill gaps identified in a full coverage review outside the devcontainer: - codeActions.spec: inline-const rejected for synthetic (isSynthetic:true) consts and for consts with zero references; extract-to-const rejected for multi-line selections, 1-char selections, and full-RHS selections - diagnostics.spec: verify multiple diagnostics returned for deeply broken source - features.spec: hover null at declaration site and schema JSON in task hover; definition null at declaration site; references with unused const (includeDeclaration true/false); completion with unrecognized namespace, no-pos path, and same-name deduplication across scopes; semantic-token delta positions are monotonically non-decreasing - formatting.spec: add formatDocumentRange tests - overlapping range returns edits, beyond-document range returns none, parse-error source returns none - rename.spec: VALID_IDENT edge cases - empty string, single underscore, name with space, digit-leading name - phase3.spec -> signatureHelpAndInlayHints.spec: rename to descriptive name; add sig-help activeParameter clamping and comment-paren handling; add inlay-hints synthetic-const suppression, before-range exclusion, and switch/default_ body traversal
Add Phase 4c/4d unit tests in rename.spec.ts: - 'returns null on the const keyword' (non-renameable position) - 'returns null on a built-in task call name' (non-renameable position) Extend extension.test.ts with 11 new automated tests covering manual test phases 1a, 1c, 1d, 2a-2d, 3a, 3c (snippet structure), and 4a. These exercise the full LSP client-server-client round-trip using real file:// documents opened in the VS Code test host. Also fixes the Mocha ui setting (bdd -> tdd) in index.ts so suite() and test() resolve correctly. Remaining manual-only items: 2e (visual token colors), 3b (visual inlay hint rendering), 3c (snippet expansion UI), 5c (live-refresh webview), 5d (graph webview SVG), 5e (output channel trace).
…ring - dsl/ast.ts: add paramLoc to MapNode, FilterNode, ParallelMapNode, AttemptsNode.fallback so the parser can record the exact token position of each lambda parameter - dsl/parser.ts: capture lambda parameter token locations in parseArrowArg and thread them through parseArrowBody into parseMapBuiltin, parseFilterBuiltin, parseParallelMapBuiltin, parseAttemptsBuiltin - lsp/symbolResolver.ts: use expr.paramLoc ?? expr.loc when defining a lambda parameter so go-to-definition jumps to the param token rather than the start of the enclosing map/filter/parallelMap call - lsp/test/symbolResolver.spec.ts: regression test pinning the definition location to the actual param token position - lsp/test/grammarDrift.spec.ts: update keyword drift guard to check all keyword and constant sub-patterns (grammar was split into typed sub-patterns in the prior commit) - vscode/syntaxes/workflow.tmLanguage.json: rewrite grammar with TypeScript-compatible scope names for better theme coverage: storage.type.function, entity.name.function, variable.other.constant, variable.parameter, variable.other.property, support.class, etc.
Add segmentLocs to DottedNameExpr AST node so each dot-separated segment carries its source position. TypeChecker.collectPropertyRefs() walks the workflow and returns PropertyRef entries for every segment that resolves successfully against the schema (e.g. .stdout on the shell.exec result type). The LSP semantic tokens feature uses this to emit 'property' tokens, giving .stdout a distinct color from the variable that precedes it - matching the TypeScript experience.
All variable bindings in the workflow DSL are const (immutable), so mark them with the 'readonly' semantic token modifier. Dark Modern and other themes color 'variable.readonly' differently from plain 'variable', which is how TypeScript makes const identifiers appear darker than let/mutable variables.
Add formatType() to the DSL for rendering TypeInfo as TypeScript syntax
({ stdout: string; exitCode: integer } instead of 'object').
Add TypeChecker.collectSymbolTypes() which walks a workflow and returns
a Map<offset, TypeInfo> covering params, const bindings, destructuring
bindings, and lambda params (map/filter/parallelMap/attempts fallback).
Hover now renders:
const r: { stdout: string; stderr: string; exitCode: integer }
(parameter) repos: string[]
(parameter) repo: string <- lambda param in map()
instead of the old 'r -- constant' / 'a -- parameter' labels.
Add the setting to vscode/package.json (scope: language-overridable so it can be set per language in settings.json). The server caches the value via onDidChangeConfiguration - the language client already pushes the full 'workflow' config section on startup and on every change via synchronize.configurationSection.
Call connection.languages.inlayHint.refresh() (workspace/inlayHint/refresh) when workflow.inlayHints.enable changes. This tells VS Code to re-request inlay hints for open documents without requiring a reload.
Add findDefinitionAt() to symbolResolver. Hover now falls back to the definition table when the cursor is on the binding name itself (e.g. the 'r' in 'const r = shell.exec(...)'), not just on a reference to it.
Lambda params (map/filter/parallelMap/(repo) =>) now get inlay type
hints showing the inferred element type (e.g. ': string' after 'repo').
Also switched const-binding hints from the old JSON-schema-string
approach to collectSymbolTypes + formatType, so all inferred const
types get hints (not just task calls), with proper type labels
(e.g. '{ stdout: string; stderr: string; exitCode: integer }'
instead of 'object').
…, reflect implemented features
fbbff8a to
249e0bd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds a full language-server and VS Code extension stack for the workflow DSL, built in three new packages:
workflow-lspts/examples/workflow/lsp/workflow-vscodets/examples/workflow/vscode/workflow-engine(schema split)ts/examples/workflow/engine/builtinTaskSchemas.tsso LSP can import schemas without pulling in runtime depsFeatures shipped
DSL package changes (
workflow-dsl)lengthon every diagnostic so squiggles cover the right range.nameLocon declarations -ConstStatementandWorkflowDeclcarry a separatenameLoc(location of the name token, not the whole statement) for precise rename/hover targets.collectPropertyRefs/collectSymbolTypesonTypeChecker- new methods used by LSP hover and semantic tokens;collectPropertyRefsresult is cached inParsedDocumentso it does not re-run on every keystroke.set(offset, ...)calls guardoffset !== undefined.LSP server features
constbindings and lambda params, gated byworkflow.inlayHints.enable)loc.offset; inline-const guards negative LSP positions.workflow/compileIRrequest - returns IR JSON or structured error list; validatesparams?.uribefore useworkflow/previewGraphrequest - returns graph model for the webviewVS Code extension
.wffile association with TextMate grammar (workflow.tmLanguage.json)workflowscaffoldg.params/nodes/edges/groupsiterations guarded with?? [].Workflow: Toggle Inlay Hints)Design docs
lsp.md- architecture overview (replaces the separatelsp-decisions.mdADR log, which is removed in this PR)lsp-plan.md- implementation plan with phase trackinglsp-future.md- deferred items (semantic selection, refactor-to-parallel, etc.)dsl-v0.1-gap.md- G21 added: inferred return type + typed lambda parametersTest coverage
diagnostics,formatting,symbols,features(hover/completion/definition/references/semantic tokens/signature help)signatureHelpAndInlayHints,codeActions,rename,compileIR,previewGraph,graphShapegrammar(TextMate grammar smoke tests),grammarDrift(grammar/server consistency guard)cancellation,serverIntegration,server,symbolResolver@vscode/test-electron(Linux/Xvfb in CI)