Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/specs/vscode.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Frontend Library (lib/src/)
- **Save before kill.** Deactivate must save session state *before* killing PTYs. CWD and scrollback queries need live processes. See ordering in `extension.ts:deactivate()`.
- **Alarm state is global.** A single `AlarmManager` instance in `message-router.ts` is shared across all routers and survives router disposal. PTY data feeds into it at module level, regardless of webview visibility.
- **PTY ownership.** Each router tracks its PTYs in `ownedPtyIds`. A module-level `globalOwnedPtyIds` set prevents a reconnecting router from stealing PTYs owned by another webview.
- **Shell login args are shell-specific.** The shared `pty-core.js` launches POSIX shells with `-l` only for shells that accept it. `csh`/`tcsh` must be spawned without `-l` so both the standalone app and VS Code extension can open a usable terminal for users whose login shell is C shell-derived.
- **mergeAlarmStates on every save path.** Both the frontend periodic save (`onSaveState` callback) and the backend deactivate refresh (`refreshSavedSessionStateFromPtys`) must merge current alarm states. Missing this causes alarm state to revert on restore.
- **Scrollback trailing newline.** Restored scrollback must end with `\n` to avoid zsh printing a `%` artifact at the top of the terminal.
- **retainContextWhenHidden.** Set on `WebviewPanel` (editor tabs) but NOT on `WebviewView` (bottom panel). The view relies on reconnect/replay when it becomes visible again; the panel keeps its DOM alive.
Expand Down
17 changes: 15 additions & 2 deletions standalone/sidecar/pty-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ function resolveDefaultShell(platform = process.platform, env = process.env) {
return env.SHELL || '/bin/sh';
}

const LOGIN_ARG_UNSUPPORTED_SHELLS = new Set(['csh', 'tcsh']);

function resolveLoginArg(shell, platform = process.platform) {
if (platform === 'win32') {
return [];
}

const shellName = path.posix.basename(shell || '').toLowerCase();
return LOGIN_ARG_UNSUPPORTED_SHELLS.has(shellName) ? [] : ['-l'];
}

function resolveDefaultCwd(platform = process.platform, env = process.env, osModule = os) {
const homedir = safeResolve(() => osModule.homedir());
const tmpdir = safeResolve(() => osModule.tmpdir());
Expand Down Expand Up @@ -51,14 +62,16 @@ function resolveSpawnConfig(options, runtime = {}) {
const fsModule = runtime.fsModule || fs;
const defaultCwd = resolveDefaultCwd(platform, env, osModule);
const missingExplicitCwd = Boolean(cwd) && !directoryExists(cwd, fsModule);
const shell = resolveDefaultShell(platform, env);

return {
cols,
rows,
cwd: missingExplicitCwd ? defaultCwd : (cwd || defaultCwd),
cwdWarning: missingExplicitCwd ? `unable to restore because directory ${cwd} was removed` : null,
env: { ...env, TERM_PROGRAM: 'MouseTerm' },
shell: resolveDefaultShell(platform, env),
shell,
loginArg: resolveLoginArg(shell, platform),
};
}

Expand Down Expand Up @@ -160,7 +173,7 @@ module.exports.create = function create(send, ptyModule) {

let p;
try {
p = pty.spawn(config.shell, [], {
p = pty.spawn(config.shell, config.loginArg, {
name: 'xterm-256color',
cols: config.cols,
rows: config.rows,
Expand Down
31 changes: 31 additions & 0 deletions standalone/sidecar/pty-core.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ test('resolveSpawnConfig uses POSIX shell and home defaults', () => {
assert.equal(config.cols, 80);
assert.equal(config.rows, 30);
assert.equal(config.env.TERM_PROGRAM, 'MouseTerm');
assert.deepEqual(config.loginArg, ['-l']);
});

test('resolveSpawnConfig uses Windows shell and profile defaults', () => {
Expand All @@ -35,6 +36,7 @@ test('resolveSpawnConfig uses Windows shell and profile defaults', () => {
assert.equal(config.cwd, 'C:\\Users\\tester');
assert.equal(config.cwdWarning, null);
assert.equal(config.env.TERM_PROGRAM, 'MouseTerm');
assert.deepEqual(config.loginArg, []);
});

test('resolveSpawnConfig preserves explicit cwd', () => {
Expand All @@ -58,6 +60,21 @@ test('resolveSpawnConfig preserves explicit cwd', () => {
assert.equal(config.cwdWarning, null);
assert.equal(config.cols, 120);
assert.equal(config.rows, 40);
assert.deepEqual(config.loginArg, ['-l']);
});

test('resolveSpawnConfig skips -l for csh-style shells that reject it', () => {
const config = resolveSpawnConfig(undefined, {
platform: 'linux',
env: { SHELL: '/bin/tcsh' },
osModule: {
homedir: () => '/home/tester',
tmpdir: () => '/tmp/fallback',
},
});

assert.equal(config.shell, '/bin/tcsh');
assert.deepEqual(config.loginArg, []);
});

test('resolveSpawnConfig falls back to the default directory when explicit cwd is missing', () => {
Expand All @@ -83,6 +100,20 @@ test('resolveSpawnConfig falls back to the default directory when explicit cwd i
assert.equal(config.rows, 40);
});

test('resolveSpawnConfig skips -l for csh', () => {
const config = resolveSpawnConfig(undefined, {
platform: 'darwin',
env: { SHELL: '/bin/csh' },
osModule: {
homedir: () => '/Users/tester',
tmpdir: () => '/tmp/fallback',
},
});

assert.equal(config.shell, '/bin/csh');
assert.deepEqual(config.loginArg, []);
});

test('create buffers scrollback for getScrollback requests', () => {
const events = [];
const listeners = {};
Expand Down
9 changes: 7 additions & 2 deletions vscode-ext/src/message-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,15 @@ export function attachRouter(
// Route webview messages to the PTY manager
const messageDisposable = webview.onDidReceiveMessage((msg: WebviewMessage) => {
switch (msg.type) {
case 'pty:spawn':
case 'pty:spawn': {
claim(msg.id);
ptyManager.spawn(msg.id, msg.options);
const spawnOptions = { ...msg.options };
if (!spawnOptions.cwd) {
spawnOptions.cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
}
ptyManager.spawn(msg.id, spawnOptions);
break;
}
case 'pty:input':
ptyManager.write(msg.id, msg.data);
break;
Expand Down
Loading