ADR 0046: VSCode Live Workspace Sidebar

Status

Accepted | Implemented (2026-03-02)

Context

Beamtalk is an interactive-first language: actors persist in running workspaces, bindings accumulate across REPL sessions, and code is reloaded live without restarts. This live environment is the defining development experience.

Today the VSCode extension is a thin LSP client wrapper. It provides editing intelligence (completions, hover, diagnostics, go-to-definition) but has no awareness of running workspaces. A developer using the REPL in a terminal has no visibility into workspace state from within their editor — they cannot see what actors are running, what bindings exist, or what the Transcript is streaming.

The WebSocket infrastructure (ADR 0017) already supports everything needed:

The gap is purely in the editor: nothing consumes this rich runtime data to surface it where developers work.

What We Are Not Building Here

ADR 0017 Phase 3 describes a Phoenix LiveView browser-based IDE with full workspace panes. That remains planned. This ADR describes a VSCode sidebar that:

  1. Delivers the live development experience now, in the editor developers already use
  2. Validates the information architecture (which panels, what data, what interactions) before committing to the larger browser UI project
  3. Shares the same WebSocket protocol — the browser UI is additive, not a replacement

ADR 0024 Tier 3 describes live workspace augmentation for LSP completions (type information from the running workspace). That is a separate concern; this ADR is about developer visibility and inspection, not completion quality. Tier 3 uses its own independent workspace WebSocket connection; the sidebar and Tier 3 both speak the same workspace WebSocket protocol but maintain separate, fully autonomous connections.

Constraints

Decision

The VSCode extension gains a Beamtalk Workspace sidebar container with two views (Phase 1), using a hybrid TreeView + WebviewView architecture, connected via a direct WebSocket from the extension host.

Views

1. Workspace Explorer (TreeView)

A TreeDataProvider showing the live workspace state as a native VSCode tree:

▾ WORKSPACE                    [● Connected]
  ▾ Bindings
      counter : Counter         [inspect]
      result  : 42
  ▾ Actors  (3 running)
    ▾ Counter  <0.234.0>        [inspect] [kill]
        value : 0
        spawned: 10:42:31
      Timer    <0.235.0>
      Logger   <0.240.0>
  ▾ Classes  (12 loaded)
      Counter
      Timer
      Logger
      ...

2. Transcript (WebviewView)

