Back to Fresh

Dev Container Spec — Gap Analysis

docs/internal/DEVCONTAINER_SPEC_GAP_ANALYSIS.md

0.3.216.6 KB
Original Source

Dev Container Spec — Gap Analysis

Comparison between the user-provided "Dev Container Lifecycle and UX Flow" specification (VS Code–style) and Fresh's current implementation as of branch claude/add-remote-indicator-WFOVB.

Companion docs:

  • AUTHORITY_DESIGN.md — the authority abstraction that underpins Fresh's container support.
  • DEVCONTAINER_PLUGIN_DESIGN.md — the plugin that owns dev-container detection and lifecycle.

Executive summary

Fresh already ships a working container workflow: a TypeScript plugin (plugins/devcontainer.ts, ~1100 lines) parses .devcontainer/devcontainer.json, drives devcontainer up on the host via editor.spawnHostProcess, and installs a container-scoped authority via editor.setAuthority({...}). The core contributes an Authority slot, a DockerExecSpawner, a destructive-restart transition flow, and the status-bar pieces (most recently a clickable {remote} indicator with a context-aware popup).

What's missing, relative to the spec, is primarily UX surface around the build lifecycle:

  • no visible Image-Pull/Build/Start/Post-Init state machine,
  • no live log streaming for devcontainer up,
  • no cancel-in-flight,
  • no retry-on-failure action,
  • no port-forwarding detection,
  • no automatic install of customizations.*.extensions.

A handful of items are intentional architectural divergences rather than gaps:

  • Fresh does not inject a headless editor server into the container (spec §5); it uses docker exec to run tools inside the container while the editor UI stays on the host.
  • Paths are not translated; the workspace is bind-mounted so host and container paths coincide.
  • Core stays free of per-backend knowledge (per AUTHORITY_DESIGN.md principles 3 & 4); orchestration lives in the plugin and the third-party devcontainer CLI.

The rest of this document walks every spec bullet and records status, evidence, and (where applicable) a suggested change.


Legend

  • ✅ Implemented — matches the spec's intent, even if wording differs.
  • ⚠️ Partial — some of the behavior is present; gaps listed inline.
  • ❌ Missing — not implemented.
  • ↔ Divergent by design — not implemented, and the design doc says it shouldn't be. Listed for completeness.

§1. Remote Authority & UI Entry Point

Spec bulletStatusNotes
Persistent UI element in bottom-left status barStatusBarElement::RemoteIndicator (added on this branch). Element is opt-in via config; placement at far-left is achieved by putting {remote} first in status_bar.left.
Click opens Quick Pick / Command Palette menuClick on {remote}Action::ShowRemoteIndicatorMenushow_remote_indicator_popup. Popup is anchored to the indicator's x-column via PopupPosition::AboveStatusBarAt.
Menu options (Local): "Reopen in Container", "Configure Dev Container"⚠️"Reopen in Container" offered when a .devcontainer/devcontainer.json is detected; it dispatches Action::PluginAction("devcontainer_attach"). "Configure Dev Container" is approximated by "Open Dev Container Config" which calls the plugin's devcontainer_open_config. Gap: when no config exists, spec implies a scaffold flow; Fresh shows a single disabled "No dev container config detected" row.
Menu options (Connecting/Building): "Show Logs", "Cancel Startup"Fresh's core has no Connecting/Building state — setAuthority is fire-and-forget from the plugin, and devcontainer up output is buffered, not streamed. Both actions would need log-streaming and process-tracking infrastructure that doesn't exist yet.
Menu options (In-Container): "Reopen Locally", "Rebuild Container", "Show Container Logs"⚠️"Reopen Locally" → clear_authority(). "Rebuild Container" → Action::PluginAction("devcontainer_rebuild"). Gap: "Show Container Logs" is not implemented; the popup offers "Show Container Info" (the existing devcontainer_show_info panel) instead.

Recommended follow-ups for §1

  • Add "Create .devcontainer/devcontainer.json" scaffolding command that writes a minimal template and opens it for editing.
  • Add "Show Container Logs" action that tails docker logs <id> through a streaming variant of spawnHostProcess (see §4 follow-up).

§2. Trigger & Initial Detection

Spec bulletStatusNotes
Detect .devcontainer/devcontainer.json or .devcontainer.json on workspace openplugins/devcontainer.ts::findConfig() checks .devcontainer/devcontainer.json, then .devcontainer.json, then .devcontainer/<subfolder>/devcontainer.json in priority order.
Non-blocking toast prompt when Local⚠️Implemented as editor.showActionPopup({...}), not a toast. The popup is modal-ish (keyboard capture) but non-blocking wrt. editing; functionally similar. Wording gap: spec says "Folder contains a Dev Container configuration. Reopen in container?"; plugin says "Attach to dev container '<name>'?" with [Attach] / [Not now].
Actions: "[Reopen in Container]" / "[Ignore]"⚠️Labels differ ("Attach" / "Not now"). Decision is persisted per-workspace in plugin global state so it isn't re-prompted on reopen — a desirable behavior the spec doesn't mention but is worth preserving.

