docs(demo): showcase remote sessions
- commit
- 781dead95fdd5033723e971249f8b8e9afe16b6c
- author
- brice · · 2026-08-11 08:23 UTC
- committer
- brice · · 2026-08-11 10:01 UTC
- parent
- da1bbb1
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +154 -88
A .dockerignore +4 -0
| @@ -0,0 +1,4 @@ | |||
| 1 | + | .git | |
| 2 | + | .worktrees | |
| 3 | + | scripts/demo/out | |
| 4 | + | docs/assets/demo.gif | |
| @@ -0,0 +1,4 @@ | |||||
| 1 | + | .git | |||
| 2 | + | .worktrees | |||
| 3 | + | scripts/demo/out | |||
| 4 | + | docs/assets/demo.gif | |||
M Makefile +3 -4
| @@ -53,10 +53,9 @@ demo: | |||
| 53 | 53 | -y -v error -i /out/demo.gif -i /out/border.png -i /out/mask.png \ | |
| 54 | 54 | -filter_complex "[0:v][1:v]overlay=0:0[bordered];[2:v]format=gray[m];[bordered][m]alphamerge,split[a][b];[a]palettegen=reserve_transparent=1[p];[b][p]paletteuse=alpha_threshold=128" \ | |
| 55 | 55 | /out/demo-masked.gif | |
| 56 | - | # --lossy above 30 smears the near-identical darks of the window chrome | |
| 57 | - | # (window bar vs terminal background) into visible banding, and the | |
| 58 | - | # conserve-memory fallback for huge inputs roughly doubles the output. | |
| 59 | - | docker run --rm --entrypoint gifsicle -v "$(CURDIR)/scripts/demo/out:/out" vev-demo --no-conserve-memory -O3 --lossy=30 -o /out/demo.gif /out/demo-masked.gif | |
| 56 | + | # Keep the final GIF lossless: the streaming FFmpeg pass has already | |
| 57 | + | # bounded memory, and preserving terminal chrome avoids color banding. | |
| 58 | + | docker run --rm --entrypoint gifsicle -v "$(CURDIR)/scripts/demo/out:/out" vev-demo --no-conserve-memory -O3 -o /out/demo.gif /out/demo-masked.gif | |
| 60 | 59 | # Temp files are owned by the container's subuid, so remove them from | |
| 61 | 60 | # inside a container rather than the host. | |
| 62 | 61 | docker run --rm --entrypoint sh -v "$(CURDIR)/scripts/demo/out:/out" vev-demo -c 'rm /out/demo-masked.gif /out/mask.png /out/border.png' | |
| @@ -53,10 +53,9 @@ demo: | |||||
| 53 | -y -v error -i /out/demo.gif -i /out/border.png -i /out/mask.png \ | 53 | -y -v error -i /out/demo.gif -i /out/border.png -i /out/mask.png \ | ||
| 54 | -filter_complex "[0:v][1:v]overlay=0:0[bordered];[2:v]format=gray[m];[bordered][m]alphamerge,split[a][b];[a]palettegen=reserve_transparent=1[p];[b][p]paletteuse=alpha_threshold=128" \ | 54 | -filter_complex "[0:v][1:v]overlay=0:0[bordered];[2:v]format=gray[m];[bordered][m]alphamerge,split[a][b];[a]palettegen=reserve_transparent=1[p];[b][p]paletteuse=alpha_threshold=128" \ | ||
| 55 | /out/demo-masked.gif | 55 | /out/demo-masked.gif | ||
| 56 | - | # --lossy above 30 smears the near-identical darks of the window chrome | 56 | + | # Keep the final GIF lossless: the streaming FFmpeg pass has already |
| 57 | - | # (window bar vs terminal background) into visible banding, and the | 57 | + | # bounded memory, and preserving terminal chrome avoids color banding. |
| 58 | - | # conserve-memory fallback for huge inputs roughly doubles the output. | 58 | + | docker run --rm --entrypoint gifsicle -v "$(CURDIR)/scripts/demo/out:/out" vev-demo --no-conserve-memory -O3 -o /out/demo.gif /out/demo-masked.gif |
| 59 | - | docker run --rm --entrypoint gifsicle -v "$(CURDIR)/scripts/demo/out:/out" vev-demo --no-conserve-memory -O3 --lossy=30 -o /out/demo.gif /out/demo-masked.gif | |||
| 60 | # Temp files are owned by the container's subuid, so remove them from | 59 | # Temp files are owned by the container's subuid, so remove them from | ||
| 61 | # inside a container rather than the host. | 60 | # inside a container rather than the host. | ||
| 62 | docker run --rm --entrypoint sh -v "$(CURDIR)/scripts/demo/out:/out" vev-demo -c 'rm /out/demo-masked.gif /out/mask.png /out/border.png' | 61 | docker run --rm --entrypoint sh -v "$(CURDIR)/scripts/demo/out:/out" vev-demo -c 'rm /out/demo-masked.gif /out/mask.png /out/border.png' | ||
M README.md +2 -2
| @@ -12,7 +12,7 @@ | |||
| 12 | 12 | <p align="center">A terminal multiplexer for Linux and macOS.<br> | |
| 13 | 13 | Like tmux, minus the prefix key. Like mosh, minus mosh. One binary.</p> | |
| 14 | 14 | ||
| 15 | - | <p align="center"><img src="docs/assets/demo.gif" alt="vev demo: named sessions, splits, stacked panes, floating window, agent bell notification, detach and re-attach" width="800"></p> | |
| 15 | + | <p align="center"><img src="docs/assets/demo.gif" alt="vev demo: local and remote sessions, splits, stacked panes, floating window, notifications, detach and re-attach" width="800"></p> | |
| 16 | 16 | ||
| 17 | 17 | --- | |
| 18 | 18 | ||
| @@ -140,5 +140,5 @@ Optional file at `~/.config/vev/config`, reloaded live. Themes, key bindings, pa | |||
| 140 | 140 | make test # go test ./... -race | |
| 141 | 141 | make lint # goimports check, go vet | |
| 142 | 142 | make mocks # regenerate mocks | |
| 143 | - | make demo # regenerate docs/assets/demo.gif (needs Docker and ~/.claude credentials) | |
| 143 | + | make demo # regenerate docs/assets/demo.gif (needs Docker) | |
| 144 | 144 | ``` | |
| @@ -12,7 +12,7 @@ | |||||
| 12 | <p align="center">A terminal multiplexer for Linux and macOS.<br> | 12 | <p align="center">A terminal multiplexer for Linux and macOS.<br> | ||
| 13 | Like tmux, minus the prefix key. Like mosh, minus mosh. One binary.</p> | 13 | Like tmux, minus the prefix key. Like mosh, minus mosh. One binary.</p> | ||
| 14 | 14 | ||||
| 15 | - | <p align="center"><img src="docs/assets/demo.gif" alt="vev demo: named sessions, splits, stacked panes, floating window, agent bell notification, detach and re-attach" width="800"></p> | 15 | + | <p align="center"><img src="docs/assets/demo.gif" alt="vev demo: local and remote sessions, splits, stacked panes, floating window, notifications, detach and re-attach" width="800"></p> |
| 16 | 16 | ||||
| 17 | --- | 17 | --- | ||
| 18 | 18 | ||||
| @@ -140,5 +140,5 @@ Optional file at `~/.config/vev/config`, reloaded live. Themes, key bindings, pa | |||||
| 140 | make test # go test ./... -race | 140 | make test # go test ./... -race | ||
| 141 | make lint # goimports check, go vet | 141 | make lint # goimports check, go vet | ||
| 142 | make mocks # regenerate mocks | 142 | make mocks # regenerate mocks | ||
| 143 | - | make demo # regenerate docs/assets/demo.gif (needs Docker and ~/.claude credentials) | 143 | + | make demo # regenerate docs/assets/demo.gif (needs Docker) |
| 144 | ``` | 144 | ``` | ||
M docs/assets/demo.gif +0 -0 binary file: no text diff
binary file: no text diff
M scripts/demo/Dockerfile +23 -9
| @@ -7,23 +7,37 @@ COPY . . | |||
| 7 | 7 | RUN CGO_ENABLED=0 go build -o /out/vev . | |
| 8 | 8 | ||
| 9 | 9 | FROM archlinux:latest | |
| 10 | - | # The base image ships a pacman.conf that skips extracting man/info pages | |
| 11 | - | # (NoExtract) to stay small; the demo wants a real `man ls`, so drop that | |
| 12 | - | # rule and reinstall coreutils to pull its man page onto disk. | |
| 13 | - | RUN pacman -Sy --noconfirm archlinux-keyring \ | |
| 10 | + | RUN --mount=type=cache,target=/var/cache/pacman/pkg,sharing=locked \ | |
| 11 | + | pacman -Sy --noconfirm archlinux-keyring \ | |
| 14 | 12 | && sed -i '/^NoExtract.*usr\/share\/man/d' /etc/pacman.conf \ | |
| 15 | 13 | && pacman -Syu --noconfirm \ | |
| 16 | 14 | vhs ttyd ffmpeg chromium \ | |
| 17 | 15 | htop fastfetch gifsicle imagemagick \ | |
| 18 | 16 | eza neovim man-db man-pages coreutils \ | |
| 19 | - | nodejs npm ncurses \ | |
| 20 | - | ttf-jetbrains-mono-nerd \ | |
| 21 | - | && pacman -Scc --noconfirm | |
| 22 | - | RUN npm install -g --allow-scripts=@anthropic-ai/claude-code @anthropic-ai/claude-code | |
| 17 | + | ncurses openssh util-linux \ | |
| 18 | + | ttf-jetbrains-mono-nerd | |
| 23 | 19 | RUN useradd -m -u 1000 demo | |
| 24 | 20 | COPY --from=build /out/vev /usr/local/bin/vev | |
| 25 | 21 | COPY scripts/demo/ticker.sh /usr/local/bin/ticker | |
| 26 | - | RUN chmod +x /usr/local/bin/ticker | |
| 22 | + | COPY scripts/demo/demo-shell.sh /usr/local/bin/demo-shell | |
| 23 | + | RUN chmod +x /usr/local/bin/ticker /usr/local/bin/demo-shell \ | |
| 24 | + | && install -d -o demo -g demo -m 0700 /home/demo/.ssh \ | |
| 25 | + | && install -d -m 0755 /run/sshd \ | |
| 26 | + | && ssh-keygen -A \ | |
| 27 | + | && ssh-keygen -q -t ed25519 -N '' -C vev-demo -f /home/demo/.ssh/id_ed25519 \ | |
| 28 | + | && cp /home/demo/.ssh/id_ed25519.pub /home/demo/.ssh/authorized_keys \ | |
| 29 | + | && { printf 'remote '; cut -d' ' -f1-2 /etc/ssh/ssh_host_ed25519_key.pub; } > /home/demo/.ssh/known_hosts \ | |
| 30 | + | && printf '%s\n' \ | |
| 31 | + | 'Host remote' \ | |
| 32 | + | ' User demo' \ | |
| 33 | + | ' IdentityFile ~/.ssh/id_ed25519' \ | |
| 34 | + | ' IdentitiesOnly yes' \ | |
| 35 | + | ' StrictHostKeyChecking yes' \ | |
| 36 | + | ' UserKnownHostsFile ~/.ssh/known_hosts' \ | |
| 37 | + | ' LogLevel ERROR' \ | |
| 38 | + | > /home/demo/.ssh/config \ | |
| 39 | + | && chmod 0600 /home/demo/.ssh/id_ed25519 /home/demo/.ssh/authorized_keys /home/demo/.ssh/known_hosts /home/demo/.ssh/config \ | |
| 40 | + | && chown -R demo:demo /home/demo/.ssh | |
| 27 | 41 | USER demo | |
| 28 | 42 | WORKDIR /home/demo | |
| 29 | 43 | ENV VHS_NO_SANDBOX=true | |
| @@ -7,23 +7,37 @@ COPY . . | |||||
| 7 | RUN CGO_ENABLED=0 go build -o /out/vev . | 7 | RUN CGO_ENABLED=0 go build -o /out/vev . | ||
| 8 | 8 | ||||
| 9 | FROM archlinux:latest | 9 | FROM archlinux:latest | ||
| 10 | - | # The base image ships a pacman.conf that skips extracting man/info pages | 10 | + | RUN --mount=type=cache,target=/var/cache/pacman/pkg,sharing=locked \ |
| 11 | - | # (NoExtract) to stay small; the demo wants a real `man ls`, so drop that | 11 | + | pacman -Sy --noconfirm archlinux-keyring \ |
| 12 | - | # rule and reinstall coreutils to pull its man page onto disk. | |||
| 13 | - | RUN pacman -Sy --noconfirm archlinux-keyring \ | |||
| 14 | && sed -i '/^NoExtract.*usr\/share\/man/d' /etc/pacman.conf \ | 12 | && sed -i '/^NoExtract.*usr\/share\/man/d' /etc/pacman.conf \ | ||
| 15 | && pacman -Syu --noconfirm \ | 13 | && pacman -Syu --noconfirm \ | ||
| 16 | vhs ttyd ffmpeg chromium \ | 14 | vhs ttyd ffmpeg chromium \ | ||
| 17 | htop fastfetch gifsicle imagemagick \ | 15 | htop fastfetch gifsicle imagemagick \ | ||
| 18 | eza neovim man-db man-pages coreutils \ | 16 | eza neovim man-db man-pages coreutils \ | ||
| 19 | - | nodejs npm ncurses \ | 17 | + | ncurses openssh util-linux \ |
| 20 | - | ttf-jetbrains-mono-nerd \ | 18 | + | ttf-jetbrains-mono-nerd |
| 21 | - | && pacman -Scc --noconfirm | |||
| 22 | - | RUN npm install -g --allow-scripts=@anthropic-ai/claude-code @anthropic-ai/claude-code | |||
| 23 | RUN useradd -m -u 1000 demo | 19 | RUN useradd -m -u 1000 demo | ||
| 24 | COPY --from=build /out/vev /usr/local/bin/vev | 20 | COPY --from=build /out/vev /usr/local/bin/vev | ||
| 25 | COPY scripts/demo/ticker.sh /usr/local/bin/ticker | 21 | COPY scripts/demo/ticker.sh /usr/local/bin/ticker | ||
| 26 | - | RUN chmod +x /usr/local/bin/ticker | 22 | + | COPY scripts/demo/demo-shell.sh /usr/local/bin/demo-shell |
| 23 | + | RUN chmod +x /usr/local/bin/ticker /usr/local/bin/demo-shell \ | |||
| 24 | + | && install -d -o demo -g demo -m 0700 /home/demo/.ssh \ | |||
| 25 | + | && install -d -m 0755 /run/sshd \ | |||
| 26 | + | && ssh-keygen -A \ | |||
| 27 | + | && ssh-keygen -q -t ed25519 -N '' -C vev-demo -f /home/demo/.ssh/id_ed25519 \ | |||
| 28 | + | && cp /home/demo/.ssh/id_ed25519.pub /home/demo/.ssh/authorized_keys \ | |||
| 29 | + | && { printf 'remote '; cut -d' ' -f1-2 /etc/ssh/ssh_host_ed25519_key.pub; } > /home/demo/.ssh/known_hosts \ | |||
| 30 | + | && printf '%s\n' \ | |||
| 31 | + | 'Host remote' \ | |||
| 32 | + | ' User demo' \ | |||
| 33 | + | ' IdentityFile ~/.ssh/id_ed25519' \ | |||
| 34 | + | ' IdentitiesOnly yes' \ | |||
| 35 | + | ' StrictHostKeyChecking yes' \ | |||
| 36 | + | ' UserKnownHostsFile ~/.ssh/known_hosts' \ | |||
| 37 | + | ' LogLevel ERROR' \ | |||
| 38 | + | > /home/demo/.ssh/config \ | |||
| 39 | + | && chmod 0600 /home/demo/.ssh/id_ed25519 /home/demo/.ssh/authorized_keys /home/demo/.ssh/known_hosts /home/demo/.ssh/config \ | |||
| 40 | + | && chown -R demo:demo /home/demo/.ssh | |||
| 27 | USER demo | 41 | USER demo | ||
| 28 | WORKDIR /home/demo | 42 | WORKDIR /home/demo | ||
| 29 | ENV VHS_NO_SANDBOX=true | 43 | ENV VHS_NO_SANDBOX=true | ||
D scripts/demo/claude-settings.json +0 -6
| @@ -1,6 +0,0 @@ | |||
| 1 | - | { | |
| 2 | - | "preferredNotifChannel": "terminal_bell", | |
| 3 | - | "permissions": { | |
| 4 | - | "allow": ["Bash(sleep:*)"] | |
| 5 | - | } | |
| 6 | - | } | |
| @@ -1,6 +0,0 @@ | |||||
| 1 | - | { | |||
| 2 | - | "preferredNotifChannel": "terminal_bell", | |||
| 3 | - | "permissions": { | |||
| 4 | - | "allow": ["Bash(sleep:*)"] | |||
| 5 | - | } | |||
| 6 | - | } | |||
A scripts/demo/compose.yaml +17 -0
| @@ -0,0 +1,17 @@ | |||
| 1 | + | name: vev-demo | |
| 2 | + | ||
| 3 | + | services: | |
| 4 | + | remote: | |
| 5 | + | image: vev-demo | |
| 6 | + | hostname: remote | |
| 7 | + | user: root | |
| 8 | + | entrypoint: ["/usr/bin/sshd", "-D", "-e"] | |
| 9 | + | ||
| 10 | + | client: | |
| 11 | + | image: vev-demo | |
| 12 | + | depends_on: | |
| 13 | + | - remote | |
| 14 | + | volumes: | |
| 15 | + | - ../../scripts/demo:/tape:ro | |
| 16 | + | - ../../scripts/demo/out:/home/demo/out | |
| 17 | + | - ../../scripts/demo/out/local:/home/demo/.local | |
| @@ -0,0 +1,17 @@ | |||||
| 1 | + | name: vev-demo | |||
| 2 | + | ||||
| 3 | + | services: | |||
| 4 | + | remote: | |||
| 5 | + | image: vev-demo | |||
| 6 | + | hostname: remote | |||
| 7 | + | user: root | |||
| 8 | + | entrypoint: ["/usr/bin/sshd", "-D", "-e"] | |||
| 9 | + | ||||
| 10 | + | client: | |||
| 11 | + | image: vev-demo | |||
| 12 | + | depends_on: | |||
| 13 | + | - remote | |||
| 14 | + | volumes: | |||
| 15 | + | - ../../scripts/demo:/tape:ro | |||
| 16 | + | - ../../scripts/demo/out:/home/demo/out | |||
| 17 | + | - ../../scripts/demo/out/local:/home/demo/.local | |||
A scripts/demo/demo-shell.sh +26 -0
| @@ -0,0 +1,26 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | set -euo pipefail | |
| 3 | + | session=${VEV:-} | |
| 4 | + | unset VEV | |
| 5 | + | ||
| 6 | + | case "${VEV_DEMO_SCENE:-}" in | |
| 7 | + | remote-deploys) | |
| 8 | + | work_marker=/tmp/vev-demo-work-shown | |
| 9 | + | if [[ $session != session=work,* || -e $work_marker ]]; then | |
| 10 | + | exec /bin/bash --noprofile --norc | |
| 11 | + | fi | |
| 12 | + | : >"$work_marker" | |
| 13 | + | printf '\033]0;deploys · remote\007' | |
| 14 | + | printf '\n\033[1;36mDeploy queue · remote\033[0m\n\n' | |
| 15 | + | printf ' \033[32m✓\033[0m api-gateway production 3m ago\n' | |
| 16 | + | printf ' \033[32m✓\033[0m web-console production 7m ago\n' | |
| 17 | + | printf ' \033[33m•\033[0m worker-pool staging running\n\n' | |
| 18 | + | printf 'Use the session picker to jump between this host and local work.\n\n' | |
| 19 | + | exec /bin/bash --noprofile --norc | |
| 20 | + | ;; | |
| 21 | + | remote-ticker) | |
| 22 | + | exec ticker | |
| 23 | + | ;; | |
| 24 | + | esac | |
| 25 | + | ||
| 26 | + | exec /bin/bash --noprofile --norc | |
| @@ -0,0 +1,26 @@ | |||||
| 1 | + | #!/usr/bin/env bash | |||
| 2 | + | set -euo pipefail | |||
| 3 | + | session=${VEV:-} | |||
| 4 | + | unset VEV | |||
| 5 | + | ||||
| 6 | + | case "${VEV_DEMO_SCENE:-}" in | |||
| 7 | + | remote-deploys) | |||
| 8 | + | work_marker=/tmp/vev-demo-work-shown | |||
| 9 | + | if [[ $session != session=work,* || -e $work_marker ]]; then | |||
| 10 | + | exec /bin/bash --noprofile --norc | |||
| 11 | + | fi | |||
| 12 | + | : >"$work_marker" | |||
| 13 | + | printf '\033]0;deploys · remote\007' | |||
| 14 | + | printf '\n\033[1;36mDeploy queue · remote\033[0m\n\n' | |||
| 15 | + | printf ' \033[32m✓\033[0m api-gateway production 3m ago\n' | |||
| 16 | + | printf ' \033[32m✓\033[0m web-console production 7m ago\n' | |||
| 17 | + | printf ' \033[33m•\033[0m worker-pool staging running\n\n' | |||
| 18 | + | printf 'Use the session picker to jump between this host and local work.\n\n' | |||
| 19 | + | exec /bin/bash --noprofile --norc | |||
| 20 | + | ;; | |||
| 21 | + | remote-ticker) | |||
| 22 | + | exec ticker | |||
| 23 | + | ;; | |||
| 24 | + | esac | |||
| 25 | + | ||||
| 26 | + | exec /bin/bash --noprofile --norc | |||
M scripts/demo/demo.tape +36 -26
| @@ -20,13 +20,34 @@ Set WaitTimeout 30s | |||
| 20 | 20 | # (70-160ms) and short hesitations at word boundaries so the rhythm | |
| 21 | 21 | # reads irregular/human; palette codes (CNS/SSP/…) stay single-speed. | |
| 22 | 22 | ||
| 23 | - | # ── Session "work": a colourful split (eza + ticker) ───── | |
| 24 | - | Type@90ms "vev " | |
| 25 | - | Sleep 160ms | |
| 26 | - | Type@75ms "new " | |
| 27 | - | Type@140ms "work" | |
| 23 | + | # ── Local and remote history, shown immediately ────────── | |
| 24 | + | Type@180ms "vev" | |
| 25 | + | Sleep 600ms | |
| 28 | 26 | Enter | |
| 29 | 27 | Sleep 1.1s | |
| 28 | + | # Keep the initial local shell so the final detach/attach cycle is meaningful. | |
| 29 | + | Alt+" " | |
| 30 | + | Sleep 550ms | |
| 31 | + | Type "RNS" | |
| 32 | + | Enter | |
| 33 | + | Sleep 550ms | |
| 34 | + | Backspace | |
| 35 | + | Type@95ms "local" | |
| 36 | + | Enter | |
| 37 | + | Sleep 1s | |
| 38 | + | ||
| 39 | + | # The picker starts with the local shell plus two ready remote sessions. | |
| 40 | + | Alt+" " | |
| 41 | + | Sleep 550ms | |
| 42 | + | Type "SSP" | |
| 43 | + | Enter | |
| 44 | + | Sleep 1.5s | |
| 45 | + | Down 2 | |
| 46 | + | Sleep 700ms | |
| 47 | + | Enter | |
| 48 | + | Sleep 2.5s | |
| 49 | + | ||
| 50 | + | # The selected work session belongs to the remote container. | |
| 30 | 51 | Type@85ms "eza " | |
| 31 | 52 | Sleep 200ms | |
| 32 | 53 | Type@75ms "-la " | |
| @@ -44,9 +65,7 @@ Enter | |||
| 44 | 65 | Sleep 1.5s | |
| 45 | 66 | ||
| 46 | 67 | # ── Session "agents": launch the bell job early so its | |
| 47 | - | # sleep overlaps the middle scenes. claude -p prints the | |
| 48 | - | # answer but emits no bell of its own (verified), so the | |
| 49 | - | # pane rings it with printf. ─────────────────────────── | |
| 68 | + | # sleep overlaps the middle scenes. ───────────────────── | |
| 50 | 69 | Alt+" " | |
| 51 | 70 | Sleep 550ms | |
| 52 | 71 | Type "CNS" | |
| @@ -56,13 +75,9 @@ Type@95ms "agents" | |||
| 56 | 75 | Enter | |
| 57 | 76 | Sleep 900ms | |
| 58 | 77 | Type@80ms "sleep " | |
| 59 | - | Type@140ms "10 " | |
| 60 | - | Sleep 170ms | |
| 61 | - | Type@90ms "&& claude " | |
| 62 | - | Type@150ms "-p " | |
| 63 | - | Type@105ms `"say hello world" ` | |
| 78 | + | Type@140ms "10" | |
| 64 | 79 | Sleep 220ms | |
| 65 | - | Type@85ms "&& printf " | |
| 80 | + | Type@85ms " && printf " | |
| 66 | 81 | Type@130ms `'\a'` | |
| 67 | 82 | Enter | |
| 68 | 83 | Sleep 1s | |
| @@ -148,35 +163,30 @@ Sleep 900ms | |||
| 148 | 163 | Alt+a | |
| 149 | 164 | Sleep 1.9s | |
| 150 | 165 | ||
| 151 | - | # ── Session picker now lists all three sessions ────────── | |
| 166 | + | # ── Session picker now lists local and remote sessions ─── | |
| 152 | 167 | Alt+" " | |
| 153 | 168 | Sleep 550ms | |
| 154 | 169 | Type "SSP" | |
| 155 | 170 | Enter | |
| 156 | - | Sleep 1.2s | |
| 157 | - | Down | |
| 158 | - | Sleep 450ms | |
| 159 | - | Down | |
| 160 | - | Sleep 450ms | |
| 161 | - | Down | |
| 171 | + | Sleep 1.5s | |
| 172 | + | Escape | |
| 162 | 173 | Sleep 650ms | |
| 163 | - | Enter | |
| 164 | - | Sleep 1.2s | |
| 165 | 174 | ||
| 166 | - | # ── Detach / re-attach: three sessions survive, ticker on ─ | |
| 175 | + | # ── Detach / re-attach: local sessions survive, ticker on ─ | |
| 167 | 176 | Alt+" " | |
| 168 | 177 | Sleep 550ms | |
| 169 | 178 | Type "DET" | |
| 170 | 179 | Enter | |
| 171 | 180 | Sleep 1s | |
| 172 | 181 | Type@90ms "vev " | |
| 173 | - | Type@75ms "ls" | |
| 182 | + | Type@75ms "ls " | |
| 183 | + | Type@120ms "--all" | |
| 174 | 184 | Enter | |
| 175 | 185 | Sleep 1.8s | |
| 176 | 186 | Type@90ms "vev " | |
| 177 | 187 | Sleep 150ms | |
| 178 | 188 | Type@80ms "attach " | |
| 179 | - | Type@140ms "work" | |
| 189 | + | Type@140ms "local" | |
| 180 | 190 | Enter | |
| 181 | 191 | Sleep 2.2s | |
| 182 | 192 | Sleep 400ms | |
| @@ -20,13 +20,34 @@ Set WaitTimeout 30s | |||||
| 20 | # (70-160ms) and short hesitations at word boundaries so the rhythm | 20 | # (70-160ms) and short hesitations at word boundaries so the rhythm | ||
| 21 | # reads irregular/human; palette codes (CNS/SSP/…) stay single-speed. | 21 | # reads irregular/human; palette codes (CNS/SSP/…) stay single-speed. | ||
| 22 | 22 | ||||
| 23 | - | # ── Session "work": a colourful split (eza + ticker) ───── | 23 | + | # ── Local and remote history, shown immediately ────────── |
| 24 | - | Type@90ms "vev " | 24 | + | Type@180ms "vev" |
| 25 | - | Sleep 160ms | 25 | + | Sleep 600ms |
| 26 | - | Type@75ms "new " | |||
| 27 | - | Type@140ms "work" | |||
| 28 | Enter | 26 | Enter | ||
| 29 | Sleep 1.1s | 27 | Sleep 1.1s | ||
| 28 | + | # Keep the initial local shell so the final detach/attach cycle is meaningful. | |||
| 29 | + | Alt+" " | |||
| 30 | + | Sleep 550ms | |||
| 31 | + | Type "RNS" | |||
| 32 | + | Enter | |||
| 33 | + | Sleep 550ms | |||
| 34 | + | Backspace | |||
| 35 | + | Type@95ms "local" | |||
| 36 | + | Enter | |||
| 37 | + | Sleep 1s | |||
| 38 | + | ||||
| 39 | + | # The picker starts with the local shell plus two ready remote sessions. | |||
| 40 | + | Alt+" " | |||
| 41 | + | Sleep 550ms | |||
| 42 | + | Type "SSP" | |||
| 43 | + | Enter | |||
| 44 | + | Sleep 1.5s | |||
| 45 | + | Down 2 | |||
| 46 | + | Sleep 700ms | |||
| 47 | + | Enter | |||
| 48 | + | Sleep 2.5s | |||
| 49 | + | ||||
| 50 | + | # The selected work session belongs to the remote container. | |||
| 30 | Type@85ms "eza " | 51 | Type@85ms "eza " | ||
| 31 | Sleep 200ms | 52 | Sleep 200ms | ||
| 32 | Type@75ms "-la " | 53 | Type@75ms "-la " | ||
| @@ -44,9 +65,7 @@ Enter | |||||
| 44 | Sleep 1.5s | 65 | Sleep 1.5s | ||
| 45 | 66 | ||||
| 46 | # ── Session "agents": launch the bell job early so its | 67 | # ── Session "agents": launch the bell job early so its | ||
| 47 | - | # sleep overlaps the middle scenes. claude -p prints the | 68 | + | # sleep overlaps the middle scenes. ───────────────────── |
| 48 | - | # answer but emits no bell of its own (verified), so the | |||
| 49 | - | # pane rings it with printf. ─────────────────────────── | |||
| 50 | Alt+" " | 69 | Alt+" " | ||
| 51 | Sleep 550ms | 70 | Sleep 550ms | ||
| 52 | Type "CNS" | 71 | Type "CNS" | ||
| @@ -56,13 +75,9 @@ Type@95ms "agents" | |||||
| 56 | Enter | 75 | Enter | ||
| 57 | Sleep 900ms | 76 | Sleep 900ms | ||
| 58 | Type@80ms "sleep " | 77 | Type@80ms "sleep " | ||
| 59 | - | Type@140ms "10 " | 78 | + | Type@140ms "10" |
| 60 | - | Sleep 170ms | |||
| 61 | - | Type@90ms "&& claude " | |||
| 62 | - | Type@150ms "-p " | |||
| 63 | - | Type@105ms `"say hello world" ` | |||
| 64 | Sleep 220ms | 79 | Sleep 220ms | ||
| 65 | - | Type@85ms "&& printf " | 80 | + | Type@85ms " && printf " |
| 66 | Type@130ms `'\a'` | 81 | Type@130ms `'\a'` | ||
| 67 | Enter | 82 | Enter | ||
| 68 | Sleep 1s | 83 | Sleep 1s | ||
| @@ -148,35 +163,30 @@ Sleep 900ms | |||||
| 148 | Alt+a | 163 | Alt+a | ||
| 149 | Sleep 1.9s | 164 | Sleep 1.9s | ||
| 150 | 165 | ||||
| 151 | - | # ── Session picker now lists all three sessions ────────── | 166 | + | # ── Session picker now lists local and remote sessions ─── |
| 152 | Alt+" " | 167 | Alt+" " | ||
| 153 | Sleep 550ms | 168 | Sleep 550ms | ||
| 154 | Type "SSP" | 169 | Type "SSP" | ||
| 155 | Enter | 170 | Enter | ||
| 156 | - | Sleep 1.2s | 171 | + | Sleep 1.5s |
| 157 | - | Down | 172 | + | Escape |
| 158 | - | Sleep 450ms | |||
| 159 | - | Down | |||
| 160 | - | Sleep 450ms | |||
| 161 | - | Down | |||
| 162 | Sleep 650ms | 173 | Sleep 650ms | ||
| 163 | - | Enter | |||
| 164 | - | Sleep 1.2s | |||
| 165 | 174 | ||||
| 166 | - | # ── Detach / re-attach: three sessions survive, ticker on ─ | 175 | + | # ── Detach / re-attach: local sessions survive, ticker on ─ |
| 167 | Alt+" " | 176 | Alt+" " | ||
| 168 | Sleep 550ms | 177 | Sleep 550ms | ||
| 169 | Type "DET" | 178 | Type "DET" | ||
| 170 | Enter | 179 | Enter | ||
| 171 | Sleep 1s | 180 | Sleep 1s | ||
| 172 | Type@90ms "vev " | 181 | Type@90ms "vev " | ||
| 173 | - | Type@75ms "ls" | 182 | + | Type@75ms "ls " |
| 183 | + | Type@120ms "--all" | |||
| 174 | Enter | 184 | Enter | ||
| 175 | Sleep 1.8s | 185 | Sleep 1.8s | ||
| 176 | Type@90ms "vev " | 186 | Type@90ms "vev " | ||
| 177 | Sleep 150ms | 187 | Sleep 150ms | ||
| 178 | Type@80ms "attach " | 188 | Type@80ms "attach " | ||
| 179 | - | Type@140ms "work" | 189 | + | Type@140ms "local" |
| 180 | Enter | 190 | Enter | ||
| 181 | Sleep 2.2s | 191 | Sleep 2.2s | ||
| 182 | Sleep 400ms | 192 | Sleep 400ms | ||
M scripts/demo/run.sh +43 -41
| @@ -3,50 +3,52 @@ set -euo pipefail | |||
| 3 | 3 | cd "$(dirname "$0")/../.." | |
| 4 | 4 | ||
| 5 | 5 | TAPE="${1:-demo.tape}" | |
| 6 | + | COMPOSE_FILE="scripts/demo/compose.yaml" | |
| 6 | 7 | install -d -m 0777 scripts/demo/out | |
| 8 | + | # Demo containers use a rootless subuid mapping, so reset generated state | |
| 9 | + | # from the same container identity rather than relying on host ownership. | |
| 10 | + | docker run --rm --entrypoint sh -v "$PWD/scripts/demo/out:/out" vev-demo -c 'rm -rf /out/local && mkdir -m 0777 /out/local' | |
| 11 | + | ||
| 12 | + | compose() { | |
| 13 | + | docker compose -f "$COMPOSE_FILE" "$@" | |
| 14 | + | } | |
| 7 | 15 | ||
| 8 | - | CLAUDE_TMP=$(mktemp -d) | |
| 9 | - | # claude writes files into the temp HOME as the container's demo user, which | |
| 10 | - | # rootless docker maps to a subuid the host cannot delete directly. Clean up | |
| 11 | - | # through the container first, where the creating uid can unlink its own | |
| 12 | - | # files, then remove the (now-empty) temp dir from the host. This never | |
| 13 | - | # suppresses a leftover: if anything still remains, it's reported so a | |
| 14 | - | # credentials-bearing directory doesn't silently linger in /tmp. | |
| 15 | 16 | cleanup() { | |
| 16 | - | # Every step here is best-effort: under `set -e`, an unguarded failure in an | |
| 17 | - | # EXIT trap would abort the trap early and replace the script's real exit | |
| 18 | - | # status with the failing cleanup command's. Guard each step with `|| true` | |
| 19 | - | # so cleanup always runs to completion and never masks the vhs run's result; | |
| 20 | - | # the final check below is what surfaces an unrecovered leftover. | |
| 21 | - | rm -rf "$CLAUDE_TMP" 2>/dev/null || true | |
| 22 | - | if [ -d "$CLAUDE_TMP" ]; then | |
| 23 | - | docker run --rm -v "$CLAUDE_TMP:/cleanup" --entrypoint sh vev-demo \ | |
| 24 | - | -c 'rm -rf /cleanup/.claude /cleanup/.claude.json' 2>/dev/null || true | |
| 25 | - | rmdir "$CLAUDE_TMP" 2>/dev/null || true | |
| 17 | + | compose down -v --remove-orphans >/dev/null 2>&1 || true | |
| 18 | + | } | |
| 19 | + | trap cleanup EXIT | |
| 20 | + | ||
| 21 | + | compose up -d remote | |
| 22 | + | ||
| 23 | + | ready="" | |
| 24 | + | for _ in $(seq 60); do | |
| 25 | + | if compose run --rm --entrypoint ssh client remote true >/dev/null 2>&1; then | |
| 26 | + | ready=yes | |
| 27 | + | break | |
| 26 | 28 | fi | |
| 27 | - | if [ -d "$CLAUDE_TMP" ]; then | |
| 28 | - | echo "warning: could not remove claude temp home, left at $CLAUDE_TMP" >&2 | |
| 29 | + | sleep 0.5 | |
| 30 | + | done | |
| 31 | + | if [ -z "$ready" ]; then | |
| 32 | + | echo "error: sshd on the remote container never accepted a connection" >&2 | |
| 33 | + | compose logs remote >&2 || true | |
| 34 | + | exit 1 | |
| 35 | + | fi | |
| 36 | + | ||
| 37 | + | start_remote_session() { | |
| 38 | + | local name=$1 scene=$2 | |
| 39 | + | if ! compose exec -u demo -T remote env VEV=demo-bootstrap SHELL=/usr/local/bin/demo-shell VEV_DEMO_SCENE="$scene" vev new "$name"; then | |
| 40 | + | echo "error: could not create remote session '$name'" >&2 | |
| 41 | + | return 1 | |
| 42 | + | fi | |
| 43 | + | if ! compose exec -u demo -T remote vev ls | grep -q "^$name"; then | |
| 44 | + | echo "error: remote session '$name' was not retained" >&2 | |
| 45 | + | compose exec -u demo -T remote vev ls >&2 || true | |
| 46 | + | return 1 | |
| 29 | 47 | fi | |
| 30 | - | return 0 | |
| 31 | 48 | } | |
| 32 | - | trap cleanup EXIT | |
| 33 | - | mkdir -p "$CLAUDE_TMP/.claude" | |
| 34 | - | cp "$HOME/.claude/.credentials.json" "$CLAUDE_TMP/.claude/" | |
| 35 | - | cp scripts/demo/claude-settings.json "$CLAUDE_TMP/.claude/settings.json" | |
| 36 | - | printf '{"hasCompletedOnboarding": true, "theme": "dark", "projects": {"/home/demo": {"hasTrustDialogAccepted": true, "hasCompletedProjectOnboarding": true, "projectOnboardingSeenCount": 1}}}\n' > "$CLAUDE_TMP/.claude.json" | |
| 37 | - | ||
| 38 | - | # Rootless docker maps host-owned files (uid 1000) to container root, but the | |
| 39 | - | # container runs as demo (uid 1000 -> a subuid), so demo cannot read the | |
| 40 | - | # credentials or write its config dir. Open up the throwaway home's contents so | |
| 41 | - | # claude can authenticate and write transcripts, then restore the mktemp | |
| 42 | - | # parent to 0700: bind mounts hand the container the inner paths directly, but | |
| 43 | - | # other host users would have to traverse the parent, which now blocks them. | |
| 44 | - | chmod -R a+rwX "$CLAUDE_TMP" | |
| 45 | - | chmod 0700 "$CLAUDE_TMP" | |
| 46 | - | ||
| 47 | - | docker run --rm --hostname vev-demo \ | |
| 48 | - | -v "$PWD/scripts/demo:/tape:ro" \ | |
| 49 | - | -v "$PWD/scripts/demo/out:/home/demo/out" \ | |
| 50 | - | -v "$CLAUDE_TMP/.claude:/home/demo/.claude" \ | |
| 51 | - | -v "$CLAUDE_TMP/.claude.json:/home/demo/.claude.json" \ | |
| 52 | - | vev-demo "/tape/$TAPE" | |
| 49 | + | ||
| 50 | + | start_remote_session work remote-deploys | |
| 51 | + | start_remote_session ticker remote-ticker | |
| 52 | + | ||
| 53 | + | compose run --rm --entrypoint vev client host add remote | |
| 54 | + | compose run --rm client "/tape/$TAPE" | |
| @@ -3,50 +3,52 @@ set -euo pipefail | |||||
| 3 | cd "$(dirname "$0")/../.." | 3 | cd "$(dirname "$0")/../.." | ||
| 4 | 4 | ||||
| 5 | TAPE="${1:-demo.tape}" | 5 | TAPE="${1:-demo.tape}" | ||
| 6 | + | COMPOSE_FILE="scripts/demo/compose.yaml" | |||
| 6 | install -d -m 0777 scripts/demo/out | 7 | install -d -m 0777 scripts/demo/out | ||
| 8 | + | # Demo containers use a rootless subuid mapping, so reset generated state | |||
| 9 | + | # from the same container identity rather than relying on host ownership. | |||
| 10 | + | docker run --rm --entrypoint sh -v "$PWD/scripts/demo/out:/out" vev-demo -c 'rm -rf /out/local && mkdir -m 0777 /out/local' | |||
| 11 | + | ||||
| 12 | + | compose() { | |||
| 13 | + | docker compose -f "$COMPOSE_FILE" "$@" | |||
| 14 | + | } | |||
| 7 | 15 | ||||
| 8 | - | CLAUDE_TMP=$(mktemp -d) | |||
| 9 | - | # claude writes files into the temp HOME as the container's demo user, which | |||
| 10 | - | # rootless docker maps to a subuid the host cannot delete directly. Clean up | |||
| 11 | - | # through the container first, where the creating uid can unlink its own | |||
| 12 | - | # files, then remove the (now-empty) temp dir from the host. This never | |||
| 13 | - | # suppresses a leftover: if anything still remains, it's reported so a | |||
| 14 | - | # credentials-bearing directory doesn't silently linger in /tmp. | |||
| 15 | cleanup() { | 16 | cleanup() { | ||
| 16 | - | # Every step here is best-effort: under `set -e`, an unguarded failure in an | 17 | + | compose down -v --remove-orphans >/dev/null 2>&1 || true |
| 17 | - | # EXIT trap would abort the trap early and replace the script's real exit | 18 | + | } |
| 18 | - | # status with the failing cleanup command's. Guard each step with `|| true` | 19 | + | trap cleanup EXIT |
| 19 | - | # so cleanup always runs to completion and never masks the vhs run's result; | 20 | + | |
| 20 | - | # the final check below is what surfaces an unrecovered leftover. | 21 | + | compose up -d remote |
| 21 | - | rm -rf "$CLAUDE_TMP" 2>/dev/null || true | 22 | + | |
| 22 | - | if [ -d "$CLAUDE_TMP" ]; then | 23 | + | ready="" |
| 23 | - | docker run --rm -v "$CLAUDE_TMP:/cleanup" --entrypoint sh vev-demo \ | 24 | + | for _ in $(seq 60); do |
| 24 | - | -c 'rm -rf /cleanup/.claude /cleanup/.claude.json' 2>/dev/null || true | 25 | + | if compose run --rm --entrypoint ssh client remote true >/dev/null 2>&1; then |
| 25 | - | rmdir "$CLAUDE_TMP" 2>/dev/null || true | 26 | + | ready=yes |
| 27 | + | break | |||
| 26 | fi | 28 | fi | ||
| 27 | - | if [ -d "$CLAUDE_TMP" ]; then | 29 | + | sleep 0.5 |
| 28 | - | echo "warning: could not remove claude temp home, left at $CLAUDE_TMP" >&2 | 30 | + | done |
| 31 | + | if [ -z "$ready" ]; then | |||
| 32 | + | echo "error: sshd on the remote container never accepted a connection" >&2 | |||
| 33 | + | compose logs remote >&2 || true | |||
| 34 | + | exit 1 | |||
| 35 | + | fi | |||
| 36 | + | ||||
| 37 | + | start_remote_session() { | |||
| 38 | + | local name=$1 scene=$2 | |||
| 39 | + | if ! compose exec -u demo -T remote env VEV=demo-bootstrap SHELL=/usr/local/bin/demo-shell VEV_DEMO_SCENE="$scene" vev new "$name"; then | |||
| 40 | + | echo "error: could not create remote session '$name'" >&2 | |||
| 41 | + | return 1 | |||
| 42 | + | fi | |||
| 43 | + | if ! compose exec -u demo -T remote vev ls | grep -q "^$name"; then | |||
| 44 | + | echo "error: remote session '$name' was not retained" >&2 | |||
| 45 | + | compose exec -u demo -T remote vev ls >&2 || true | |||
| 46 | + | return 1 | |||
| 29 | fi | 47 | fi | ||
| 30 | - | return 0 | |||
| 31 | } | 48 | } | ||
| 32 | - | trap cleanup EXIT | 49 | + | |
| 33 | - | mkdir -p "$CLAUDE_TMP/.claude" | 50 | + | start_remote_session work remote-deploys |
| 34 | - | cp "$HOME/.claude/.credentials.json" "$CLAUDE_TMP/.claude/" | 51 | + | start_remote_session ticker remote-ticker |
| 35 | - | cp scripts/demo/claude-settings.json "$CLAUDE_TMP/.claude/settings.json" | 52 | + | |
| 36 | - | printf '{"hasCompletedOnboarding": true, "theme": "dark", "projects": {"/home/demo": {"hasTrustDialogAccepted": true, "hasCompletedProjectOnboarding": true, "projectOnboardingSeenCount": 1}}}\n' > "$CLAUDE_TMP/.claude.json" | 53 | + | compose run --rm --entrypoint vev client host add remote |
| 37 | - | 54 | + | compose run --rm client "/tape/$TAPE" | |
| 38 | - | # Rootless docker maps host-owned files (uid 1000) to container root, but the | |||
| 39 | - | # container runs as demo (uid 1000 -> a subuid), so demo cannot read the | |||
| 40 | - | # credentials or write its config dir. Open up the throwaway home's contents so | |||
| 41 | - | # claude can authenticate and write transcripts, then restore the mktemp | |||
| 42 | - | # parent to 0700: bind mounts hand the container the inner paths directly, but | |||
| 43 | - | # other host users would have to traverse the parent, which now blocks them. | |||
| 44 | - | chmod -R a+rwX "$CLAUDE_TMP" | |||
| 45 | - | chmod 0700 "$CLAUDE_TMP" | |||
| 46 | - | ||||
| 47 | - | docker run --rm --hostname vev-demo \ | |||
| 48 | - | -v "$PWD/scripts/demo:/tape:ro" \ | |||
| 49 | - | -v "$PWD/scripts/demo/out:/home/demo/out" \ | |||
| 50 | - | -v "$CLAUDE_TMP/.claude:/home/demo/.claude" \ | |||
| 51 | - | -v "$CLAUDE_TMP/.claude.json:/home/demo/.claude.json" \ | |||
| 52 | - | vev-demo "/tape/$TAPE" | |||