A streaming text view in the sidebar showing live Transcript output pushed from the workspace. Uses a WebviewViewProvider (sidebar-resident, not a floating panel) with a lightweight HTML view — no framework required, just a scrolling <pre> with auto-scroll, clear button, and a max-lines cap (e.g., 10,000 lines, matching the workspace's ring buffer). The cap is a safety requirement — a runaway actor writing to Transcript could otherwise freeze the webview with unbounded DOM growth.

Transcript is workspace-wide (shared across all sessions), matching the Smalltalk Transcript model.

3. Connection lifecycle

The extension owns the REPL session. There is no support for connecting to sessions started outside VSCode — if a user runs beamtalk repl in an external terminal, bindings from that session are not shown. Actors and classes are workspace-wide and always visible regardless.

Corner case — manual beamtalk repl in a VSCode integrated terminal: If a user opens a VSCode integrated terminal and runs beamtalk repl themselves (rather than using the "Start REPL" command), the sidebar sees the workspace (actors, classes) but not their bindings. This is intentional: the extension doesn't know the session ID. A future expansion could monitor the active terminal's output stream for the [beamtalk] session: <id> line and adopt the session automatically — the Python extension does this to detect which conda/venv environment a user activates in a terminal, and the Azure CLI extension similarly detects login completions. This would close the gap for power users who prefer manual terminal control without changing the default ownership model.

The extension host manages a single WorkspaceClient per VSCode window:

On "Start REPL" command (or auto-start on .bt file open):
  1. Read beamtalk.toml to find project root
  2. Run `beamtalk repl` in a new integrated terminal
  3. REPL prints session ID to stdout on connect (e.g. "[beamtalk] session: abc123")
  4. Extension captures session ID from terminal output
  5. Connect WebSocket to workspace, authenticate, subscribe to push channels
  6. Store session ID — used for bindings queries

On disconnect / workspace idle-timeout:
  → Show disconnected state in tree
  → Offer "Restart REPL" action

On terminal closed by user:
  → Sidebar shows disconnected state, session bindings cleared

Workspace persistence: a Beamtalk workspace (BEAM node) survives REPL disconnects (ADR 0004). If the extension reconnects after a terminal close, actors and classes persist. Bindings from the previous session are gone (ephemeral by design), and the new session starts empty — this is correct and expected.

Connection Architecture: Direct WebSocket (not LSP-proxied)

The extension host opens the WebSocket connection directly, independent of the LSP server:

VSCode Extension Host
  ├── LanguageClient  →  beamtalk-lsp (stdio)      [editing intelligence]
  └── WorkspaceClient →  ws://127.0.0.1:{port}/ws   [live runtime state]

The LSP connection and workspace connection are independent. The workspace sidebar works even if the LSP is restarting, and vice versa.

Interaction with ADR 0024 Tier 3: The LSP opens its own independent WebSocket connection to the workspace for Tier 3. The two connections have different purposes and are fully autonomous:

VSCode Extension Host
  ├── LanguageClient  →  beamtalk-lsp (stdio)        [editing intelligence]
  └── WorkspaceClient →  ws://127.0.0.1:{port}/ws    [sidebar: session, bindings, actors, transcript]

beamtalk-lsp (Rust)
  └── WorkspaceTypeClient → ws://127.0.0.1:{port}/ws [Tier 3: class-loaded events, class hierarchy]

The LSP's connection is read-only and minimal — it subscribes to class-loaded push events, queries classes and class metadata on connect, and maintains an updated in-memory class hierarchy for completions. No REPL session is needed. The LSP discovers the workspace via --print-sysroot (BT-1010). This keeps the LSP autonomous and independent of the extension host's connection lifecycle.

Refresh Strategy

Most updates are event-driven via existing push channels. The extension owns the REPL terminal, so it can observe eval completion directly (terminal output parsing or eval op response). The only missing push event is class-loaded — see Protocol Changes below.

Protocol Changes Required

One small addition to the existing protocol:

class-loaded push event: When any session loads, reloads, or eval-defines a class, the workspace broadcasts a push event to all WebSocket subscribers — analogous to the existing actors channel spawned/stopped events. This lets the sidebar refresh the Classes section without polling.

No bindings session param is needed: the extension owns the REPL session and queries its own session's bindings directly. The complete op's session param pattern already exists and works the same way.

Phase 2 addition — methods(className) op (beamtalk_ws_handler.erl):

Information Architecture (Informing ADR-0017 Phase 3)

This sidebar defines the panel structure that ADR 0017 Phase 3 should implement in the browser:

PanelData SourceUpdate Trigger
Workspace Explorerbindings, actors, classes opspush events + eval completion
Transcripttranscript push channelserver push
Inspectorinspect opon demand (expand / click)
(future) Test Resultstest/test-all opson demand

The browser IDE should implement the same panels with the same data sources.

Prior Art

Jupyter VSCode Extension (Variables Panel)

Jupyter uses a React-based WebviewPanel for the Variables panel (legacy) and a native TreeView via NotebookVariableProvider for the newer debugger-integrated view. The key lesson: TreeView integrates cleanly with native VSCode UI but can only render label+icon; WebviewPanel provides full rendering freedom at the cost of iframe overhead. Jupyter ended up with both: tree for the debugger, webview for the rich data viewer. We adopt the same hybrid split — tree for structured state, webview for the streaming Transcript.

The Jupyter ownership model is the exact parallel: the extension starts the kernel and therefore always knows the session. We adopt this directly — the extension starts beamtalk repl in an integrated terminal and owns the session ID. There is no support for externally-started sessions, just as Jupyter's variables panel only reflects the kernel it started. Jupyter triggers variable refresh on cell execution completion, panel activation, and kernel restart — no polling. We adopt the same.

Docker / Kubernetes Extensions (TreeView polling)

These extensions poll every few seconds with a configurable interval, but only while the view is visible (guarded by onDidChangeVisibility). We avoid polling entirely by using the workspace's existing push channels — the BEAM already pushes actor lifecycle events and Transcript output, so polling is unnecessary.

DAP (Debug Adapter Protocol)

The Erlang LS and ElixirLS extensions use DAP for variable inspection during breakpoint debugging. DAP's scopesvariables → recursive variablesReference chain maps well to tree exploration. We do not use DAP here — Beamtalk's live workspace model is not a debugger (no breakpoints, no stepping, no paused execution). The workspace is always running; inspection is non-intrusive. DAP would be an impedance mismatch.

Pharo VSCode Extension

The Pharo extension connects to a live Pharo image via TCP socket and uses DAP for variable inspection, but provides no image browser UI. The full Smalltalk browser experience is deferred. This confirms the prior art gap: nobody has yet built a live Smalltalk-style workspace browser in VSCode. We can be the first.

Observer / LiveDashboard (Elixir)

The Elixir community uses Observer (standalone Wx GUI) or LiveDashboard (browser) for live OTP inspection. Neither is integrated into the editor. The absence is a noted gap in the community. Beamtalk's sidebar fills this gap — live actor inspection without leaving the editor.

User Impact

Newcomer (from Python/JS/Ruby)

Smalltalk Developer

Erlang/BEAM Developer

Production Operator

Tooling Developer (ADR 0024 Tier 3)

Steelman Analysis

Option A: Hybrid TreeView + WebviewView (Recommended)

Option B: Full WebviewPanel

Option C: LSP-Proxied WebSocket

Option D: Do Nothing (Status Quo)

Tension Points

Alternatives Considered

Full WebviewPanel for Everything

Show all four panels (explorer, transcript, inspector, classes) as a single React webview, like the browser workspace UI.

Rejected because:

LSP-Proxied Connection

Route all workspace communication through the LSP server: extension → LSP → WebSocket → workspace.

Rejected because:

Debug Adapter Protocol (DAP)

Implement a DAP server backed by the workspace, exposing bindings and actor state as DAP variables.

Rejected because:

Polling TreeView (no push)

Connect to the workspace, poll bindings/actors/classes on a configurable interval.

Rejected because:

Do Nothing (Status Quo)

Rely on the terminal REPL for all workspace inspection. The REPL already supports actors, bindings, inspect, classes, and Transcript output.

Rejected because:

Consequences

Positive

Negative

Neutral

Implementation

Phase 0: Protocol Prerequisites (S effort)

One runtime-side change:

  1. class-loaded push event (beamtalk_repl_server.erl, beamtalk_ws_handler.erl)

    • Broadcast {"type":"push","channel":"classes","event":"loaded","data":{"class":"Counter"}} to all WebSocket subscribers when any session loads, reloads, or eval-defines a class — same structured format as the existing actors channel push events
  2. Session ID on REPL startup (beamtalk-cli/src/commands/repl.rs)

    • REPL prints session ID to stdout on connect, e.g. [beamtalk] session: abc123
    • Extension parses this from terminal output to establish session ownership

Phase 1: Connection + Workspace Explorer + Transcript (M effort)

  1. WorkspaceClient class (editors/vscode/src/workspaceClient.ts)

    • WebSocket connection management (connect, auth, reconnect with backoff)
    • bindings(sessionId), actors(), classes(), inspect(pid), sessions() op wrappers
    • Push channel subscription (transcript, actors, classes) — classes is the channel name; events within it are loaded (and future: reloaded, removed)
    • Session lifecycle (clone, close)
  2. WorkspaceTreeDataProvider (editors/vscode/src/workspaceTreeView.ts)

    • TreeDataProvider<WorkspaceNode> implementation
    • Nodes: ConnectedRoot / DisconnectedRoot / BindingsSection / ActorSection / ClassesSection / BindingItem / ActorItem / ClassItem
    • onDidChangeTreeData wired to push events
    • Context menus: kill, inspect, reload via package.json contributions
  3. TranscriptViewProvider (editors/vscode/src/transcriptView.ts)

    • WebviewViewProvider for sidebar Transcript
    • Auto-scrolling <pre>, clear button, max-lines cap (10,000 lines)
    • Wired to transcript push channel from WorkspaceClient
  4. Auto-connect (editors/vscode/src/extension.ts)

    • File watcher on ~/.beamtalk/workspaces/*/port
    • Derive workspace ID from beamtalk.toml project root hash
    • Connect on port file appearance; disconnect on file removal
  5. package.json additions

    • viewsContainers: beamtalk-workspace sidebar container with icon
    • views: beamtalk.workspaceExplorer, beamtalk.transcript
    • menus/view/item/context: kill, inspect, reload commands
    • configuration: beamtalk.workspace.autoConnect (default: true)

Phase 2: Method Browser + Rich Inspector (M effort)

Method browser (TreeView extension):

Rich inspector (WebviewPanel):

Files to Create/Modify

FileChange
editors/vscode/src/workspaceClient.tsNew — WebSocket client
editors/vscode/src/workspaceTreeView.tsNew — TreeDataProvider
editors/vscode/src/transcriptView.tsNew — WebviewViewProvider
editors/vscode/src/extension.tsRegister views, wire auto-connect
editors/vscode/package.jsonView containers, commands, config

References