Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e3cdf20
fix: show viewer session PID
Fred-Wu Oct 5, 2026
255f8fc
test: cover viewer session PID switching
Fred-Wu Oct 5, 2026
a8cd3ec
test: update viewer panel mocks
Fred-Wu Oct 5, 2026
9a1c05e
fix: remove unused session status globals
Fred-Wu Oct 5, 2026
e295a18
fix: preserve focused viewer PID
Fred-Wu Oct 5, 2026
a63ed53
test: preserve viewer PID on workspace refresh
Fred-Wu Oct 5, 2026
b2b1ce4
fix: restore active PID after viewer focus
Fred-Wu Oct 5, 2026
386eafd
test: cover leaving and switching viewers
Fred-Wu Oct 5, 2026
34e47a6
test: cover viewer to editor PID restoration
Fred-Wu Oct 5, 2026
b7da006
test: remove flaky viewer focus integration coverage
Fred-Wu Oct 5, 2026
227e89b
fix: preserve viewer PID only during workspace refresh
Fred-Wu Oct 5, 2026
a1600c6
fix: remove unused viewer session state
Fred-Wu Oct 5, 2026
e708fbc
fix: restore viewer PID after terminal focus
Fred-Wu Oct 5, 2026
d1eb898
fix: reconcile viewer PID on session cleanup
Fred-Wu Oct 5, 2026
80a04e8
fix: restore terminal PID when viewer loses focus
Fred-Wu Oct 5, 2026
a2e0342
fix: defer viewer blur status fallback
Fred-Wu Oct 5, 2026
958657c
fix: keep disconnected viewer status detached
Fred-Wu Oct 5, 2026
bde05c7
fix: preserve viewer PID without an active terminal
Fred-Wu Oct 5, 2026
16a8c06
fix: ignore stale viewer focus messages
Fred-Wu Oct 5, 2026
1c2850e
fix: make viewer blur status deterministic
Fred-Wu Oct 6, 2026
466929c
fix: revert terminal status cache and update PID from active session-…
Fred-Wu Oct 6, 2026
f71cc84
fix: separate viewer PID display from terminal session activation
Fred-Wu Oct 7, 2026
8f2dfa0
Merge upstream/main into feature/viewer-session-pid
Fred-Wu Oct 7, 2026
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 src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<apiImp
});
}));
context.subscriptions.push(vscode.window.onDidChangeActiveTerminal(session.switchSessionByTerminal));
void session.switchSessionByTerminal(vscode.window.activeTerminal);