Recommended follow-ups for §2

  • Align copy: rename plugin labels to "Reopen in Container" / "Ignore" to match spec and VS Code wording. (Pure i18n change.)
  • Keep the per-workspace one-shot dismissal behavior.

§3. Transition & Initialization

Spec bulletStatusNotes
Window reload / disposal of local watchers, LSPsPer AUTHORITY_DESIGN.md principle 7, every authority transition drops and rebuilds the entire Editor. Terminals, LSPs, watchers, buffers, plugin state are all recreated against the new authority.
UI transitions to "Connecting" overlay / splashOnly a single editor.setStatus("Rebuilding dev container...") status-bar message; the editor remains fully interactive during devcontainer up. No overlay, no splash, no state change visible in the remote indicator itself.
Connection Agent background process to Docker⚠️Not a long-lived "agent" — the plugin invokes devcontainer up via editor.spawnHostProcess, awaits the result, parses JSON on stdout, then calls setAuthority. No streaming, no sub-states. Once attached, DockerExecSpawner is the "agent" that routes subsequent process spawns through docker exec.

Recommended follow-ups for §3

  • Add a RemoteIndicatorState::Connecting variant and a way for the plugin to set it (e.g. editor.setRemoteState("connecting" \| "failed")). Wire the indicator to render a spinner/underline while connecting.
  • Optionally render a translucent modal overlay during the restart itself (between the plugin's setAuthority call and the first frame of the new editor instance).

§4. Environment Provisioning (The Build)

Spec bulletStatusNotes
Three-phase state machine: Image Pull/Build → Container Start → Post-InitNo state machine exists. The plugin delegates to devcontainer up, which emits final JSON after the whole pipeline completes; intermediate phases are not observed.
Status bar: "Spinning/Building" stateRemoteIndicator has Local / Connected / Disconnected. No Building variant.
Progress toast: "Building Dev Container..."⚠️Only a single setStatus call (status.rebuilding). It is persistent until overwritten, but there is no spinner / progress animation.
[Show Logs] button → dedicated "Dev Container Output" terminal streaming stdout/stderreditor.spawnHostProcess returns {stdout, stderr, exit_code} only after the child process exits — there is no streaming API at the plugin surface. Adding one would require (a) a streaming variant of the plugin command, (b) a virtual-buffer terminal to render into, and (c) log-multiplexing glue.
Cancel button → graceful Docker shutdown → return to LocalNo cancellation. The underlying tokio::process::Command::output() call is awaited to completion; there's no handle to kill the child, and the plugin API exposes no kill primitive.

Recommended follow-ups for §4 This is the biggest implementation gap. Minimum viable additions:

  1. Streaming spawn API: new plugin command editor.spawnHostProcessStreaming(command, args, cwd?, callbacks?) that emits line-delta events; reuse PluginProcessOutput async message shape with a delta flag.
  2. Process cancellation: expose editor.killPluginProcess(id) that stores the tokio::process::Child handle at spawn time and kills on request. Core already owns the async runtime.
  3. Build-log virtual buffer: the plugin opens a virtual buffer (editor.createVirtualBufferInSplit, used elsewhere) and appends streamed log lines. Tailing this buffer is what "Show Logs" reveals.
  4. New RemoteIndicator sub-states: extend RemoteIndicatorState with Connecting { phase: BuildPhase } and render a Unicode spinner glyph. The plugin would transition it explicitly via a new op (editor.setRemoteState(...) or setAuthority with a phase field on the payload).

§5. Remote Server Injection

Spec bulletStatusNotes
Inject "Server Agent" (headless editor core) via docker execIntentional divergence. AUTHORITY_DESIGN.md explicitly says "Not a remote extension host." Fresh's UI stays on the host; only spawned processes (LSP servers, terminals, formatters, spawnProcess calls) cross into the container via DockerExecSpawner. Each process is one-shot docker exec, not a long-lived agent.
Client/Server socket or SSH tunnelNot applicable. Process outputs are returned over ordinary pipes from each docker exec invocation.
Workspace bind-mount to /workspaces/<folder-name>Handled externally by the devcontainer CLI (honoring workspaceFolder/workspaceMount from devcontainer.json). Fresh reads remoteWorkspaceFolder from the devcontainer up JSON output and passes it as -w to every docker exec.

No follow-ups recommended. The VS Code architecture requires a remote editor; Fresh's architecture doesn't. Porting that design would be a principles-level change rather than a feature addition.


§6. Lifecycle Hook Execution

Spec bulletStatusNotes
Execute hooks in order: initializeCommandonCreateCommandupdateContentCommandpostCreateCommand⚠️The devcontainer CLI (third-party, invoked via devcontainer up) runs onCreateCommand, updateContentCommand, postCreateCommand, postStartCommand, and postAttachCommand itself. initializeCommand is defined to run on the host before creation, and the plugin never invokes it — it's shown in the info panel but omitted from the lifecycle runner (devcontainer.ts:638-644). This is a spec violation, regardless of whether we implement the full state machine.
Auto-focus the output terminal during postCreateCommandThere is no output terminal to focus (see §4 — no log streaming).
Manual "Run Lifecycle Command" pickerDev Container: Run Lifecycle Command palette command lets the user run any lifecycle hook on demand. Covers the same set minus initializeCommand.

Recommended follow-ups for §6

  • Fix initializeCommand omission: add it to the lifecycle array in devcontainer.ts:638-644 and run it via editor.spawnHostProcess (so it runs on the host, per spec) before calling devcontainer up during devcontainer_attach.
  • Include initializeCommand in the Run Lifecycle Command picker for parity with the info panel.

§7. Ready State

Spec bulletStatusNotes
Remote Indicator flips to "Connected" state with container nameAuthority::display_label is set to Container:<short-id> by the plugin when building the payload (devcontainer.ts::buildContainerAuthorityPayload). The status bar renders it via connection_display_string(). After this branch, the {remote} element shows it persistently with the Connected color palette.
New terminals default to shell inside containerAuthority::terminal_wrapper is set to { command: "docker", args: ["exec", "-it", "-u", user, "-w", workspace, id, "bash", "-l"], manages_cwd: true }. Terminal manager honors this unconditionally.
Install/enable plugins from customizations.vscode.extensionsThe plugin reads customizations into its config type but does nothing with it. VS Code extensions aren't applicable to Fresh (different plugin model); a Fresh-specific customizations.fresh.plugins namespace would need to be defined first.
Port forwarding: detect opened ports, offer to forward to localhostforwardPorts is displayed in the info panel (Section_ports), but nothing enforces, detects, or routes it. Docker container ports are reachable on the host only if the devcontainer CLI configured the container's own port mappings (e.g. via appPort). There is no watch-for-new-listeners logic.

Recommended follow-ups for §7

  • Define a Fresh customizations namespace: spec-out customizations.fresh.plugins: string[] (plugin file paths or names), then have the plugin iterate and call editor.loadPlugin(path) after attach.
  • Port forwarding: lower priority since devcontainer up already honors forwardPorts for non-publish ports. A "Show Forwarded Ports" action that tabulates docker port <id> output would cover the observability half of the spec.

§8. Error Handling

Spec bulletStatusNotes
On build failure: stop reload, keep log terminal open, offer [Retry] or [Reopen Locally]⚠️Partial. If devcontainer up exits non-zero, the plugin shows status.rebuild_failed in the status bar and does NOT call setAuthority, so the authority transition is never queued — equivalent to "stop reload" and "stay local". But: no log terminal exists to be "kept open", and there is no action popup with [Retry] / [Reopen Locally].
Remote Indicator menu surfaces retry / revert after failureNot implemented. The {remote} popup's state machine (Local / Connected / Disconnected) has no "FailedBuild" branch — after a failed attach the indicator reads as Local, offering only "Reopen in Container" again, which is effectively a retry but without a dedicated [Retry] label or preservation of logs.

Recommended follow-ups for §8

  • When the build-log virtual buffer exists (§4), have the plugin keep it open on failure instead of closing it.
  • Add a RemoteIndicatorState::FailedAttach { last_error: String } variant so the indicator renders with error styling and the popup offers "Retry" / "Reopen Locally" / "Show Build Logs".
  • Persist the last-failure log path on the plugin side so "Show Build Logs" remains useful across restarts.

Summary of work items (prioritized)

Small (align with spec without architectural changes)

  1. Fix the initializeCommand omission in devcontainer.ts (§6).
  2. Rename the attach-prompt action labels to "Reopen in Container" / "Ignore" (§2).
  3. Add a "Show Forwarded Ports" palette command that runs docker port <id> and renders the output (§7).

Medium (requires new plugin-API surface)

  1. Streaming spawn API + process cancellation (§4, §8).
  2. RemoteIndicatorState::Connecting / FailedAttach variants plus a plugin op to transition them (§3, §4, §8).
  3. Build-log virtual buffer + "Show Logs" action (§4).
  4. "Show Container Logs" action via tailing docker logs <id> (§1).
  5. Scaffold-new-config command for Local when no devcontainer.json is present (§1).

Large / out of scope (principles-level divergence)

  1. Remote editor host in the container (§5) — not recommended; violates AUTHORITY_DESIGN.md principles 2–4 and the "shrink the core" stance.

Non-goals already documented

  • No cross-authority composition, multi-root workspaces, or credential syncing — called out in AUTHORITY_DESIGN.md "What this refactor is not."

Closing note

The implementation gap is dominated by UX surface (progress visibility, logs, cancel, retry, port forwarding visibility) rather than by architectural misalignment. The authority layer, the plugin boundary, and the restart-based transition model together cover what the spec describes as "Connection Agent" and "Remote Server Injection" in an idiomatic way for a terminal editor. Most remaining items can land as additive plugin-API surface (streaming, cancellation, indicator states) plus plugin-side UI work, without touching the authority contract.