10. The VS Code host

src/host/vscode/DesignEditorProvider.ts implements vscode.CustomEditorProvider<DesignDocument> for .design (PLT-1), with supportsMultipleEditorsPerDocument: true (a split view shows a second webvie…

Esta página está em inglês; a tradução está a caminho.

10.1 Custom editor provider#

src/host/vscode/DesignEditorProvider.ts implements vscode.CustomEditorProvider<DesignDocument> for *.design (PLT-1), with supportsMultipleEditorsPerDocument: true (a split view shows a second webview of the same document) and retainContextWhenHidden: false (ADR-10: a hidden tab costs nothing; when shown again the webview rebuilds the replica from a snapshot and restores its viewport and tool state from vscode.getState(), within the “re-show after hide” budget of §12).

Method Does
openCustomDocument(uri, {backupId}) Reads the file (or the backup when backupId is set, which carries the base file hash and the unsaved events), parses it tolerantly (FMT-001 §9), migrates an older designVersion in memory (saved on the next save), builds the DocumentState, creates the DesignController.
resolveCustomEditor(document, panel) Sets the webview options and the HTML with its CSP (§13), attaches the panel to the controller, sends init.
saveCustomDocument Asks every webview of the document to flush unsent text (flush), waits for the queue, serialises (tree + WAL, one event per line with a leading comma), writes. Records the written content hash so the watcher ignores its own write.
saveCustomDocumentAs Same to another URI; copies the assets folder.
revertCustomDocument Re-reads the file; drops unsaved groups.
backupCustomDocument Writes {baseHash, events} (only the unsaved events, not the whole document) to the backup destination; hot exit restores from it (PLT-5).
onDidChangeCustomDocument Fires one CustomDocumentEditEvent per commit group with label, undo, redo (§4.6).

“Open as Text” and “Open as Design” (PLT-6) reopen the URI with vscode.openWith.

10.2 Document lifecycle#

text
open ─► read + fold ─► controller ─► webview init (snapshot)
          │
          ├─ commits from any source ─► WAL in memory ─► deltas to webviews ─► dirty
          ├─ save ─► write tree + WAL ─► clean (consolidate when FMT-001's thresholds say so)
          ├─ file changed on disk ─► merge (§10.4)
          └─ close last editor ─► dispose controller, stop worker, unregister from bridge

10.3 The controller#

src/host/controller/DesignController.ts has no vscode import. It owns:

  • the authoritative DocumentState and the WAL of unsaved events;
  • the queue: commits from the webviews, the bridge, VS Code commands, the script worker and proposals are serialised in arrival order; a commit waits only for earlier commits, never for a running script (scripts run on drafts, §8.1);
  • the undo and redo groups;
  • the selection per webview and the “editor selection” the CLI sees (the most recently focused webview’s);
  • proposals (§10.7);
  • the script worker;
  • the webview protocol (§11.5).

It sees the outside through a Workbench interface (read and write files, show messages, open URIs, secrets, licence view, settings), which src/host/vscode, dev/ and the tests implement.

10.4 File watching and merging external changes#

A FileSystemWatcher on each open document and its assets folder. On change (PLT-8):

  1. Ignore it if the content hash equals our last write.
  2. Read and parse the new file. If its tree and WAL extend the content we last read (same tree, our WAL is a prefix after normalising the line order union merge can produce), theirs = the events we do not have.
  3. Otherwise (a checkout to another branch, a consolidation done elsewhere), take the new file as the new base, theirs = everything, and the controller’s unsaved groups are ours.
  4. rebase(base, theirs, ours) (§4.7); conflicts and dropped events are shown in a notification with “Show details” and in the history panel.
  5. If nothing was unsaved, the document stays clean; otherwise it stays dirty with ours on top.

Changes in the assets folder only invalidate decoded images and font faces with that hash.

10.5 Bridge server#

Started on activation when ud.cli.liveEditing is on (default). It registers in the bridge folder, answers ping for documents open in this window, and hands requests to the matching controller. The token is 32 random bytes per window, written only to the registry entry (§13).

10.6 Licensing client#

src/host/vscode/licensing/ follows Project Board’s client: the vendored Soft Forge Payments client for sign-in (src/third-party/licensing-client/), offline signature checks of the key, trial, grace and clock rules, key prefix UD (LIC-1 to LIC-6). The client produces a LicenceView { canEdit, plan, reason } that the registry gate (§3.3 step 3) asks.

The CLI’s view. Headless ud cannot read VS Code’s SecretStorage. When the licence state changes, the host writes a licence ticket next to the CLI shim (global storage, mode 0600): the signed key or session key itself plus the last server confirmation time. The CLI verifies the signature offline with the same public key and applies the same expiry and grace rules. No ticket, or an invalid one, means the Free plan: read tools and ud render work, mutations exit with code 4 and a message (LIC-5). Through the bridge, the host decides.

10.7 AI runner and proposals#

src/host/vscode/ai/ runs Claude Code (AI-1): claude --print --output-format stream-json --model <tier> with an MCP config that attaches ud mcp (AI-11), the skills, and an allowed-tool list (mcp__ui-designer__*, Read, Grep, Glob; AI-14). The child process gets UD_ACTOR (for example Claude (opus)) and UD_PROPOSAL=<id>.

A proposal is a forked draft of the document held by the controller (AI-5). Bridge requests that carry the proposal id run on it instead of on the document. The AI panel renders before and after from the base and the proposal and lists the proposal’s journal as a change list. Accept commits the proposal’s events as one group (rebasing over anything that happened meanwhile); Discard drops the draft. With “Apply directly”, requests run on the document and each turn is closed as one group (AI-4). Cancel kills the process and drops the proposal, so a failed or cancelled run leaves the document unchanged.

10.8 Language model tools#

The tools of §9.1 with lmTool: true are registered with vscode.lm.registerTool (AI-12). Their declarations in package.json are generated from the tool table (§15.3). Invocation runs the same handler in the host against the active document, signed with the chat participant’s name.