// start language service
if (util.config().get<boolean>('lsp.enabled')) {
Expand Down
6 changes: 6 additions & 0 deletions src/listViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ export function getListViewerScript(documentGeneration: number, initial: ListVie
return `
const vscode = acquireVsCodeApi();
const documentGeneration = ${documentGeneration};
window.addEventListener('focus', () => {
vscode.postMessage({ message: 'dataview/focus', documentGeneration });
});
window.addEventListener('blur', () => {
vscode.postMessage({ message: 'dataview/blur', documentGeneration });
});
let nextRequestId = 0;
const viewer = (${createListViewer.toString()})({
root: document.body, list: document.getElementById('list'), back: document.getElementById('back'), reset: document.getElementById('reset'),
Expand Down
172 changes: 149 additions & 23 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,6 @@ export let requestFile: string;
export let requestLockFile: string;
export let sessionDir: string;
export let workingDir: string;
let rVer: string;
let pid: string;
let info: SessionInfo;
export let globalPipePath: string | undefined;
export let workspaceFile: string;

Expand All @@ -112,6 +109,7 @@ const terminalRegistry = new TerminalSessionRegistry<Session, vscode.Terminal>(
terminal => !terminal.exitStatus && window.terminals.includes(terminal),
);
const documentSessions = new Map<string, Session>();
let focusedDataViewSessionId: string | null = null;
const sessionDocumentBound = new vscode.EventEmitter<Uri>();
export const onDidBindSessionDocument = sessionDocumentBound.event;

Expand All @@ -135,8 +133,14 @@ export function unbindSessionDocument(uri: Uri): void { documentSessions.delete(
export function unregisterSessionTransport(target: Session): void {
for (const [uri, owner] of documentSessions) { if (owner === target) { documentSessions.delete(uri); } }
terminalRegistry.releaseSession(target);
if (sessions.get(target.sessionId) === target) { sessions.delete(target.sessionId); }
if (activeSession === target) { void clearActiveSession(); }
const current = sessions.get(target.sessionId) === target;
const focused = current && focusedDataViewPanel && focusedDataViewSessionId === target.sessionId;
if (current) { sessions.delete(target.sessionId); }
if (activeSession === target) {
void clearActiveSession();
} else if (focused) {
resetStatusBar();
}
}

function clearActiveSession(): Promise<void> {
Expand All @@ -146,7 +150,15 @@ function clearActiveSession(): Promise<void> {
activeSession = undefined;
workspaceData = { search: [], loaded_namespaces: [], globalenv: {} };
workingDir = '';
resetStatusBar();

const focusedSession = sessions.get(focusedDataViewSessionId ?? '');

if (focusedDataViewPanel && focusedSession) {
updateSessionStatusBar(focusedSession);
} else {
resetStatusBar();
}

rWorkspace?.refresh();
return setContext('rSessionActive', false);
}
Expand Down Expand Up @@ -181,7 +193,7 @@ export async function executeSessionCode(target: Session, code: string): Promise
export function updateSessionWorkspace(target: Session, data: WorkspaceData): void {
target.workspaceData = data;
if (activeSession === target) {
void refreshActiveSession(target);
void refreshActiveSession(target, false);
}
}

Expand All @@ -201,6 +213,9 @@ export function registerSessionTransport(id: string, host: string, directory: st
sessions.set(id, target);
return target;
}
// An unattached terminal has no session to restore after a viewer loses focus.
let selectedTerminalDetached = false;

/** Wait for the same connected owner used by execution and terminal selection. */
export function waitForTerminalReady(terminal: vscode.Terminal, timeout = 30000): Promise<boolean> {
return new Promise(resolve => {
Expand Down Expand Up @@ -269,6 +284,7 @@ interface DataViewRequestMessage {
const dynamicDataViewPanels = new Map<string, vscode.WebviewPanel>();
const dynamicDataViewStateGenerations = new WeakMap<vscode.WebviewPanel, number>();
const documentGenerations = new WeakMap<Webview, number>();
let focusedDataViewPanel: vscode.WebviewPanel | undefined;
let documentGenerationRevision = 0;

function escapeHtml(text: string): string {
Expand All @@ -282,6 +298,35 @@ function escapeHtml(text: string): string {
return text.replace(/[&<>"']/g, c => map[c]);
}

function focusDataViewPanel(panel: vscode.WebviewPanel, sessionId: string | null): void {
focusedDataViewPanel = panel;
focusedDataViewSessionId = sessionId;

const session = sessions.get(sessionId ?? '');
if (session) {
updateSessionStatusBar(session);
} else {
resetStatusBar();
}
}

function blurDataViewPanel(panel: vscode.WebviewPanel): void {
if (focusedDataViewPanel !== panel) {
return;
}

focusedDataViewPanel = undefined;
focusedDataViewSessionId = null;

if (selectedTerminalDetached) {
resetStatusBar();
} else if (activeSession) {
updateSessionStatusBar(activeSession);
Comment on lines +323 to +324

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Restore the selected terminal's display when refocusing it

This fallback always displays activeSession, which can differ from the still-selected terminal after choosing an Interactive session. VS Code does not emit onDidChangeActiveTerminal when keyboard focus returns to the same terminal, so switchSessionByTerminal() cannot correct the display. Reproduction on macOS arm64 / VS Code 1.140.0: (1) select attached terminal B, (2) open/focus Interactive session A without changing the terminal selection, (3) open and focus A's expanded table or list viewer, (4) focus terminal B again, without selecting another terminal. The actual dataview/blur arrives with panel.active === true; the terminal-selection event count stays unchanged; this branch leaves A's PID displayed in B's terminal. The live Interactive table check recorded actual R 4.6.1: 35567 versus terminal B's expected R 4.6.0: 35770, with one active-terminal event before and after refocus. Both native viewer checks reproduce the same result. Please restore B's display for a real terminal-focus transition while preserving the sidebar fix and leaving activeSession unchanged. This needs focus information beyond panel.active, since both sidebar and terminal focus leave that flag true.

To adapt the real-panel reproduction patch I posted earlier, keep the setup that selects B then explicitly activates A, and replace the workbench.view.explorer transition with:

const before = terminalEvents; // increment in onDidChangeActiveTerminal
terminal.show(false);
await vscode.commands.executeCommand('workbench.action.terminal.focus');
await reviewWait(() => messages.some(message => message.message === 'dataview/blur'));
await new Promise(resolve => setTimeout(resolve, 100));
assert.strictEqual(terminalEvents, before);
assert.strictEqual(session.activeSession?.sessionId, a.sessionId);
assert.strictEqual(statusBar.text, `R 4.6.0: ${terminalPid}`);

Do not call switchSessionByTerminal() after refocusing B in this check: doing so supplies the missing event and masks the display regression.

} else {
resetStatusBar();
}
}

function registerDataViewPanel(
panel: vscode.WebviewPanel, key: string, viewId: string, sessionId: string | null,
stateGeneration?: number,
Expand All @@ -292,7 +337,16 @@ function registerDataViewPanel(
if (stateGeneration !== undefined) {
dynamicDataViewStateGenerations.set(panel, stateGeneration);
}
panel.onDidChangeViewState(({ webviewPanel }) => {
if (!webviewPanel.active) {
blurDataViewPanel(panel);
return;
Comment on lines +340 to +343

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Restore the active session's PID when leaving a viewer

This listener writes A's PID when A's viewer gains focus, but returns immediately when it loses focus. With B still the active session, switching from A's viewer back to an ordinary, unbound .R editor therefore leaves A's PID displayed even though session-based completions and workspace actions still use B. The regular text-editor handler only tracks the editor; it does not restore this status item. I reproduced the transition using a real VS Code WebviewPanel followed by showTextDocument(): the panel becomes inactive, activeSession remains B, and the status still shows A. Please restore the appropriate session display when no session-owned viewer is active, and cover the viewer-to-editor transition as well as switching between viewers.

}

focusDataViewPanel(panel, sessionId);
});
panel.onDidDispose(() => {
blurDataViewPanel(panel);
documentGenerations.delete(webview);
const currentStateGeneration = dynamicDataViewStateGenerations.get(panel);
dynamicDataViewStateGenerations.delete(panel);
Expand Down Expand Up @@ -328,6 +382,20 @@ function attachDynamicDataViewBridge(panel: vscode.WebviewPanel, viewId: string,
};

webview.onDidReceiveMessage(async (raw: unknown) => {
const focusMessage = raw as { message?: string; documentGeneration?: number };
if (focusMessage.message === 'dataview/focus' || focusMessage.message === 'dataview/blur') {
if (focusMessage.documentGeneration === documentGenerations.get(webview)) {
if (focusMessage.message === 'dataview/focus') {
if (panel.active) {
focusDataViewPanel(panel, sessionId);
}
} else {
blurDataViewPanel(panel);
}
}
return;
}

const msg = raw as Partial<DataViewRequestMessage>;
if (msg.message !== 'dataview/request' || typeof msg.requestId !== 'number' ||
typeof msg.documentGeneration !== 'number' ||
Expand Down Expand Up @@ -887,6 +955,7 @@ export async function shutdownSessionWatcher(): Promise<void> {
activeConnections.clear();
pendingSessionAttachments.clear();
pipeClient = undefined;
sessionSelectionRevision++;

if (globalSessionServer) {
await new Promise<void>((resolve) => {
Expand Down Expand Up @@ -1170,6 +1239,16 @@ export async function showDataView(
if (message.documentGeneration !== documentGenerations.get(webview)) {
return;
}
if (message.message === 'dataview/focus') {
if (panel.active) {
focusDataViewPanel(panel, sessionId);
}
return;
}
if (message.message === 'dataview/blur') {
blurDataViewPanel(panel);
return;
}
if (!Array.isArray(message.path) || !message.path.every(index => Number.isSafeInteger(index) && index > 0)) {
return;
}
Expand Down Expand Up @@ -1409,6 +1488,12 @@ export async function getTableHtml(webview: Webview, file: string | undefined, t
<script>
const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : { postMessage: () => {} };
const documentGeneration = ${documentGeneration};
window.addEventListener('focus', () => {
vscode.postMessage({ message: 'dataview/focus', documentGeneration });
});
window.addEventListener('blur', () => {
vscode.postMessage({ message: 'dataview/blur', documentGeneration });
});
let requestIdSeq = 1;
const pending = new Map();
let gridApi;
Expand Down Expand Up @@ -1977,10 +2062,16 @@ export function getListHtml(

import * as rstudioapi from './rstudioapi';

export async function activateSession(session: Session): Promise<void> {
export async function activateSession(session: Session, updateStatus = true): Promise<void> {
sessionSelectionRevision++;
selectedTerminalDetached = false;
if (updateStatus) {
focusedDataViewPanel = undefined;
focusedDataViewSessionId = null;
}

activeSession = session;
const refreshed = refreshActiveSession(session);
const refreshed = refreshActiveSession(session, updateStatus);
if (!session.workspaceUnavailable) { scheduleWorkspaceRefresh(); }
await refreshed;
}
Expand All @@ -1992,7 +2083,7 @@ function activateAttachedSession(
): Promise<void> {
// Replacing the selected connection preserves the user's logical session choice,
// even if that same session was reselected while discovery was pending.
if (previous && activeSession === previous) { return activateSession(session); }
if (previous && activeSession === previous) { return activateSession(session, !focusedDataViewPanel); }

// Automatic selection may not override a newer choice of a different session.
if (selectionRevision !== sessionSelectionRevision || selectedTerminal !== window.activeTerminal) {
Expand All @@ -2002,24 +2093,18 @@ function activateAttachedSession(
const hasNativeTerminal = terminalRegistry.forSession(session)?.kind === 'native';
const shouldPreserveSelectedTerminal = hasNativeTerminal || selected?.kind === 'explicit';
const preferred = shouldPreserveSelectedTerminal ? selected?.session : undefined;
return activateSession(preferred ?? session);
return activateSession(preferred ?? session, !focusedDataViewPanel);
}

async function refreshActiveSession(session: Session): Promise<void> {
async function refreshActiveSession(session: Session, updateStatus = true): Promise<void> {
pipeClient = session.socket;
if (!session.requester) { globalPipePath = session.pipePath; }
pid = session.pid;
rVer = session.rVer;
info = session.info;
sessionDir = session.sessionDir;
workingDir = session.workingDir;
workspaceData = session.workspaceData;

if (sessionStatusBarItem) {
const version = rVer.replace(/^R (?:version )?/, '').replace(/\s+\(.*/, '');
sessionStatusBarItem.text = `R ${version}: ${pid}`;
sessionStatusBarItem.tooltip = `${info.version || rVer}\nProcess ID: ${pid}\nCommand: ${info.command}\nStart time: ${info.start_time}\nClick to attach to active terminal.`;
sessionStatusBarItem.show();
if (updateStatus) {
updateSessionStatusBar(session);
}
rWorkspace?.refresh();
await setContext('rSessionActive', !session.workspaceUnavailable);
Expand All @@ -2041,6 +2126,15 @@ export async function activateSessionById(sessionId: string, options?: RSessionA
return true;
}

function updateSessionStatusBar(session: Session): void {
if (sessionStatusBarItem) {
const version = session.rVer.replace(/^R (?:version )?/, '').replace(/\s+\(.*/, '');
sessionStatusBarItem.text = `R ${version}: ${session.pid}`;
sessionStatusBarItem.tooltip = `${session.info.version || session.rVer}\nProcess ID: ${session.pid}\nCommand: ${session.info.command}\nStart time: ${session.info.start_time}\nClick to attach to active terminal.`;
sessionStatusBarItem.show();
}
}

export function resetStatusBar(): void {
if (sessionStatusBarItem) {
sessionStatusBarItem.text = 'R: (not attached)';
Expand Down Expand Up @@ -2085,9 +2179,39 @@ async function findLocalTerminal(rPid: string): Promise<NativeTerminalCandidate

export async function switchSessionByTerminal(terminal: vscode.Terminal | undefined): Promise<void> {
sessionSelectionRevision++;
const owner = terminalRegistry.ownerOf(terminal);
if (owner) { await activateSession(owner); }
else { resetStatusBar(); }

if (!terminal) {
selectedTerminalDetached = false;
if (focusedDataViewPanel) {
return;
}
if (activeSession) {
updateSessionStatusBar(activeSession);
} else {
resetStatusBar();
}
return;
}

focusedDataViewPanel = undefined;
focusedDataViewSessionId = null;

const target = terminalRegistry.ownerOf(terminal);

if(!target) {
selectedTerminalDetached = true;
if (!focusedDataViewPanel) { resetStatusBar(); }
return;
}
selectedTerminalDetached = false;
const activated = activateSession(target, false);
const revision = sessionSelectionRevision;
await activated;

if (revision !== sessionSelectionRevision || activeSession !== target || !isConnectedSession(target)) {
return;
}
if (!focusedDataViewPanel) { updateSessionStatusBar(target); }
}

function sendToSocket(socket: IpcSocket, data: Record<string, unknown>): void {
Expand Down Expand Up @@ -2398,6 +2522,8 @@ export async function cleanupSession(sessionId: string, closingSocket?: IpcSocke
}
if (activeSession === session) {
await clearActiveSession();
} else if (focusedDataViewPanel && focusedDataViewSessionId === sessionId) {
resetStatusBar();
}
}

Expand Down
Loading
Loading