docs/internal/DEVCONTAINER_SPEC_GAP_ANALYSIS.md
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.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:
devcontainer up,customizations.*.extensions.A handful of items are intentional architectural divergences rather than gaps:
docker exec to run tools inside the container
while the editor UI stays on the host.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.
| Spec bullet | Status | Notes |
|---|---|---|
| Persistent UI element in bottom-left status bar | ✅ | StatusBarElement::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 menu | ✅ | Click on {remote} → Action::ShowRemoteIndicatorMenu → show_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
docker logs <id> through
a streaming variant of spawnHostProcess (see §4 follow-up).| Spec bullet | Status | Notes |
|---|---|---|
Detect .devcontainer/devcontainer.json or .devcontainer.json on workspace open | ✅ | plugins/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
| Spec bullet | Status | Notes |
|---|---|---|
| Window reload / disposal of local watchers, LSPs | ✅ | Per 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 / splash | ❌ | Only 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
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.setAuthority call and the first frame
of the new editor instance).| Spec bullet | Status | Notes |
|---|---|---|
| Three-phase state machine: Image Pull/Build → Container Start → Post-Init | ❌ | No 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" state | ❌ | RemoteIndicator 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/stderr | ❌ | editor.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 Local | ❌ | No 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:
editor.spawnHostProcessStreaming(command, args, cwd?, callbacks?)
that emits line-delta events; reuse PluginProcessOutput async
message shape with a delta flag.editor.killPluginProcess(id) that
stores the tokio::process::Child handle at spawn time and kills
on request. Core already owns the async runtime.editor.createVirtualBufferInSplit, used elsewhere) and appends
streamed log lines. Tailing this buffer is what "Show Logs"
reveals.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).| Spec bullet | Status | Notes |
|---|---|---|
Inject "Server Agent" (headless editor core) via docker exec | ↔ | Intentional 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 tunnel | ↔ | Not 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.
| Spec bullet | Status | Notes |
|---|---|---|
Execute hooks in order: initializeCommand → onCreateCommand → updateContentCommand → postCreateCommand | ⚠️ | 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 postCreateCommand | ❌ | There is no output terminal to focus (see §4 — no log streaming). |
| Manual "Run Lifecycle Command" picker | ✅ | Dev 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
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.initializeCommand in the Run Lifecycle Command picker for
parity with the info panel.| Spec bullet | Status | Notes |
|---|---|---|
| Remote Indicator flips to "Connected" state with container name | ✅ | Authority::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 container | ✅ | Authority::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.extensions | ❌ | The 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 localhost | ❌ | forwardPorts 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
customizations.fresh.plugins: string[] (plugin file paths or names),
then have the plugin iterate and call editor.loadPlugin(path) after
attach.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.| Spec bullet | Status | Notes |
|---|---|---|
| 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 failure | ❌ | Not 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
RemoteIndicatorState::FailedAttach { last_error: String }
variant so the indicator renders with error styling and the popup
offers "Retry" / "Reopen Locally" / "Show Build Logs".initializeCommand omission in devcontainer.ts (§6).docker port <id> and renders the output (§7).RemoteIndicatorState::Connecting / FailedAttach variants plus a
plugin op to transition them (§3, §4, §8).docker logs <id> (§1).devcontainer.json
is present (§1).AUTHORITY_DESIGN.md principles 2–4 and the "shrink the
core" stance.AUTHORITY_DESIGN.md "What this
refactor is not."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.