diff --git a/.devcontainer/codespaces/cloud-session.test.mjs b/.devcontainer/codespaces/cloud-session.test.mjs index 9e5264eb21f..ac41a012279 100644 --- a/.devcontainer/codespaces/cloud-session.test.mjs +++ b/.devcontainer/codespaces/cloud-session.test.mjs @@ -27,6 +27,14 @@ if (args[0] === 'codespace' && args[1] === 'list') { `, ); chmodSync(mockGh, 0o755); + for (const [name, script] of Object.entries({ + infocmp: '[ "$1" = "$SUPPORTED_TERM" ]', + tmux: 'printf "%s" "$TERM"', + })) { + const executable = join(fixtureDir, name); + writeFileSync(executable, `#!/bin/sh\n${script}\n`); + chmodSync(executable, 0o755); + } }); after(() => rmSync(fixtureDir, { recursive: true, force: true })); @@ -58,6 +66,34 @@ function remoteCommand(args) { return invocationArgs[6]; } +for (const { name, supportedTerm, expectedTerm } of [ + { + name: 'uses xterm-256color when the remote terminal definition is missing', + supportedTerm: 'xterm-256color', + expectedTerm: 'xterm-256color', + }, + { + name: 'keeps the terminal type when the remote definition is available', + supportedTerm: 'xterm-ghostty', + expectedTerm: 'xterm-ghostty', + }, +]) { + test(name, () => { + const result = spawnSync('/bin/sh', ['-c', remoteCommand(['--opencode', '--legacy'])], { + encoding: 'utf8', + env: { + ...process.env, + PATH: `${fixtureDir}:${process.env.PATH}`, + TERM: 'xterm-ghostty', + SUPPORTED_TERM: supportedTerm, + }, + }); + + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, expectedTerm); + }); +} + test('starts a named OpenCode session in a worktree', () => { const command = remoteCommand(['--opencode', 'fix-flaky', '--legacy', '--model', 'test']); diff --git a/scripts/cloud-session.mjs b/scripts/cloud-session.mjs index 7dc54aa00f4..e84e840e686 100644 --- a/scripts/cloud-session.mjs +++ b/scripts/cloud-session.mjs @@ -23,6 +23,11 @@ const MACHINE = 'premiumLinux'; const gh = (...args) => execFileSync('gh', args, { encoding: 'utf8' }).trim(); const ghTty = (...args) => spawnSync('gh', args, { stdio: 'inherit' }); +// Check the remote terminal database before tmux starts. Older images can lack +// terminal definitions such as xterm-ghostty. +const TERMINAL_FALLBACK = + 'if ! infocmp "$TERM" >/dev/null 2>&1; then export TERM=xterm-256color; fi'; + function findCodespace(retry = false) { try { const list = JSON.parse(gh('codespace', 'list', '-R', REPO, '--json', 'name,state')); @@ -237,7 +242,7 @@ switch (cmd) { name, '--', '-t', - `tmux new -As ${tmuxSession} '${remoteCommand(cmd, launcher, rest.join(' '))}'`, + `${TERMINAL_FALLBACK}; tmux new -As ${tmuxSession} '${remoteCommand(cmd, launcher, rest.join(' '))}'`, ); process.exitCode = status ?? 1; }