Ink (React CLI framework) clears its output using repeated EL2+CUU1
sequences, one per previously-drawn line. When /clear resets Ink's
internal line counter, the next frame only erases a few lines instead
of the full previous output. In a real terminal the old content is in
scrollback and invisible, but pyte's fixed-size screen retains it,
producing ghost content (e.g. duplicated prompts) in SVG screenshots.
Added AltScreen.expand_clear_sequences() which detects runs of 3+
EL2+CUU1 pairs that don't reach row 0 and extends them to erase all
lines up to the top of the screen. Both DockerExecSession and
TerminalSession call this before feeding data to pyte.
Also made on_session_end() idempotent (contextlib.suppress KeyError)
to prevent a race when close_session() and natural session exit both
call it.
Added docs/ink-clear-fix.md with root cause analysis, byte-level
explanation, and reproduction script.
- close_session() now calls on_session_end() to remove the session from
sessions dict and routes, preventing zombie entries that persist after
the container is gone
- _remove_container() now closes the active session before removing the
app from apps_by_slug/apps, so session cleanup can still reference the
app during teardown
- Updated and added tests to verify session tracking cleanup
- handle DECSET ?47 as an alternate screen mode so tmux clear redraws don't overlay stale content in screenshots
- keep AltScreen mode checks aligned with 47/1047/1048/1049 variants used by full-screen TUIs
- document the screenshot debugging workflow in .github/skills/screenshot-debugging/SKILL.md for repeatable escape-sequence analysis
Allows per-container customization of the auto command. For example:
WEBTERM_DOCKER_AUTO_COMMAND='tmux new-session -ADs {container}'
This creates a tmux session named after the container instead of using
a fixed session name for all containers.
When a container has no labels, Docker returns {"Labels": null} in the
inspect response. The code was using .get("Labels", {}) which only
returns the default when the key is missing, not when it's null.
This caused _has_webterm_label() to raise TypeError, which was silently
caught by the event watcher's exception handler, causing it to reconnect
and miss the container start event.
Fixed by using .get("Labels") or {} pattern in:
- _handle_event() when processing start events
- _get_container_command() when extracting command label
- _get_container_theme() when extracting theme label
Added test for null labels case.
On iPad with a hardware keyboard, keybar modifiers were cleared without
being applied if the textarea wasn't focused. Now keybar Ctrl/Shift
intercepts document-level keydown events, applies the modifiers to the
pressed key, sends the correct sequence, and then clears the modifiers.