From dc38ace612fd02a640d5718f166ac3dd910035b0 Mon Sep 17 00:00:00 2001 From: Raphael Vigee Date: Thu, 8 Oct 2026 11:47:00 +0200 Subject: [PATCH 1/2] feat(playground): focus the terminal on ready, bash completion for heph - The terminal was focused while its element was still hidden, which is a no-op; focus it after the render that reveals it. - Install generates heph's bash completion (`heph tool completions bash`) and the login shell sources it. Releases without the command go without. Co-Authored-By: Claude Opus 5.5 --- website/src/components/playground/Playground.tsx | 7 ++++++- website/src/components/playground/vm.ts | 13 +++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/website/src/components/playground/Playground.tsx b/website/src/components/playground/Playground.tsx index 2d59941..9691f15 100644 --- a/website/src/components/playground/Playground.tsx +++ b/website/src/components/playground/Playground.tsx @@ -110,6 +110,12 @@ export function Playground() { useEffect(() => teardown, [teardown]); + // The terminal stays hidden until the VM is ready, and a hidden element + // can't take focus: focus it once the render that reveals it is done. + useEffect(() => { + if (status.kind === 'ready') termRef.current?.xterm.focus(); + }, [status.kind]); + const release = releases?.find((r) => r.tag === tag) ?? (picked?.tag === tag ? picked : undefined); @@ -151,7 +157,6 @@ export function Playground() { }, }); await enterShell(vm); - termRef.current.xterm.focus(); setStatus({ kind: 'ready', vcpus, restored }); } catch (e) { if (!ac.signal.aborted) setStatus({ kind: 'error', message: errorMessage(e) }); diff --git a/website/src/components/playground/vm.ts b/website/src/components/playground/vm.ts index 4fa41b8..cc07094 100644 --- a/website/src/components/playground/vm.ts +++ b/website/src/components/playground/vm.ts @@ -41,6 +41,8 @@ int fcntl64(int fd, int cmd, long arg) { return fcntl(fd, cmd, arg); } `; const HEPH_BIN = '/opt/heph/bin/heph'; const COMPAT_LIB = '/opt/heph/lib/libhephcompat.so'; +// heph's bash completion, generated by the installed release. +const COMPLETION = '/etc/heph-completion.bash'; type SetupStep = | { run: string } @@ -69,7 +71,12 @@ const BASE_SETUP: SetupStep[] = [ }, { write: '/etc/profile.d/heph.sh', - content: ['export PS1="\\w \\$ "', 'cat /etc/heph-welcome', ''].join('\n'), + content: [ + 'export PS1="\\w \\$ "', + `[ -n "$BASH_VERSION" ] && [ -r ${COMPLETION} ] && . ${COMPLETION}`, + 'cat /etc/heph-welcome', + '', + ].join('\n'), }, ]; @@ -280,7 +287,9 @@ export async function bootPlayground( 'mkdir -p /root/examples && { mountpoint -q /root/examples || mount -t tmpfs tmpfs /root/examples; }', ].join(' && '); await vm.writeFile('/tmp/heph-install.sh', script); - await run(vm, `${mounts} && sh /tmp/heph-install.sh && rm /tmp/heph-install.sh && heph version`); + // Releases without `heph tool completions` just go without completion. + const completion = `{ heph tool completions bash > ${COMPLETION} 2>/dev/null || rm -f ${COMPLETION}; }`; + await run(vm, `${mounts} && sh /tmp/heph-install.sh && rm /tmp/heph-install.sh && ${completion} && heph version`); } catch (e) { vm.dispose(); throw e; From 6f002e6ff8ca39d3aa7fb0decacc6a9fc127e593 Mon Sep 17 00:00:00 2001 From: Raphael Vigee Date: Thu, 8 Oct 2026 11:47:28 +0200 Subject: [PATCH 2/2] refactor(playground): run() takes a list of commands and chains them Co-Authored-By: Claude Opus 5.5 --- website/src/components/playground/vm.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/website/src/components/playground/vm.ts b/website/src/components/playground/vm.ts index cc07094..ebf97b0 100644 --- a/website/src/components/playground/vm.ts +++ b/website/src/components/playground/vm.ts @@ -89,7 +89,9 @@ const PLUGIN_OPTIONS: Record = { 'heph-go-plugin': ' options:\n gotool: host\n', }; -async function run(vm: Vm, command: string, timeoutMs = 300_000): Promise { +/** Run `commands` in one exec, stopping at the first that fails. */ +async function run(vm: Vm, commands: string | string[], timeoutMs = 300_000): Promise { + const command = [commands].flat().join(' && '); const { output, exitCode } = await vm.exec(command, { timeoutMs }); if (exitCode !== 0) { throw new Error(`\`${command}\` exited ${exitCode}:\n${output.slice(-2000)}`); @@ -279,17 +281,19 @@ export async function bootPlayground( const script = files.map(([path, content]) => ( `mkdir -p '${path.replace(/\/[^/]+$/, '')}'\ncat > '${path}' <<'HEPH_PLAYGROUND_EOF'\n${content}${content.endsWith('\n') ? '' : '\n'}HEPH_PLAYGROUND_EOF\n` )).join(''); - // Mounts the image lacks: pseudo-terminals for `heph run --shell`, and, - // for the examples, a filesystem with extended attributes, which - // `codegen = "copy"` needs (the root filesystem has none). - const mounts = [ + await vm.writeFile('/tmp/heph-install.sh', script); + await run(vm, [ + // Mounts the image lacks: pseudo-terminals for `heph run --shell`, and, + // for the examples, a filesystem with extended attributes, which + // `codegen = "copy"` needs (the root filesystem has none). 'mountpoint -q /dev/pts || { mkdir -p /dev/pts && mount -t devpts devpts /dev/pts; }', 'mkdir -p /root/examples && { mountpoint -q /root/examples || mount -t tmpfs tmpfs /root/examples; }', - ].join(' && '); - await vm.writeFile('/tmp/heph-install.sh', script); - // Releases without `heph tool completions` just go without completion. - const completion = `{ heph tool completions bash > ${COMPLETION} 2>/dev/null || rm -f ${COMPLETION}; }`; - await run(vm, `${mounts} && sh /tmp/heph-install.sh && rm /tmp/heph-install.sh && ${completion} && heph version`); + 'sh /tmp/heph-install.sh', + 'rm /tmp/heph-install.sh', + // Releases without `heph tool completions` just go without completion. + `{ heph tool completions bash > ${COMPLETION} 2>/dev/null || rm -f ${COMPLETION}; }`, + 'heph version', + ]); } catch (e) { vm.dispose(); throw e;