docs/internal/DEVCONTAINER_USABILITY_TEST_2026-04-26.md
Date: 2026-04-26
Method: Moderated think-aloud, single participant (TUI-savvy developer profile), executed via tmux against target/debug/fresh.
Project under test: vscode-remote-try-python (Flask app + mcr.microsoft.com/devcontainers/python:1-3.12)
Container runtime: Docker, healthy.
Dev Container Detected modal appeared on launch with Reopen in Container / Ignore. Zero hunting required.File Explorer to [Container], and the status bar replaced Local with Container:1110a8fa510a. Two persistent signals.Up 21 hours), but the auto-opened build log only contained the CLI version line. As a first-time user I'd be unsure whether it just attached, just built, or skipped.curl, Rebuild ★ SEQ 5/7Dev Container: Rebuild, but in alphabetical order it sorts after every Show * command. Searching by "rebuild" finds it instantly; searching by "container" makes you scroll.devcontainer.json was edited but the open buffer didn't refresh until a rebuild ran. A user editing the file from a sibling buffer wouldn't see their own change reflected.⠿ Building spinner; a fresh build-<timestamp>.log is auto-opened in a split.apt. The log uses \r (CR-only) line endings, so apt's progress bars compress into one ~2 KB-long line. The buffer looks empty even while the file grows.exit code 100, yarn NO_PUBKEY). Modal: Dev Container Attach Failed with good affordances (Retry, Show Build Logs, Reopen Locally, Dismiss) — but the modal body is a JS stack trace from devContainersSpecCLI.js, not the underlying apt failure. Status bar truncates to Attach failed: at async uG (.... Have to dig into the log file to find the real cause.*Terminal 0* disappeared). Friction for users with long-running shells.0.0.0.0:9000 inside the container, but docker ps showed empty PORTS. No host mapping, no notification fired despite portsAttributes.9000.onAutoForward: notify.port, Ports, Forward, Forwarded returned either zero matches or unrelated commands (Suspend Process, Mouse Support, etc.). The commands Dev Container: Show Ports and Show Forwarded Ports exist (visible when filtering by container) but cannot be located by their natural keywords.curl http://172.17.0.2:9000/ from the host returned the page. Only an option for docker-savvy users.devcontainer.json. No toast, no file-explorer badge, no obvious indicator on the buffer. Existing container kept running so status bar still showed Container:e794813713ef.Dev Container: palette commands disappeared. Verified by scrolling alphabetically through the D section — only Debug…, Decrease Split Size, Dedent…, Delete…, Dump Config, Duplicate Line. Rebuild, Detach, Open Config, Show Build Logs — all gone.Dev Container: commands did not return to the palette. The participant has no in-editor path back; an editor restart is the only recovery.Ctrl+P sometimes goes to terminal, sometimes opens palette, depending on focus. The status hint Ctrl+Space to exit terminal mode exists but is easy to miss..fresh-cache/devcontainer-logs/build-<UTC-timestamp>.log (e.g. build-2026-04-26_19-12-05.log, then _19-16-40, then _19-18-36). 6 files had stacked up from prior sessions during this run. No rotation/cleanup observed.⠿ Building while it ran.tail from the shell showed real progress while the in-editor buffer didn't.\r ruins apt output. Status bar reported CR line endings and the cursor sat at Ln 1, Col 2088. apt's progress bars write \r between updates, so the entire 2 KB+ apt section is one logical line that renders nearly blank. A user watching the build screen would think nothing was happening./, scrollable, copyable. For a TUI/vim user this is the right model. grep'ing the file from outside found exit code 100 and the GPG error in seconds._19-12-05.log) was still in a tab; rebuild #2's log opened as a separate tab. Tabs accumulate.app.py, middle = devcontainer.json, bottom = build-log + terminal. By the end of Task 2 the bottom panes were ~5 lines tall each.devcontainer.json was open in two adjacent splits simultaneously (lines 33–36 visible in both). Looked like the rebuild auto-opened the config in a new split rather than focusing the existing one.devcontainer.json wrapped aggressively (# License…\ninformation. across two visual lines) just because the split was narrow.Decrease Split Size exists in the palette but I didn't trigger it.Dev Container: Show Forwarded Ports and Show Ports produced no visible pane — possibly the panel did open but couldn't be rendered in the crowded layout, or it opened off-screen. No status confirmation either way.name × name × name × tabs at the top with □ × controls, making it obvious which buffers belong where.Ctrl+P. When focus was inside the terminal pane, Ctrl+P keystrokes were eaten by the shell instead of opening the palette. Required Ctrl+Space first ("Exit Terminal Mode") to free the editor's bindings. The status hint Terminal 0 opened (Ctrl+Space to ... shows once when the terminal opens, then disappears — easy to miss.#buffer palette mode is advertised but unreliable in this session. Bottom of the palette shows file | >command | :line | #buffer mode hints. Trying #devcontainer to switch buffers landed in terminal-mode (because focus was on the terminal pane) instead of routing through the palette. Discoverability of "you must be focused outside the terminal first" is poor.Alt+] Next Split / Alt+W Close Tab / Ctrl+E Focus File Explorer all show as bound shortcuts in the palette. Reasonable, but I never naturally discovered the cycle order between the 3 horizontal splits — there's no visible "split N of 3" indicator on each pane.The single design choice that turns the rest of the devcontainer UX from "good" to "actively unusable by the third rebuild" is the always-split, never-close pane strategy. Calling it out as its own section because most of the Medium-severity items in the bug table are downstream of it.
Every meaningful devcontainer event opens a new horizontal split below the existing splits and shows the relevant buffer there:
Show Forwarded Ports / Show Container Logs → also wants its own
paneAfter 3–4 lifecycle events the right column is 5+ splits stacked vertically, each ~5–7 rows tall. Buffer text wraps aggressively, status-bar messages get truncated, and the command palette popup itself stops rendering because there's nowhere to put it (this is the popup-invisibility bug above).
devcontainer.json across two splits", "splits don't
compact", and "stale build-log tabs accumulate" rows in the bug
table all collapse into "the layout strategy is wrong" once you
look at them together.closeStaleBuildLogBuffers is
the source of the existing
attach_closes_stale_build_log_buffer_from_previous_run
regression test — but it only fires on cold-start, not on
in-session rebuilds. Extending that to in-session would close one
of the loops driving the layout pressure.Of all the things the devcontainer feature does well (auto-detect prompt, status-bar attach indicator, build logs as inspectable buffers, JSONC LSP install offer), the pane proliferation is the single decision that turns those wins into liabilities. Worth fixing before any of the smaller items in the bug table — most of those will become non-issues once the panel strategy changes.
devcontainer.json parsing fails — never silently disable commands.\r properly (or render in a dedicated panel with progress bars).forwardPorts / detected at runtime, plus a port-forward toast on bind.e794813713ef.devcontainer.json restored to valid JSON; the postCreateCommand was hardened to be GPG-failure-tolerant (skips broken yarn apt source if curl is missing) — kept for future test runs.vscode-remote-try-python/.fresh-cache/devcontainer-logs/build-2026-04-26_19-*.log.Status legend (added after the 2026-04-26 retest below):
| Severity | Bug / Gap | Reference | Status (2026-04-26) |
|---|---|---|---|
| Critical | Pane-splitting strategy is "always-split, never-close": every lifecycle event (attach, rebuild, Show Ports, terminal open) adds a new horizontal split, nothing ever closes one. By the third rebuild the right column is 5+ splits ~5 rows tall, and the layout becomes the root cause of several Medium rows below (popup invisible, Show Ports no visible pane, splits don't compact, duplicate devcontainer.json across splits, stale build-log tabs). | "Pane Splitting" section | Confirmed — root cause; fixing this collapses ~5 downstream rows |
| Critical | After a devcontainer.json syntax error and a Rebuild, all Dev Container: palette commands disappear and do not return after the JSON is fixed — only an editor restart recovers. | Task 4 | Confirmed (post-rebuild restart cycle re-runs plugin load against broken JSON; harness can't reproduce because it shortcuts the restart) |
| High | Auto port-forwarding doesn't publish ports declared in portsAttributes / forwardPorts; no host mapping, no onAutoForward: notify toast. | Task 3 | Confirmed |
| High | devcontainer.json syntax errors fail silently — no toast, no file-explorer badge, no inline marker. | Task 4 | Confirmed |
| High | Palette filter ranking is unpredictable: same query yields different results across invocations, and natural keywords don't surface obvious commands. | Task 3, Task 4, Cross-cutting #1 | Conditional — fuzzy ranking degenerates only with many commands loaded; harness has too few to reproduce |
Show Ports, Show Forwarded Ports) cannot be located by typing port/forward/forwarded. | Task 3 | Disconfirmed — retest in fresh tmux session found these instantly via port and via Show Forwarded; original symptom was the cramped-layout / popup-invisible bug below | |
| Medium | Palette popup renders nothing when the layout has many horizontal splits — the prompt shows the filter but no result list appears, and Enter reports No selection. | Panes (added) | Confirmed |
| Medium | Build-log buffer doesn't tail live; lags the on-disk file by tens of seconds. | Logs | Untested |
| Medium | \r-only output (apt progress) collapses to one ~2 KB line and renders nearly blank in the buffer. | Task 2, Logs | Disconfirmed — real logs are mixed \r + \n (each progress line ends in \n), and the editor renders them fine. The original status-bar reading of Col 2088 was likely a misread |
Attach Failed modal body shows a JS stack trace from devContainersSpecCLI.js instead of the root cause. | Task 2 | Disconfirmed — harness failed_attach_popup_includes_actual_failure_reason shows the modal does include FAKE_DC_UP_FAIL_REASON text. Stack trace seen interactively was likely from an earlier failed-state buffer in the same session | |
devcontainer.json modified on disk) don't reload until a side effect (rebuild) reopens them. | Cross-cutting #2 | Disconfirmed — harness externally_modified_buffer_reloads_on_disk_change shows the buffer reloads from disk | |
Dev Container: Show Ports / Show Forwarded Ports produce no visible pane, and no status confirmation. | Panes | Disconfirmed — same root cause as palette popup invisibility (above); the panel does open, the cramped layout hid it | |
| Medium | Rebuild silently terminates open terminal tabs (*Terminal 0* disappears). | Task 2 | Untested |
| Low | Build-log files accumulate in .fresh-cache/devcontainer-logs/ with no rotation/cleanup. | Logs | Confirmed |
| Low | Stale build-log tabs stay open after subsequent rebuilds. | Logs | Confirmed |
| Low | Same buffer (devcontainer.json) gets duplicated across two splits after rebuild. | Panes | Confirmed |
| Low | New splits don't compact existing ones or shrink the sidebar; bottom panes shrink to ~5 lines. | Panes | Confirmed |
| Low | No per-pane "split N of M" indicator; cycle order isn't discoverable. | Navigation | Confirmed (UX gap, not a bug) |
| Low | Terminal pane traps Ctrl+P; the Ctrl+Space exit hint shows once on terminal open then disappears. | Cross-cutting #3, Navigation | Confirmed |
| Low | LSP install prompt and Dev Container Detected prompt overlap with ambiguous z-order. | Navigation | Confirmed |
| Low | Reuse-existing-container path produces a build log containing only the CLI version line — user can't tell whether it built, attached, or skipped. | Task 1 | Confirmed |
| Low | Dev Container: Rebuild sorts alphabetically after every Show * command in the palette. | Task 2 | Confirmed (consequence of alphabetical sort + naming) |
| Low | Palette doesn't gate commands by state: Attach and Cancel Startup remain offered while already attached, alongside Detach. | Task 2 | Confirmed — covered by failing test palette_attach_command_hidden_when_already_attached |
A follow-up retest revisited each finding via the in-tree fake
devcontainer CLI harness (E2E tests under
crates/fresh-editor/tests/e2e/plugins/devcontainer_usability_repros.rs)
and a fresh interactive tmux run. Three substantive corrections to
the original report:
Palette filter problems are conditional, not universal. In a
fresh tmux session the filter found Dev Container: Show Forwarded Ports instantly via port and Show Forwarded. The unreliable
ranking did reappear later in the same session once many splits
accumulated and a broken-JSON rebuild had been triggered, so the
filter degeneracy is a real bug — but it manifests only under load
(many commands registered) and/or in pathological session state,
not on every keystroke. The harness can't reproduce it because it
loads ~10× fewer commands than a real install.
The "popup invisible" symptom is its own bug, distinct from the
filter. With 5+ horizontal splits stacked in the right column,
the palette accepts the filter text (>Dev shows in the prompt)
but no result list is drawn anywhere on screen, and Enter
reports No selection. This explains the original "Show
Forwarded Ports / Show Ports produce no visible pane" symptom in
Task 3 and several of the "filter returns nothing" symptoms in
Tasks 3 and 4.
Several medium-severity items were observation artifacts, not
real bugs. The \r-rendering, external-buffer-reload, and
failed-attach-modal-text claims all fail to reproduce when driven
programmatically through the fake-CLI harness, and could not be
re-triggered interactively. Most likely cause for the original
reports: stale tmux captures, popup focus capture by the terminal
pane, or a prior failed-attach buffer still being visible from
an earlier rebuild attempt. Those rows have been struck through
above.
The repro tests live in devcontainer_usability_repros.rs. One is
a #[test] that fails on master (palette_attach_command_hidden_when_already_attached).
The other two — for the post-rebuild command-disappearance bug and
the popup-invisible-with-many-splits bug — are #[ignore]'d with
notes explaining the harness limitations that prevent CI repro
(no editor-restart hook; PTY too tall to crowd the popup off-screen).