037 · Visual editor
A visual Markdown editor, with live code blocks and language servers (spec_visual_editor_037)
| Status | Approved (2026-10-08). Phase 1 implemented on visual-editor (2026-10-08): LanguageService, and completion, hover, problems and go to definition in the source editor, checked with real gopls in WebKit. Still to do in phase 1: the references panel (VE-34), and opening a definition outside the workspace read-only (a snackbar says where it is). Phase 2 implemented the same day: the visual editor in advanced mode, faithful to every Markdown file in the repository (VE-90), with CodeMirror code blocks and Mermaid; still to do in phase 2: off-screen code blocks without an editor (VE-25), tables’ column alignment shown (it’s kept in the file), and Visual at a search hit (Preview shows it). Phase 3 implemented the same day: Go, TypeScript, JavaScript and Python blocks on their servers (completion, problems, hover, definition in the block), checked in WebKit with real gopls and typescript-language-server; Python’s too with pyright (2026-10-09, once the client sent pyright the configuration notification it waits for). |
| Source | Phase 1: proto/blitz/v1/language.proto, apps/service/internal/server/language.go, pkg/engine/language.go, pkg/engine/lsp/documents.go (with manager.go), pkg/engine/tools/procgroup_windows.go; the page’s files/language.ts, files/languageUi.tsx, LanguageServers.tsx. Phase 2: the page’s files/visual/ (schema.ts, markdown.ts, codeBlock.ts, VisualEditor.tsx, visual.css) and files/EditorPane.tsx; the markdown filegroups that give the round-trip test the repository’s Markdown. Phase 3: pkg/engine/lsp/snippet.go (with documents.go), pkg/engine/language.go’s CodeBlock, and the page’s files/language.ts and files/visual/codeBlock.ts |
| Tests | pkg/engine/lsp/documents_test.go, pkg/engine/language_test.go, apps/service/internal/server/language_test.go, pkg/engine/tools/procgroup_windows_test.go (Windows only), apps/desktop/web/src/files/language.test.ts, apps/desktop/web/src/files/visual/markdown.test.ts (with testdata/edge.md); phase 3: pkg/engine/lsp/snippet_test.go, TestGoplsSnippets in pkg/engine/lsp/manager_test.go (BLITZ_TEST_GOPLS=1), apps/desktop/web/src/files/visual/codeBlock.test.ts |
| Depends on | spec_files_029 (the editor, previews, FIL-64 below), spec_filetools_006 FS-60–62 (the language servers the agent uses), spec_desktop_024 DSK-24 (what the page renders), spec_project_config_031 (trust), spec_service_021 SVC-01 |
Markdown files open in the Files editor as a read-only preview, and are changed in their source. This makes the preview the editor: you write in the rendered document (headings, lists, tables, links), and its code blocks are real code editors, with completion, problems and hover from the language’s server (gopls first). The same language servers come to the Files editor’s source view, so a .go file gets them too.
The owner’s brief (2026-10-08) asked for TipTap with Monaco code blocks, over a WebSocket bridge to gopls. Decided the same day, after looking at what Blitz has:
- Code blocks are CodeMirror 6, not Monaco. The Files editor is CodeMirror already (FIL-40): one editor, one theme, one set of keys. CodeMirror needs no web workers, no
evaland no extra fonts, so it works unchanged in the app’s web views (Chromium in the Electron app since v0.7.0; WebKit and WebKitGTK in the Wails app before), in Edge on Windows (spec_windows_036) and under VS Code’s content security policy. Monaco would add 3–5 MB, worker bundling and a second editor. - The editor reaches language servers through the service’s API, not a WebSocket. The service already runs a language server per workspace and language for the agent (FS-61), kills it with the workspace, and (on macOS and Linux) when the service dies. A new
LanguageService(FIL-64) puts the editor on the same servers. No WebSocket: the app’s web view refuses them, the VS Code proxy doesn’t carry them, and the service never listens on a port (SVC-01). No second gopls beside the agent’s.
What the brief asked, and where this spec answers it:
| Brief | Here |
|---|---|
| TipTap for the Markdown document | §3 (VE-01–VE-14) |
| Code blocks as real editors (React node views) | §4 (VE-20–VE-25), CodeMirror |
| Completion and diagnostics from gopls | §5–§6 (VE-30–VE-47) |
| A bridge from the page to gopls | §5, LanguageService over the existing proxy |
| Web workers working in WebKitGTK | None needed (CodeMirror) |
| Code edits reaching the document, so saving keeps them | VE-22 |
| gopls started, checked and stopped with the app, no orphans | VE-45, VE-46 (and a job object on Windows) |
| CSP allowing injected components | Nothing to allow: no iframes, workers or eval (VE-52) |
| Phase | What |
|---|---|
| 1 | LanguageService (FIL-64), and language intelligence in the Files editor’s source view: completion, hover, problems, go to definition, for files on disk (unsaved text included) |
| 2 | The visual editor for Markdown (TipTap): editing in the rendered document, faithful to the file’s Markdown; code blocks as CodeMirror editors with highlighting and word completion |
| 3 | Language intelligence in code blocks: each block a document of its own for its language’s server (Go first, then TypeScript and Python) |
Each phase ships on its own; phase 2 doesn’t need phase 1, and phase 3 needs both.
- VE-01 Owner’s decision, 2026-10-08. The visual editor is an advanced feature (spec_desktop_024 DSK-60a). In the simple window nothing changes: Markdown files open in Preview, read-only, with the Preview / Source switch (FIL-54). With Show advanced settings on, Markdown files (
.md,.markdown;.mdxkeeps Preview, since its JSX can’t be edited faithfully) open in Visual instead, the switch reads Visual / Source, and ⌘⇧V / Ctrl+Shift+V switches; FIL-54’s rules for which view a file opens in stay (at a line, or empty, it opens in Source). Visual is the document as Preview shows it (the document style of FIL-57, the user’s CSS included), and editable. Turning advanced off while a file is open in Visual shows it in Preview, its unsaved edits kept (VE-06). - VE-02 The editor is TipTap 3 (ProseMirror) in a React component (
@tiptap/react), loaded the first time a Markdown file opens (a chunk of its own, like Mermaid’s). Exact versions are pinned (DSK-99). - VE-03 It edits CommonMark with GitHub’s extensions (what the chat and Preview render, remark-gfm): headings 1–6, paragraphs, bold, italic,
strikethrough,inline code, links, images, bullet, numbered and task lists (nested), quotes, tables (alignment, header row), horizontal rules, hard breaks, code blocks (§4) and Mermaid diagrams (VE-24). - VE-04 Faithful to the file. Opening a file and saving it without a change writes the same bytes. Changing one part rewrites only that part: each top-level block keeps the source it was read from (its range in the file), and the serializer writes an untouched block’s source as it was, so list markers (
-or*), emphasis (*or_), heading style, table spacing, line endings and wrapping stay the author’s. A block that changed is written in one house style (-lists,*emphasis,**strong, ATX headings, fenced code with backticks, tables padded to their columns), and the blank lines around it are kept. - VE-05 What Visual can’t edit is kept, not lost: raw HTML, front matter (
---YAML at the top), footnotes, reference-style link definitions, and anything else the parser doesn’t map to the schema show as a source block: its Markdown in a small CodeMirror editor, labelled (“HTML”, “Front matter”, …), edited as text. Raw HTML is never rendered (DSK-24). Since 2026-10-12 math isn’t one of them: it’s drawn and edited in place (spec_files_029 FIL-67). - VE-06 The text model stays the tab’s (FIL-40–45): Visual edits produce the tab’s text (the serializer runs on each change, debounced to 150 ms, and at once before a save, a switch to Source or closing), so the dot for unsaved changes, ⌘S,
FILE_CHANGEDwith Overwrite / Reload, unsaved changes surviving a switch and asking on close all work as in Source. A file changed on disk while open reloads Visual as it reloads Source (FIL-33), keeping the cursor’s block when it can. - VE-06a 2026-10-08, the owner’s review. A line to type on at the end. When a document’s last block isn’t a paragraph (a code block, a diagram, a table, a list), the editor shows an empty line after it (TipTap’s trailing node), so text can follow it without switching to Source. It isn’t in the file: a new empty paragraph writes nothing (VE-04), so it’s written only once something is typed on it. ↓ at a code block’s last line and ⌘Enter reach it too (VE-21).
- VE-07 Undo and redo are the document’s (ProseMirror’s history), across text and code blocks (VE-22). Switching to Source and back starts a new history in the other view, as switching tabs does today. Loading the file (and loading it again, from disk or from Source) isn’t an edit: undo never goes past it (2026-10-09: one ⌘Z just after opening emptied the document, which was then written to the tab and could be saved).
- VE-08 Typing Markdown works as it would in the source:
#makes a heading,-/1./[ ]a list,>a quote,```+ Enter a code block,---+ Enter a rule,**bold**and`code`as they’re closed. ⌘B, ⌘I, ⌘E (code), ⌘⇧S (strike, TipTap’s), ⌘K (link), ⌘⌥1–6 (headings), ⌘⇧7 / ⌘⇧8 / ⌘⇧9 (numbered, bullet, task list); Ctrl on Windows and Linux. - VE-09 Revised 2026-10-08 (the owner’s review: a menu that shows only on a selection is too hidden). A toolbar is always shown, docked at the foot of the editor’s panel under the document (2026-10-08, the owner’s review: at the top it crowded the tabs and the file’s bar), the document scrolling above it, in one row that scrolls sideways when the panel is narrower (fading at an end with more; the table’s actions scroll into view as the cursor enters a table): headings 1–3; bold, italic, code, strike; bulleted, numbered and task lists; quote, code block, diagram, divider; link, image and table; and, while the cursor is in a table, its row and column actions. Right-click in the document opens the same actions as a menu (the table’s first, in a table). Both come from one list (
actionGroups), and both show which marks and blocks are in effect. / at the start of an empty line still opens a menu of blocks, filtered as you type. - VE-10 Links: ⌘K or the bar edits the URL and text. A relative path completes from the workspace’s files (FindFiles), and links behave as in Preview when clicked with ⌘ (Ctrl): FIL-56’s rules, in the editor’s tab. A plain click places the cursor.
- VE-11 Images aren’t loaded (DSK-24): an image is the box Preview shows (its alt text and path). Image… (toolbar, right-click) inserts one at the cursor from its address (a workspace path or a URL) and description, or edits the one selected. Pasting or dropping an image file is out of scope (VE-60).
- VE-12 Tables: every cell is ruled, rows and columns, in both themes (
--doc-table-rule, a light tint of the text colour, in Preview too); Table (toolbar, right-click) inserts a 3×3 table with a header row; Tab and Shift+Tab move between cells; the toolbar and the right-click menu add rows and columns and remove them or the table. (Setting a column’s alignment is to do; a file’s alignment is kept.) Cells hold inline content only, as GitHub’s tables do. - VE-13 Pasting: Markdown text is parsed as Markdown; HTML (from a browser or a word processor) is cleaned to what the schema holds (headings, emphasis, links, lists, tables, code), with scripts, styles, event handlers and unknown elements dropped; anything else is plain text. (⌘⇧V stays the switch to Source, FIL-54, rather than a plain paste.) Copy puts Markdown and the rendered HTML on the clipboard (DSK-72a’s two forms).
- VE-15 Styled as Blitz, not as TipTap. TipTap is headless, but its extensions, ProseMirror and their menus come with styles of their own (selection outlines, gap cursors, table resize handles, placeholder text, drop cursors, floating menus); none of them reach the page. The editor imports no package CSS: every rule is the app’s, in a stylesheet of its own over the M3 tokens (
m3.css) and the document’s--doc-*variables (files/document.css), so the document looks exactly as Preview did and follows the theme (light, dark, compact density) and the user’s document CSS (FIL-57). The / menu, the selection bar, the link editor, the table menu and the language menu are the app’s own controls (ui/controls.tsx: menus, buttons, fields), not TipTap’s. Focus, selection, the cursor in an empty block, a selected node (an image, a rule, a code block), table cell selection and drop positions are drawn in the accent colour, as the rest of the app draws them. - VE-16 A design pass before it ships. Each block kind, the menus and the code blocks are screenshotted with the fake service (
?fake) in WebKit, in light and dark, against Preview’s rendering of the same document; differences are fixed, not accepted. The pass is part of phase 2, with time set aside for it. - VE-14 Find (FIL-72) works in Visual as in Preview: the find bar’s matches marked in the document, Enter to the next. Workspace search hits open Visual at the hit (SRCH-43). Export as PDF (DSK-93) prints the document from a read-only render of the tab’s text, as it does today.
- VE-20 A fenced code block is a CodeMirror 6 editor inside the document (a TipTap node view): the Files editor’s setup (FIL-40–42: highlighting by the fence’s language, from
@codemirror/language-data, bracket matching, indentation, word completion), without line numbers by default, growing with its content. Its header shows the language as a button (built 2026-10-08): it opens a picker of the usual languages, searched as you type, which also takes any name typed (the fence’s info string); choosing Mermaid makes the block a diagram (VE-24). And Copy. A new code block (toolbar, right-click, /) takes the cursor, ready to type. The toolbar’s inline-code action is labelled Inline code, apart from Code block. - VE-21 The cursor moves in and out as if the block were text: Up at its first line or Left at its start leaves it for the block above, Down and Right likewise below; Backspace in an empty block turns it into a paragraph; ⌘Enter (Ctrl+Enter) leaves it for a new paragraph after it. Tab indents inside it.
- VE-22 Edits reach the document at once. Each change in the CodeMirror editor is dispatched to ProseMirror as a transaction in the same event (no debounce), replacing the block’s text by the changed range; a change to the block from ProseMirror (undo, a paste over it, a collaborative replace in future) is applied to the CodeMirror editor. One undo history covers both (the code editor has none of its own), so ⌘Z after typing in a block undoes the typing. The serializer reads the block’s text from the document, so a save always has what’s on screen. Every editor’s tooltips (completion, hover, problems) go in one layer fixed over the window (
tooltipLayerinfiles/codemirror.ts), which never makes the page taller, and the window never scrolls as a page (2026-10-09: each code block’s tooltip box, the window’s height, stacked below the app, and a focus or scrollIntoView then scrolled the whole window into the empty space, leaving it unusable). - VE-23 An indented code block (four spaces) shows as a code block, and is written back indented unless changed (VE-04); a changed one is written fenced.
- VE-24 A
```mermaidblock shows its diagram (FIL-55), with Edit to show its source in a code block above the diagram, redrawn as you type (debounced to 300 ms); a source that doesn’t parse shows why, as today. - VE-25 Many blocks stay cheap: a block off screen keeps its text but not its editor (created as it scrolls into view), so a document with 200 code blocks opens as fast as Preview did.
A new Connect service, in a new language.proto beside the other protos. Every request names its workspace (SVC-10). The page’s transport carries unary calls and server streams, not two-way streams (FIL-64), so a document’s changes are calls and its diagnostics are a stream.
- VE-30 Documents.
OpenDocumentopens a document for the editor and returns its ID, its language and the server’s state:ready,starting,missing(the command isn’t installed: what to install, VE-44),disabled(configuration),untrusted(VE-43) ornone(no server for the language). A document is a workspace file (path, relative) with its current text (unsaved edits included), or a code block (VE-50).ChangeDocumentsends the whole new text with a version (the editor calls it debounced to 150 ms);CloseDocumentends it. Calls with an older version than the document’s are answered for the newer one or refused asSTALE. - VE-31 Leases. Documents belong to the client that opened them, and a client that goes away (the window closed, the page reloaded, a crash) mustn’t leave them open: each document lasts while its client calls or watches within 2 minutes (
KeepDocuments, every 30 s, from the page), then closes. At most 64 editor documents per workspace; opening more closes the least recently used. - VE-32 Completion.
Complete(document, version, line, column, trigger)returns up to 200 items: label, kind, detail, documentation (Markdown), the text to insert or the edit to make (a range and text; snippets as LSP snippets), the extra edits some servers add (an import for gopls), whether the list is incomplete (ask again as typing narrows it), and the server’s sort and filter text. The editor shows them with CodeMirror’s autocompletion beside FIL-42’s words, the server’s first; documentation shows beside the list, rendered as Markdown. Tab (2026-10-09, in files and code blocks): accepts an open completion; at a line’s start or over a selection it indents, and after a blank it inserts an indent; elsewhere, after a word or a., it asks for completions and inserts nothing. Shift-Tab outdents. Afterinitializedthe client sendsworkspace/didChangeConfiguration(empty settings), and answersworkspace/configurationwith a null for each item: pyright answers nothing until it hears the first. - VE-33 Hover.
Hover(document, version, line, column)returns Markdown and the range it’s about; the editor shows it after the pointer rests 400 ms (or ⌘I, Ctrl+I), rendered with the chat’s renderer. The hover is as wide as its content up tomin(760px, 70vw)and at most 45% of the window tall, so it fits above or below the word; code in it keeps its lines and scrolls sideways (2026-10-12, #18: it was a word or two a line in WebKit, its box shrunk inside the zero-size tooltip layer; the layer now covers the window, each editor’s box at its corner, and on glass tooltips get the menus’ blur and solid surface). - VE-34 Definition and references.
DefinitionandReferencesreturn locations (path, line, column, the line’s text, at most 100), as the agent’s tool does. ⌘-click (Ctrl-click) or F12 goes to a definition: in the same file, the cursor moves; in another workspace file, it opens in the editor at the line; outside the workspace (the standard library, the module cache), it opens read-only in a tab of its own when the service may read it, else the location is shown. ⇧F12 lists references in a panel below the editor. - VE-35 Diagnostics.
WatchDiagnostics(workspace)streams each open editor document’s diagnostics as the server publishes them: document, version, and each diagnostic’s range, severity, message, source and code. The editor underlines them (@codemirror/lint: errors, warnings, information and hints), with the message on hover and a mark in the gutter; the file’s tab and the status bar count errors and warnings. The stream keeps a client’s documents alive (VE-31). - VE-36 Positions are lines and columns of characters (1-based), as the agent’s tool uses (FS-60); the engine converts to and from the server’s UTF-16 offsets.
- VE-37 Errors carry a reason (
NO_SERVER,SERVER_MISSING,UNTRUSTED,SERVER_FAILED,UNKNOWN_DOCUMENT,STALE). The editor shows none of them as an alert: a document without a server gets FIL-42’s completion and a quiet note in the status bar (“No Go language server: install gopls”), and a failure says why there once. A language server’s failure (SERVER_FAILED) is never Connect’sUnavailable, which says the Blitz service itself is gone: the page takes only that, with no reason orSHUTTING_DOWN, as the service lost, so a hover the server can’t answer leaves the editor open.
- VE-40 The editor’s documents and the agent’s share each workspace’s servers (
lsp.Manager, inpkg/engine/lsp): one gopls per workspace, as now.initializeadds what the editor uses: completion (snippets, documentation in Markdown, resolving details, the extra edits), and the diagnostics’ code and related information. - VE-41 Unsaved text. A document the editor has open is the server’s copy of that file while it’s open: its text is the editor’s (unsaved edits included), and the engine’s sync from disk (the agent’s tool, the diagnostics after its edits) leaves it alone, so the agent’s diagnostics for a file you’re editing are for what you see. When the editor closes it, the next sync is from disk again.
- VE-42 Open documents. The editor’s documents don’t count towards the 32 documents the engine keeps open for the agent (FS-61’s cap closes only the agent’s own), so a file you’re editing is never closed under you.
- VE-43 Trust. Language servers run the project’s code (gopls loads its packages, and
go listruns its build), so the editor’s servers start only in a workspace whose project settings are trusted (spec_project_config_031): its project settings are trusted, or it has none needing trust (a plain folder); while they’re new, changed or declined,OpenDocumentanswersuntrusted, and the editor offers Trust this project (FIL-61). The agent’slsptool is unchanged (§10). - VE-44 A missing server is said, with what to install (
go install golang.org/x/tools/gopls@latest,npm i -g typescript-language-server typescript,npm i -g pyright,rustup component add rust-analyzer). Installing it for you is FIL-62, a later step. - VE-45 Health. A server that exits is started again on the next request (after 1 minute when it failed to start, as FS-61), and its documents are opened in it again, so the editor needs no action; a server that stops answering (no reply to a request in 30 s, three times running) is stopped and started again.
GetLanguageStatus(workspace)lists the servers: language, command, state, since when, and the last error, shown in Settings › Workspaces › Language servers with Restart. - VE-46 No orphans. A workspace’s servers stop when it closes and when the service stops (FS-62), and when the service dies they die with it: on macOS and Linux by the process group and the parent-death guard (as today); on Windows, by a job object (
KILL_ON_JOB_CLOSE) that holds every process a language server starts (gopls’sgo listincluded), so nothing outlives a crash there either. Tests start a fake server, kill the service and check no process is left.
- VE-50 A block is a document. A code block in a language with a server is opened as a document of its own (
OpenDocumentwith the Markdown file’s path, aCodeBlockwith the block’s language as an extension and an ID its window gives it, and its text), kept open while its editor is (built: every block has one until VE-25; a changed language opens it again). The engine gives it a file of its own the server can read without it existing on disk (an overlay):blitz-snippets/<hash>/snippet.<ext>under the workspace’s root, so it sees the workspace’s modules and packages but isn’t part of any package beside the Markdown file. Nothing is written to disk. (Built 2026-10-08: not under.blitz, as first planned: gopls gives files in a folder whose name starts with.or_no diagnostics.) (Built 2026-10-12: a Go block in a workspace nogo.modorgo.workcovers, its module in a folder such asbackend/, is said to be in a folder of the engine’s own outside the workspace, made for it and removed when the servers stop: gopls lists a file outside a module from its folder, which must exist. gopls’s “No packages found for open file” about a block is dropped; its errors still show.) - VE-51 Making a block a whole file. Most blocks are fragments. Before the server sees one, the engine adds what the language needs, hidden from the editor, and maps positions across it: for Go,
package snippetwhen the block has nopackageline, and the block’s statements insidefunc _() { … }when it doesn’t start with a declaration (package,import,func,type,var,const, or a comment before one); TypeScript and Python blocks are files as they are. Diagnostics on the added lines are dropped, and “declared and not used” in a wrapped Go block is shown as a hint, as is “undefined: fmt” for a name the block uses as a package (fmt.Println): a fragment’s imports are left out. A definition in the block comes back with an empty path, and moves the cursor in the block. - VE-52 Nothing else to allow. Code blocks are DOM inside the document, not iframes, and CodeMirror needs no workers or
eval, so the desktop page, the Windows tray’s page and VS Code’s policy need no change. - VE-53 Languages with blocks in phase 3: Go, then TypeScript and JavaScript, then Python. Rust needs its block inside a Cargo project, so it keeps FIL-42’s completion.
- VE-55 New packages, pinned (DSK-99) and in THIRD_PARTY_NOTICES (all MIT):
@tiptap/react,@tiptap/pm,@tiptap/starter-kitand the table, task list and link extensions; a Markdown parser that gives each block its source range (markdown-itwith its source maps, or mdast through remark, which Preview has already; decided in phase 2) and@codemirror/lint. Together about 300 kB, in the visual editor’s chunk, loaded with the first Markdown file. - VE-56 Platforms: the app on macOS, Linux and Windows (Electron’s Chromium since v0.7.0; WebKit and WebKitGTK before), the Windows tray’s page in Edge, and a browser in development. The VS Code extension shows only the chat (VSC-04), so it gets none of this until it shows files.
- VE-90 Round trip. Every Markdown file in this repository (
docs/, the specs, READMEs,.agents/) is parsed and serialized without a change and compared byte for byte with the file (VE-04), in Vitest, without a DOM. A set of edits (a word in a paragraph, a list item added, a table cell, a code block’s text) changes only the lines it should. - VE-91 Pasted HTML: scripts, handlers, styles and unknown elements are dropped (VE-13); raw HTML in a file stays a source block (VE-05).
- VE-92 The service and engine:
LanguageServiceagainst the fake language server (lsptest, which gains completion), its leases (a client gone closes its documents), versions (STALE), unsaved text over disk syncs, the editor’s documents outside the agent’s cap, Go block wrapping and position mapping, and the trust gate; and against real gopls whenBLITZ_TEST_GOPLS=1(asTestGopls). - VE-93 No orphans (VE-46): a fake server that starts a child, the service killed, nothing left (Linux, macOS, Windows).
- VE-94 The page, by hand (until Playwright,
.agents/TODO.md): editing each block kind, code blocks’ keys and undo, completion and problems in a Go file and a Go block, in the app on macOS and Linux and the tray’s page on Windows.
- Preview stays for the simple window; Visual is advanced (VE-01): simple shows Preview and Source, advanced shows Visual and Source.
- The trust gate is the editor’s (VE-43). The agent’s
lsptool is unchanged; gating it too is a separate decision. - Phases in order: the language service first, then the visual editor, then language servers in code blocks.
- Installing a missing server (FIL-62) waits: VE-44 only says what to install.
- TipTap is styled as the app (VE-15), with a design pass (VE-16).
- VE-60 Images pasted or dropped into a document (they’d be saved beside it), collaborative editing, comments, and Markdown in the chat’s composer.
- VE-61 Code actions (quick fixes, organize imports), rename, formatting with the server, signature help and semantic highlighting: later, once the basics are used.