diff --git a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts index b865271e8ab..fb6abe518c1 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts @@ -11,8 +11,8 @@ import { RuntimeInitialState } from '../../common/positron/extHost.positron.protocol.js'; import { extHostNamedCustomer, IExtHostContext } from '../../../services/extensions/common/extHostCustomers.js'; -import { ILanguageRuntimeClientCreatedEvent, ILanguageRuntimeInfo, ILanguageRuntimeMessage, ILanguageRuntimeMessageCommClosed, ILanguageRuntimeMessageCommData, ILanguageRuntimeMessageCommOpen, ILanguageRuntimeMessageError, ILanguageRuntimeMessageInput, ILanguageRuntimeMessageOutput, ILanguageRuntimeMessagePrompt, ILanguageRuntimeMessageState, ILanguageRuntimeMessageStream, ILanguageRuntimeMetadata, ILanguageRuntimeSessionState as ILanguageRuntimeSessionState, ILanguageRuntimeService, ILanguageRuntimeStartupFailure, LanguageRuntimeMessageType, RuntimeCodeExecutionMode, RuntimeCodeFragmentStatus, RuntimeErrorBehavior, RuntimeState, ILanguageRuntimeExit, RuntimeOutputKind, RuntimeExitReason, ILanguageRuntimeMessageWebOutput, PositronOutputLocation, LanguageRuntimeSessionMode, ILanguageRuntimeMessageResult, ILanguageRuntimeMessageClearOutput, ILanguageRuntimeMessageIPyWidget } from '../../../services/languageRuntime/common/languageRuntimeService.js'; -import { ILanguageRuntimeSession, ILanguageRuntimeSessionManager, IRuntimeSessionMetadata, IRuntimeSessionService } from '../../../services/runtimeSession/common/runtimeSessionService.js'; +import { ILanguageRuntimeClientCreatedEvent, ILanguageRuntimeInfo, ILanguageRuntimeMessage, ILanguageRuntimeMessageCommClosed, ILanguageRuntimeMessageCommData, ILanguageRuntimeMessageCommOpen, ILanguageRuntimeMessageError, ILanguageRuntimeMessageInput, ILanguageRuntimeMessageOutput, ILanguageRuntimeMessagePrompt, ILanguageRuntimeMessageState, ILanguageRuntimeMessageStream, ILanguageRuntimeMetadata, ILanguageRuntimeSessionState as ILanguageRuntimeSessionState, ILanguageRuntimeService, ILanguageRuntimeStartupFailure, LanguageRuntimeMessageType, RuntimeCodeExecutionMode, RuntimeCodeFragmentStatus, RuntimeErrorBehavior, RuntimeState, ILanguageRuntimeExit, RuntimeOutputKind, RuntimeExitReason, ILanguageRuntimeMessageWebOutput, PositronOutputLocation, LanguageRuntimeSessionMode, ILanguageRuntimeMessageResult, ILanguageRuntimeMessageClearOutput, ILanguageRuntimeMessageIPyWidget, RuntimeStartupPhase } from '../../../services/languageRuntime/common/languageRuntimeService.js'; +import { ILanguageRuntimeSession, ILanguageRuntimeSessionManager, IRuntimeSessionMetadata, IRuntimeSessionService, RuntimeStartMode } from '../../../services/runtimeSession/common/runtimeSessionService.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { IPositronConsoleService } from '../../../services/positronConsole/browser/interfaces/positronConsoleService.js'; @@ -36,7 +36,7 @@ import { Selection } from '../../../../editor/common/core/selection.js'; import { ITextResourceEditorInput } from '../../../../platform/editor/common/editor.js'; import { IPositronDataExplorerService } from '../../../services/positronDataExplorer/browser/interfaces/positronDataExplorerService.js'; import { ISettableObservable, observableValue } from '../../../../base/common/observableInternal/base.js'; -import { IRuntimeStartupService, RuntimeStartupPhase } from '../../../services/runtimeStartup/common/runtimeStartupService.js'; +import { IRuntimeStartupService } from '../../../services/runtimeStartup/common/runtimeStartupService.js'; import { SerializableObjectWithBuffers } from '../../../services/extensions/common/proxyIdentifier.js'; import { isWebviewReplayMessage } from '../../../contrib/positronWebviewPreloads/browser/utils.js'; import { IPositronWebviewPreloadService } from '../../../services/positronWebviewPreloads/browser/positronWebviewPreloadService.js'; @@ -105,6 +105,7 @@ class ExtHostLanguageRuntimeSessionAdapter implements ILanguageRuntimeSession { private readonly _onDidCreateClientInstanceEmitter = new Emitter(); private _currentState: RuntimeState = RuntimeState.Uninitialized; + private _lastUsed: number = Date.now(); private _clients: Map> = new Map>(); @@ -324,6 +325,13 @@ class ExtHostLanguageRuntimeSessionAdapter implements ILanguageRuntimeSession { return Array.from(this._clients.values()); } + /** + * Returns the last time this session was used (currently, "used" means "executed code") + */ + get lastUsed(): number { + return this._lastUsed; + } + /** * Convenience method to get the session's ID without having to access the * the metadata directly. @@ -396,6 +404,7 @@ class ExtHostLanguageRuntimeSessionAdapter implements ILanguageRuntimeSession { } execute(code: string, id: string, mode: RuntimeCodeExecutionMode, errorBehavior: RuntimeErrorBehavior): void { + this._lastUsed = Date.now(); this._proxy.$executeCode(this.handle, code, id, mode, errorBehavior); } @@ -1171,7 +1180,7 @@ export class MainThreadLanguageRuntime this._runtimeStartupService.registerMainThreadLanguageRuntime(this._id); this._disposables.add( - this._runtimeStartupService.onDidChangeRuntimeStartupPhase((phase) => { + this._languageRuntimeService.onDidChangeRuntimeStartupPhase((phase) => { if (phase === RuntimeStartupPhase.Discovering) { this._proxy.$discoverLanguageRuntimes(); } @@ -1238,7 +1247,9 @@ export class MainThreadLanguageRuntime sessionName, sessionMode, uri, - 'Extension-requested runtime selection via Positron API'); + 'Extension-requested runtime selection via Positron API', + RuntimeStartMode.Starting, + true); return sessionId; } diff --git a/src/vs/workbench/browser/positronNewProjectWizard/newProjectWizardState.ts b/src/vs/workbench/browser/positronNewProjectWizard/newProjectWizardState.ts index 95592c4ade0..36419214563 100644 --- a/src/vs/workbench/browser/positronNewProjectWizard/newProjectWizardState.ts +++ b/src/vs/workbench/browser/positronNewProjectWizard/newProjectWizardState.ts @@ -5,9 +5,9 @@ // Other dependencies. import { IFileDialogService } from '../../../platform/dialogs/common/dialogs.js'; -import { ILanguageRuntimeMetadata, ILanguageRuntimeService } from '../../services/languageRuntime/common/languageRuntimeService.js'; +import { ILanguageRuntimeMetadata, ILanguageRuntimeService, RuntimeStartupPhase } from '../../services/languageRuntime/common/languageRuntimeService.js'; import { IRuntimeSessionService } from '../../services/runtimeSession/common/runtimeSessionService.js'; -import { IRuntimeStartupService, RuntimeStartupPhase } from '../../services/runtimeStartup/common/runtimeStartupService.js'; +import { IRuntimeStartupService } from '../../services/runtimeStartup/common/runtimeStartupService.js'; import { EnvironmentSetupType, NewProjectWizardStep, PythonEnvironmentProvider } from './interfaces/newProjectWizardEnums.js'; import { IWorkbenchLayoutService } from '../../services/layout/browser/layoutService.js'; import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js'; @@ -163,7 +163,7 @@ export class NewProjectWizardStateManager this._condaPythonVersionInfo = undefined; this._minimumRVersion = undefined; - if (this._services.runtimeStartupService.startupPhase === RuntimeStartupPhase.Complete) { + if (this._services.languageRuntimeService.startupPhase === RuntimeStartupPhase.Complete) { // If the runtime startup is already complete, initialize the wizard state and update // the interpreter-related state. this._initDefaultsFromExtensions() @@ -174,7 +174,7 @@ export class NewProjectWizardStateManager } else { // Register disposables. this._register( - this._services.runtimeStartupService.onDidChangeRuntimeStartupPhase( + this._services.languageRuntimeService.onDidChangeRuntimeStartupPhase( async (phase) => { if (phase === RuntimeStartupPhase.Discovering) { // At this phase, the extensions that provide language runtimes will diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/actionBar.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/actionBar.tsx index 9ca48289344..11b62373717 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/actionBar.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/actionBar.tsx @@ -23,7 +23,7 @@ import { usePositronConsoleContext } from '../positronConsoleContext.js'; import { PositronActionBarContextProvider } from '../../../../../platform/positronActionBar/browser/positronActionBarContext.js'; import { PositronConsoleState } from '../../../../services/positronConsole/browser/interfaces/positronConsoleService.js'; import { RuntimeExitReason, RuntimeState } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; -import { ILanguageRuntimeSession } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; +import { ILanguageRuntimeSession, RuntimeStartMode } from '../../../../services/runtimeSession/common/runtimeSessionService.js'; import { ConsoleInstanceMenuButton } from './consoleInstanceMenuButton.js'; /** @@ -299,7 +299,9 @@ export const ActionBar = (props: ActionBarProps) => { session.metadata.sessionMode, session.metadata.notebookUri, `User-requested new session from console action bar ` + - `after session ${session.metadata.sessionId} exited.` + `after session ${session.metadata.sessionId} exited.`, + RuntimeStartMode.Starting, + false ); return; } else { diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/consoleCore.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/consoleCore.tsx index 86c97d0a6d0..c73e5183b3f 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/consoleCore.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/consoleCore.tsx @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -16,7 +16,7 @@ import { EmptyConsole } from './emptyConsole.js'; import { ConsoleInstance } from './consoleInstance.js'; import { usePositronConsoleContext } from '../positronConsoleContext.js'; import { StartupStatus } from './startupStatus.js'; -import { RuntimeStartupPhase } from '../../../../services/runtimeStartup/common/runtimeStartupService.js'; +import { RuntimeStartupPhase } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; // ConsoleCoreProps interface. interface ConsoleCoreProps { @@ -35,11 +35,11 @@ export const ConsoleCore = (props: ConsoleCoreProps) => { const positronConsoleContext = usePositronConsoleContext(); const [startupPhase, setStartupPhase] = useState( - positronConsoleContext.runtimeStartupService.startupPhase); + positronConsoleContext.languageRuntimeService.startupPhase); useEffect(() => { const disposables = - positronConsoleContext.runtimeStartupService.onDidChangeRuntimeStartupPhase( + positronConsoleContext.languageRuntimeService.onDidChangeRuntimeStartupPhase( e => { setStartupPhase(e); }); diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/startupStatus.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/startupStatus.tsx index d7dc5817564..c77a1799193 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/startupStatus.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/startupStatus.tsx @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -14,7 +14,7 @@ import { localize } from '../../../../../nls.js'; import { usePositronConsoleContext } from '../positronConsoleContext.js'; import { ProgressBar } from '../../../../../base/browser/ui/progressbar/progressbar.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { RuntimeStartupPhase } from '../../../../services/runtimeStartup/common/runtimeStartupService.js'; +import { RuntimeStartupPhase } from '../../../../services/languageRuntime/common/languageRuntimeService.js'; // Load localized copy for control. const initalizing = localize('positron.console.initializing', "Starting up"); @@ -41,7 +41,7 @@ export const StartupStatus = () => { const [discovered, setDiscovered] = useState(positronConsoleContext.languageRuntimeService.registeredRuntimes.length); const [startupPhase, setStartupPhase] = - useState(positronConsoleContext.runtimeStartupService.startupPhase); + useState(positronConsoleContext.languageRuntimeService.startupPhase); useEffect(() => { const disposableStore = new DisposableStore(); @@ -65,7 +65,7 @@ export const StartupStatus = () => { // When the startup phase changes, update the phase. disposableStore.add( - positronConsoleContext.runtimeStartupService.onDidChangeRuntimeStartupPhase( + positronConsoleContext.languageRuntimeService.onDidChangeRuntimeStartupPhase( phase => { setStartupPhase(phase); })); diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts index 15e92dc74a9..9b6ad972ccb 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts @@ -1,16 +1,17 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ import * as nls from '../../../../nls.js'; -import { Emitter } from '../../../../base/common/event.js'; +import { Event, Emitter } from '../../../../base/common/event.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ILanguageRuntimeMetadata, ILanguageRuntimeService, formatLanguageRuntimeMetadata } from './languageRuntimeService.js'; +import { ILanguageRuntimeMetadata, ILanguageRuntimeService, RuntimeStartupPhase, formatLanguageRuntimeMetadata } from './languageRuntimeService.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationNode, } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { ISettableObservable, observableValue } from '../../../../base/common/observableInternal/base.js'; /** * The implementation of ILanguageRuntimeService @@ -26,6 +27,9 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti private readonly _onDidRegisterRuntimeEmitter = this._register(new Emitter); + // The current startup phase; an observeable value. + private _startupPhase: ISettableObservable; + //#endregion Private Properties //#region Constructor @@ -40,6 +44,19 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti ) { // Call the base class's constructor. super(); + + this._startupPhase = observableValue( + 'runtime-startup-phase', RuntimeStartupPhase.Initializing); + this.onDidChangeRuntimeStartupPhase = Event.fromObservable(this._startupPhase); + } + + /** + * Sets the startup phase + * + * @param phase The new phase + */ + setStartupPhase(phase: RuntimeStartupPhase): void { + this._startupPhase.set(phase, undefined); } //#endregion Constructor @@ -52,6 +69,11 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti // An event that fires when a new runtime is registered. readonly onDidRegisterRuntime = this._onDidRegisterRuntimeEmitter.event; + /** + * Event tracking the current startup phase. + */ + onDidChangeRuntimeStartupPhase: Event; + /** * Gets the registered runtimes. */ @@ -117,6 +139,13 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti return this._registeredRuntimesByRuntimeId.get(runtimeId); } + /** + * Returns the current startup phase. + */ + get startupPhase(): RuntimeStartupPhase { + return this._startupPhase.get(); + } + //#endregion ILanguageRuntimeService Implementation } diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts index 72a1b310ec5..1836afeb987 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntimeService.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -540,6 +540,51 @@ export enum LanguageRuntimeStartupBehavior { Manual = 'manual' } +/** + * The phases through which the runtime startup service progresses as Positron + * starts. + */ +export enum RuntimeStartupPhase { + /** + * Phase 1: The startup sequence has not yet begun. + */ + Initializing = 'initializing', + + /** + * Phase 2: If the workspace is not trusted, we cannot proceed with startup, + * since many runtimes run arbitrary code at startup (often from the + * workspace contents) and we cannot trust them to do so safely. The startup + * sequence stays at `AwaitingTrust` until workspace trust is granted. + */ + AwaitingTrust = 'awaitingTrust', + + /** + * Phase 3: Positron is reconnecting to runtimes that are already running. + * We only enter this phase when reloading the UI, or when reopening a + * browser tab. + */ + Reconnecting = 'reconnecting', + + /** + * Phase 4: Positron is starting any runtimes that are affiliated with the + * workspace. We enter this phase on a fresh start of Positron, when no + * existing sessions are running. + */ + Starting = 'starting', + + /** + * Phase 5: Positron is discovering all the runtimes on the machine. This + * can take a while, but does precede startup for workspaces that have no + * affiliated runtimes (so we don't know what to start yet). + */ + Discovering = 'discovering', + + /** + * Phase 6: Startup is complete. In this phase, we start any runtimes + * recommended by extensions if nothing was started in previous phases. + */ + Complete = 'complete', +} export interface ILanguageRuntimeMessageState extends ILanguageRuntimeMessage { /** The new state */ @@ -661,9 +706,16 @@ export interface ILanguageRuntimeService { // Needed for service branding in dependency injector. readonly _serviceBrand: undefined; - // An event that fires when a new runtime is registered. + /** + * An event that fires when a new runtime is registered. + */ readonly onDidRegisterRuntime: Event; + /** + * Event tracking the current startup phase. + */ + onDidChangeRuntimeStartupPhase: Event; + /** * Gets the registered language runtimes. */ @@ -691,5 +743,17 @@ export interface ILanguageRuntimeService { * @param runtimeId The ID of the runtime to unregister */ unregisterRuntime(runtimeId: string): void; + + /** + * Sets the current startup phase. + * + * @param phase The new startup phase + */ + setStartupPhase(phase: RuntimeStartupPhase): void; + + /** + * Returns the current startup phase. + */ + get startupPhase(): RuntimeStartupPhase; } diff --git a/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts b/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts index 00a82c895af..1f9730175a5 100644 --- a/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts +++ b/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -191,21 +191,29 @@ export class PositronConsoleService extends Disposable implements IPositronConso // Call the disposable constructor. super(); - // Start a Positron console instance for each running runtime. + // Start a Positron console instance for each running runtime. Only + // activate the first one. + let first = true; this._runtimeSessionService.activeSessions.forEach(session => { if (session.metadata.sessionMode === LanguageRuntimeSessionMode.Console) { - this.startPositronConsoleInstance(session, SessionAttachMode.Connected); - } - }); + // The instance should be activated if it is the foreground + // session. + let activate = false; + if (this._runtimeSessionService.foregroundSession && + session.sessionId === this._runtimeSessionService.foregroundSession.sessionId) { + activate = true; + } - // Get the foreground session. If there is one, set the active Positron console instance. - if (this._runtimeSessionService.foregroundSession) { - const positronConsoleInstance = this._positronConsoleInstancesBySessionId.get( - this._runtimeSessionService.foregroundSession.sessionId); - if (positronConsoleInstance) { - this.setActivePositronConsoleInstance(positronConsoleInstance); + // The instance should also be activated if it is the first + // session and there is no designated foreground session. + if (first && !this._runtimeSessionService.foregroundSession) { + activate = true; + } + + this.startPositronConsoleInstance(session, SessionAttachMode.Connected, activate); + first = false; } - } + }); // Register the onWillStartSessiopn event handler so we start a new // Positron console instance before a runtime starts up. @@ -248,7 +256,7 @@ export class PositronConsoleService extends Disposable implements IPositronConso this._positronConsoleInstancesBySessionId.set(e.session.sessionId, positronConsoleInstance); } else { // New runtime with a new language, so start a new Positron console instance. - this.startPositronConsoleInstance(e.session, attachMode); + this.startPositronConsoleInstance(e.session, attachMode, e.activate); } })); @@ -396,7 +404,9 @@ export class PositronConsoleService extends Disposable implements IPositronConso LanguageRuntimeSessionMode.Console, undefined, // No notebook URI (console sesion) `User executed code in language ${languageId}, and no running runtime was found ` + - `for the language.`); + `for the language.`, + RuntimeStartMode.Starting, + true); } // Get the Positron console instance for the language ID. @@ -434,14 +444,15 @@ export class PositronConsoleService extends Disposable implements IPositronConso * Starts a Positron console instance for the specified runtime session. * * @param runtime The runtime for the new Positron console instance. - * @param attachMode A value which indicates the mode in which to attach the - * session. + * @param attachMode A value which indicates the mode in which to attach the session. + * @param activate Whether to activate the console instance immediately * * @returns The new Positron console instance. */ private startPositronConsoleInstance( session: ILanguageRuntimeSession, - attachMode: SessionAttachMode + attachMode: SessionAttachMode, + activate: boolean ): IPositronConsoleInstance { // Create the new Positron console instance. const positronConsoleInstance = this._register(this._instantiationService.createInstance( @@ -463,11 +474,13 @@ export class PositronConsoleService extends Disposable implements IPositronConso // Fire the onDidStartPositronConsoleInstance event. this._onDidStartPositronConsoleInstanceEmitter.fire(positronConsoleInstance); - // Set the active positron console instance. - this._activePositronConsoleInstance = positronConsoleInstance; + // Set the active positron console instance, if requested + if (activate) { + this._activePositronConsoleInstance = positronConsoleInstance; - // Fire the onDidChangeActivePositronConsoleInstance event. - this._onDidChangeActivePositronConsoleInstanceEmitter.fire(positronConsoleInstance); + // Fire the onDidChangeActivePositronConsoleInstance event. + this._onDidChangeActivePositronConsoleInstanceEmitter.fire(positronConsoleInstance); + } // Listen for console width changes. this._register(positronConsoleInstance.onDidChangeWidthInChars(width => { @@ -532,7 +545,7 @@ class PositronConsoleInstance extends Disposable implements IPositronConsoleInst * Gets or sets the disposable store. This contains things that are disposed when a runtime is * detached. */ - private _runtimeDisposableStore = this._register(new DisposableStore()); + private readonly _runtimeDisposableStore = new DisposableStore(); /** * Gets or sets the runtime state. @@ -1863,8 +1876,7 @@ class PositronConsoleInstance extends Disposable implements IPositronConsoleInst } // Dispose of the runtime event handlers. - this._runtimeDisposableStore.dispose(); - this._runtimeDisposableStore = new DisposableStore(); + this._runtimeDisposableStore.clear(); } else { // We are not currently attached; warn. console.warn(`Attempt to detach already detached session ${this._session.metadata.sessionName}.`); diff --git a/src/vs/workbench/services/positronVariables/common/positronVariablesService.ts b/src/vs/workbench/services/positronVariables/common/positronVariablesService.ts index 93b32c7c593..1fb8df6d503 100644 --- a/src/vs/workbench/services/positronVariables/common/positronVariablesService.ts +++ b/src/vs/workbench/services/positronVariables/common/positronVariablesService.ts @@ -79,8 +79,8 @@ export class PositronVariablesService extends Disposable implements IPositronVar super(); // Start a Positron variables instance for each running runtime. - this._runtimeSessionService.activeSessions.forEach(session => { - this.startPositronVariablesInstance(session); + this._runtimeSessionService.activeSessions.forEach((session, idx) => { + this.startPositronVariablesInstance(session, idx === 0); }); // Get the foreground session. If there is one, set the active Positron variables instance. @@ -96,8 +96,7 @@ export class PositronVariablesService extends Disposable implements IPositronVar // Register the onWillStartSession event handler so we start a new Positron variables // instance before a runtime starts up. this._register(this._runtimeSessionService.onWillStartSession(e => { - this.createOrAssignPositronVariablesInstance(e.session); - + this.createOrAssignPositronVariablesInstance(e.session, e.activate); })); // Register session cleanup handler @@ -256,9 +255,15 @@ export class PositronVariablesService extends Disposable implements IPositronVar /** * Creates or assigns a Positron variables instance for the specified session. - * @param session The session to create or assign a Positron variables instance for. + * + * @param session The session to create or assign a Positron variables + * instance for. + * @param activate Whether to activate the Positron variables instance + * after creating it. */ - private createOrAssignPositronVariablesInstance(session: ILanguageRuntimeSession) { + private createOrAssignPositronVariablesInstance( + session: ILanguageRuntimeSession, + activate: boolean) { // Ignore background sessions if (session.metadata.sessionMode === LanguageRuntimeSessionMode.Background) { return; @@ -315,15 +320,20 @@ export class PositronVariablesService extends Disposable implements IPositronVar } // If we got here, we need to start a new Positron variables instance. - this.startPositronVariablesInstance(session); + this.startPositronVariablesInstance(session, activate); } /** * Starts a Positron variables instance for the specified runtime. + * * @param session The runtime session for the new Positron variables instance. + * @param activate Whether to activate the Positron variables instance after creating it. + * * @returns The new Positron variables instance. */ - private startPositronVariablesInstance(session: ILanguageRuntimeSession): IPositronVariablesInstance { + private startPositronVariablesInstance( + session: ILanguageRuntimeSession, + activate: boolean): IPositronVariablesInstance { // Create the new Positron variables instance. const positronVariablesInstance = this._register(new PositronVariablesInstance( session, this._logService, this._notificationService, this._accessibilityService)); @@ -336,11 +346,13 @@ export class PositronVariablesService extends Disposable implements IPositronVar // Fire the onDidStartPositronVariablesInstance event. this._onDidStartPositronVariablesInstanceEmitter.fire(positronVariablesInstance); - // Set the active Positron variables instance. - this._activePositronVariablesInstance = positronVariablesInstance; + if (activate) { + // Set the active Positron variables instance. + this._activePositronVariablesInstance = positronVariablesInstance; - // Fire the onDidChangeActivePositronVariablesInstance event. - this._onDidChangeActivePositronVariablesInstanceEmitter.fire(positronVariablesInstance); + // Fire the onDidChangeActivePositronVariablesInstance event. + this._onDidChangeActivePositronVariablesInstanceEmitter.fire(positronVariablesInstance); + } // Return the instance. return positronVariablesInstance; diff --git a/src/vs/workbench/services/runtimeSession/common/activeRuntimeSession.ts b/src/vs/workbench/services/runtimeSession/common/activeRuntimeSession.ts index 4bfc6737c44..923885c228c 100644 --- a/src/vs/workbench/services/runtimeSession/common/activeRuntimeSession.ts +++ b/src/vs/workbench/services/runtimeSession/common/activeRuntimeSession.ts @@ -21,6 +21,7 @@ import { ILanguageRuntimeGlobalEvent, ILanguageRuntimeSession, ILanguageRuntimeS * active language runtime session. */ export class ActiveRuntimeSession extends Disposable { + public state: RuntimeState; // The event emitter for the onDidReceiveRuntimeEvent event. diff --git a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts index 4faca9e6440..bdb29dd396a 100644 --- a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts +++ b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -164,7 +164,8 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession // one to start. this._logService.trace(`Language runtime ${formatLanguageRuntimeMetadata(languageRuntimeInfos[0])} automatically starting`); this.autoStartRuntime(languageRuntimeInfos[0], - `A file with the language ID ${languageId} was opened.`); + `A file with the language ID ${languageId} was opened.`, + true); })); // When an extension activates, check to see if we have any disconnected @@ -180,7 +181,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession // Attempt to reconnect the session. this._logService.debug(`Extension ${extensionId.value} has been reloaded; ` + `attempting to reconnect session ${session.sessionId}`); - this.restoreRuntimeSession(session.runtimeMetadata, session.metadata); + this.restoreRuntimeSession(session.runtimeMetadata, session.metadata, false); } } } @@ -336,7 +337,8 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession sessionMode, notebookUri, source, - RuntimeStartMode.Switching); + RuntimeStartMode.Switching, + true); } else { // Shut down any other runtime consoles for the language. const activeSession = @@ -358,7 +360,8 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession sessionMode, undefined, // No notebook URI (console session) source, - RuntimeStartMode.Switching); + RuntimeStartMode.Switching, + true); } } @@ -433,13 +436,15 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession * @param notebookUri The notebook URI to attach to the session, if any. * @param source The source of the request to start the runtime. * @param startMode The mode in which to start the runtime. + * @param activate Whether to activate/focus the session after it is started. */ async startNewRuntimeSession(runtimeId: string, sessionName: string, sessionMode: LanguageRuntimeSessionMode, notebookUri: URI | undefined, source: string, - startMode = RuntimeStartMode.Starting): Promise { + startMode = RuntimeStartMode.Starting, + activate: boolean): Promise { // See if we are already starting the requested session. If we // are, return the promise that resolves when the session is ready to // use. This makes it possible for multiple requests to start the same @@ -465,7 +470,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession // workspace is trusted. if (!this._workspaceTrustManagementService.isWorkspaceTrusted()) { if (sessionMode === LanguageRuntimeSessionMode.Console) { - return this.autoStartRuntime(languageRuntime, source, startMode); + return this.autoStartRuntime(languageRuntime, source, activate); } else { throw new Error(`Cannot start a ${sessionMode} session in an untrusted workspace.`); } @@ -475,7 +480,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession this._logService.info( `Starting session for language runtime ` + `${formatLanguageRuntimeMetadata(languageRuntime)} (Source: ${source})`); - return this.doCreateRuntimeSession(languageRuntime, sessionName, sessionMode, source, startMode, notebookUri); + return this.doCreateRuntimeSession(languageRuntime, sessionName, sessionMode, source, startMode, activate, notebookUri); } /** @@ -509,10 +514,13 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession * * @param runtimeMetadata The metadata of the runtime to start. * @param sessionMetadata The metadata of the session to start. + * @param activate Whether to activate/focus the session after it is + * reconnected. */ async restoreRuntimeSession( runtimeMetadata: ILanguageRuntimeMetadata, - sessionMetadata: IRuntimeSessionMetadata): Promise { + sessionMetadata: IRuntimeSessionMetadata, + activate: boolean): Promise { // See if we are already starting the requested session. If we // are, return the promise that resolves when the session is ready to @@ -586,7 +594,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession // Actually reconnect the session. try { - await this.doStartRuntimeSession(session, sessionManager, RuntimeStartMode.Reconnecting); + await this.doStartRuntimeSession(session, sessionManager, RuntimeStartMode.Reconnecting, activate); startPromise.complete(sessionMetadata.sessionId); } catch (err) { startPromise.error(err); @@ -665,7 +673,9 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession session.metadata.sessionName, session.metadata.sessionMode, session.metadata.notebookUri, - `'Restart Interpreter' command invoked`); + `'Restart Interpreter' command invoked`, + RuntimeStartMode.Starting, + true); return; } else if (state === RuntimeState.Starting || state === RuntimeState.Restarting) { @@ -865,7 +875,8 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession * * @param runtime The runtime to start. * @param source The source of the request to start the runtime. - * @param startMode The mode in which to start the runtime. + * @param activate Whether to activate/focus the new session after it + * starts. * * @returns A promise that resolves with a session ID for the new session, * if one was started. @@ -873,7 +884,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession async autoStartRuntime( metadata: ILanguageRuntimeMetadata, source: string, - startMode = RuntimeStartMode.Starting, + activate: boolean ): Promise { // Check the setting to see if we should be auto-starting. const autoStart = this._configurationService.getValue( @@ -892,7 +903,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession `${formatLanguageRuntimeMetadata(metadata)} ` + `automatically starting. Source: ${source}`); - return this.doAutoStartRuntime(metadata, source, startMode); + return this.doAutoStartRuntime(metadata, source, activate); } else { this._logService.debug(`Deferring the start of language runtime ` + `${formatLanguageRuntimeMetadata(metadata)} (Source: ${source}) ` + @@ -909,7 +920,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession `${formatLanguageRuntimeMetadata(metadata)} ` + `automatically starting after workspace trust was granted. ` + `Source: ${source}`); - this.doAutoStartRuntime(metadata, source, startMode); + this.doAutoStartRuntime(metadata, source, activate); })); } @@ -961,12 +972,13 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession * * @param metadata The metadata for the runtime to start. * @param source The source of the request to start the runtime. - * @param startMode The mode in which to start the runtime. + * @param activate Whether to activate/focus the new session after it is + * started. */ private async doAutoStartRuntime( metadata: ILanguageRuntimeMetadata, source: string, - startMode: RuntimeStartMode): Promise { + activate: boolean): Promise { // Auto-started runtimes are (currently) always console sessions. const sessionMode = LanguageRuntimeSessionMode.Console; @@ -1069,7 +1081,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession } } - return this.doCreateRuntimeSession(metadata, metadata.runtimeName, sessionMode, source, startMode, notebookUri); + return this.doCreateRuntimeSession(metadata, metadata.runtimeName, sessionMode, source, RuntimeStartMode.Starting, activate, notebookUri); } /** @@ -1080,6 +1092,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession * @param sessionMode The mode for the new session. * @param source The source of the request to start the runtime. * @param startMode The mode in which to start the runtime. + * @param activate Whether to activate/focus the session after it is started. * @param notebookDocument The notebook document to attach to the session, if any. * * Returns a promise that resolves with the session ID when the runtime is @@ -1090,6 +1103,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession sessionMode: LanguageRuntimeSessionMode, source: string, startMode: RuntimeStartMode, + activate: boolean, notebookUri?: URI): Promise { this.setStartingSessionMaps(sessionMode, runtimeMetadata, notebookUri); @@ -1142,7 +1156,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession // Actually start the session. try { - await this.doStartRuntimeSession(session, sessionManager, startMode); + await this.doStartRuntimeSession(session, sessionManager, startMode, activate); startPromise.complete(sessionId); } catch (err) { startPromise.error(err); @@ -1157,21 +1171,24 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession * @param session The session to start. * @param manager The session manager for the session. * @param startMode The mode in which the session is starting. + * @param activate Whether to activate/focus the session after it is started. */ private async doStartRuntimeSession(session: ILanguageRuntimeSession, manager: ILanguageRuntimeSessionManager, - startMode: RuntimeStartMode): + startMode: RuntimeStartMode, + activate: boolean): Promise { // Fire the onWillStartRuntime event. const evt: IRuntimeSessionWillStartEvent = { session, startMode, + activate }; this._onWillStartRuntimeEmitter.fire(evt); // Attach event handlers to the newly provisioned session. - this.attachToSession(session, manager); + this.attachToSession(session, manager, activate); try { // Attempt to start, or reconnect to, the session. @@ -1240,9 +1257,12 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession * * @param session The session to attach. * @param manager The session's manager. + * @param activate Whether to activate/focus the session after it is started. */ - private attachToSession(session: ILanguageRuntimeSession, - manager: ILanguageRuntimeSessionManager): void { + private attachToSession( + session: ILanguageRuntimeSession, + manager: ILanguageRuntimeSessionManager, + activate: boolean): void { // Clean up any previous active session info for this session. const oldSession = this._activeSessionsBySessionId.get(session.sessionId); @@ -1264,12 +1284,11 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession // Process the state change. switch (state) { case RuntimeState.Ready: + // If the session is a console session, make it the + // foreground session if it isn't already. if (session !== this._foregroundSession && - session.metadata.sessionMode === LanguageRuntimeSessionMode.Console) { - // When a new console is ready, activate it. We avoid - // re-activation if already active since the resulting - // events can cause Positron behave as though a new - // runtime were started (e.g. focusing the console) + session.metadata.sessionMode === LanguageRuntimeSessionMode.Console && + activate) { this.foregroundSession = session; } @@ -1309,6 +1328,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession this._onWillStartRuntimeEmitter.fire({ session, startMode: RuntimeStartMode.Restarting, + activate: false }); } break; diff --git a/src/vs/workbench/services/runtimeSession/common/runtimeSessionService.ts b/src/vs/workbench/services/runtimeSession/common/runtimeSessionService.ts index ce5d4c159cb..ecd0d5a0672 100644 --- a/src/vs/workbench/services/runtimeSession/common/runtimeSessionService.ts +++ b/src/vs/workbench/services/runtimeSession/common/runtimeSessionService.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -46,6 +46,9 @@ export interface IRuntimeSessionWillStartEvent { /** The mode in which the session is starting. */ startMode: RuntimeStartMode; + /** Whether the runtime should be activated when it starts */ + activate: boolean; + /** The session about to start */ session: ILanguageRuntimeSession; } @@ -139,6 +142,9 @@ export interface ILanguageRuntimeSession extends IDisposable { /** The current state of the runtime (tracks events above) */ getRuntimeState(): RuntimeState; + /** Timestamp of when the runtime was last used */ + get lastUsed(): number; + /** * The (cached) current set of client instances that are known to Positron. * Note that this list may not reflect the full set of clients that are @@ -346,6 +352,9 @@ export interface IRuntimeSessionService { * @param sessionMode The mode of the session to start. * @param source The source of the request to start the runtime, for debugging purposes * (not displayed to the user) + * @param startMode The mode in which to start the runtime. + * @param activate Whether to activate/focus the session after it is + * started. * * Returns a promise that resolves to the session ID of the new session. */ @@ -353,13 +362,15 @@ export interface IRuntimeSessionService { sessionName: string, sessionMode: LanguageRuntimeSessionMode, notebookUri: URI | undefined, - source: string): Promise; + source: string, + startMode: RuntimeStartMode, + activate: boolean): Promise; /** * Validates a persisted runtime session before reconnecting to it. * * @param runtimeMetadata The metadata of the runtime. - * @param sesionId The ID of the session to validate. + * @param sessionId The ID of the session to validate. */ validateRuntimeSession( runtimeMetadata: ILanguageRuntimeMetadata, @@ -370,23 +381,28 @@ export interface IRuntimeSessionService { * * @param runtimeMetadata The metadata of the runtime to start. * @param sessionMetadata The metadata of the session to start. + * @param activate Whether to activate/focus the session after it is reconnected. */ restoreRuntimeSession( runtimeMetadata: ILanguageRuntimeMetadata, - sessionMetadata: IRuntimeSessionMetadata): Promise; + sessionMetadata: IRuntimeSessionMetadata, + activate: boolean): Promise; /** * Automatically starts a runtime. * * @param runtime The runtime to start. * @param source The source of the request to start the runtime. + * @param activate Whether to activate/focus the session after it is + * started. * * @returns A promise that resolves with a session ID for the new session, * if one was started. */ autoStartRuntime( metadata: ILanguageRuntimeMetadata, - source: string): Promise; + source: string, + activate: boolean): Promise; /** * Selects a previously registered runtime as the active runtime. diff --git a/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.test.ts b/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.test.ts index a2e543a4d74..cda88a08b21 100644 --- a/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.test.ts +++ b/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.test.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -198,7 +198,7 @@ suite('Positron - RuntimeSessionService', () => { async function restoreSession( sessionMetadata: IRuntimeSessionMetadata, runtimeMetadata = runtime, ) { - await runtimeSessionService.restoreRuntimeSession(runtimeMetadata, sessionMetadata); + await runtimeSessionService.restoreRuntimeSession(runtimeMetadata, sessionMetadata, true); // Ensure that the session gets disposed after the test. const session = runtimeSessionService.getSession(sessionMetadata.sessionId); @@ -233,7 +233,7 @@ suite('Positron - RuntimeSessionService', () => { } async function autoStartSession(runtimeMetadata = runtime) { - const sessionId = await runtimeSessionService.autoStartRuntime(runtimeMetadata, startReason); + const sessionId = await runtimeSessionService.autoStartRuntime(runtimeMetadata, startReason, true); assert.ok(sessionId); const session = runtimeSessionService.getSession(sessionId); assert.ok(session instanceof TestLanguageRuntimeSession); @@ -320,7 +320,7 @@ suite('Positron - RuntimeSessionService', () => { } else { startMode = RuntimeStartMode.Starting; } - sinon.assert.calledOnceWithExactly(target, { startMode, session }); + sinon.assert.calledOnceWithExactly(target, { startMode, session, activate: true }); assert.ifError(error); }); @@ -627,7 +627,7 @@ suite('Positron - RuntimeSessionService', () => { test('auto start console does nothing if automatic startup is disabled', async () => { configService.setUserConfiguration('interpreters.automaticStartup', false); - const sessionId = await runtimeSessionService.autoStartRuntime(runtime, startReason); + const sessionId = await runtimeSessionService.autoStartRuntime(runtime, startReason, true); assert.strictEqual(sessionId, ''); assertServiceState(); @@ -639,10 +639,10 @@ suite('Positron - RuntimeSessionService', () => { let sessionId: string; if (action === 'auto start') { - sessionId = await runtimeSessionService.autoStartRuntime(runtime, startReason); + sessionId = await runtimeSessionService.autoStartRuntime(runtime, startReason, true); } else { sessionId = await runtimeSessionService.startNewRuntimeSession( - runtime.runtimeId, sessionName, LanguageRuntimeSessionMode.Console, undefined, startReason); + runtime.runtimeId, sessionName, LanguageRuntimeSessionMode.Console, undefined, startReason, RuntimeStartMode.Starting, true); } assert.strictEqual(sessionId, ''); @@ -760,6 +760,7 @@ suite('Positron - RuntimeSessionService', () => { sinon.assert.calledOnceWithExactly(willStartSession, { session, startMode: RuntimeStartMode.Restarting, + activate: false }); }); @@ -841,6 +842,7 @@ suite('Positron - RuntimeSessionService', () => { session: newSession, // Since we restarted from an exited state, the start mode is 'starting'. startMode: RuntimeStartMode.Starting, + activate: true }); assert.strictEqual(newSession.metadata.sessionName, session.metadata.sessionName); diff --git a/src/vs/workbench/services/runtimeSession/test/common/testLanguageRuntimeSession.ts b/src/vs/workbench/services/runtimeSession/test/common/testLanguageRuntimeSession.ts index 08a6d2b5a7c..5b13b73955f 100644 --- a/src/vs/workbench/services/runtimeSession/test/common/testLanguageRuntimeSession.ts +++ b/src/vs/workbench/services/runtimeSession/test/common/testLanguageRuntimeSession.ts @@ -90,6 +90,10 @@ export class TestLanguageRuntimeSession extends Disposable implements ILanguageR return this._currentState; } + get lastUsed(): number { + return 0; + } + openResource(_resource: URI | string): Promise { throw new Error('Not implemented.'); } diff --git a/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts b/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts index dbd541f1ee3..96e9cb34936 100644 --- a/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts +++ b/src/vs/workbench/services/runtimeSession/test/common/testRuntimeSessionService.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -24,7 +24,7 @@ import { LanguageRuntimeService } from '../../../languageRuntime/common/language import { ILanguageRuntimeMetadata, ILanguageRuntimeService, LanguageRuntimeSessionLocation, LanguageRuntimeSessionMode, LanguageRuntimeStartupBehavior } from '../../../languageRuntime/common/languageRuntimeService.js'; import { IPositronModalDialogsService } from '../../../positronModalDialogs/common/positronModalDialogs.js'; import { RuntimeSessionService } from '../../common/runtimeSession.js'; -import { IRuntimeSessionService } from '../../common/runtimeSessionService.js'; +import { IRuntimeSessionService, RuntimeStartMode } from '../../common/runtimeSessionService.js'; import { TestLanguageRuntimeSession } from './testLanguageRuntimeSession.js'; import { TestOpenerService, TestPositronModalDialogService, TestCommandService, TestRuntimeSessionManager } from '../../../../test/common/positronWorkbenchTestServices.js'; import { TestExtensionService, TestStorageService, TestWorkspaceTrustManagementService } from '../../../../test/common/workbenchTestServices.js'; @@ -107,6 +107,8 @@ export async function startTestLanguageRuntimeSession( options?.sessionMode ?? LanguageRuntimeSessionMode.Console, options?.notebookUri, options?.startReason ?? 'Test requested to start a runtime session', + RuntimeStartMode.Starting, + true ); // Get the session. diff --git a/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts b/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts index 0c7cbe6a4f0..b026f0616dc 100644 --- a/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts +++ b/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts @@ -12,17 +12,14 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IEphemeralStateService } from '../../../../platform/ephemeralState/common/ephemeralState.js'; import { IExtensionService } from '../../extensions/common/extensions.js'; -import { ILanguageRuntimeExit, ILanguageRuntimeMetadata, ILanguageRuntimeService, LanguageRuntimeSessionLocation, LanguageRuntimeSessionMode, LanguageRuntimeStartupBehavior, RuntimeExitReason, RuntimeState, formatLanguageRuntimeMetadata } from '../../languageRuntime/common/languageRuntimeService.js'; -import { IRuntimeStartupService, RuntimeStartupPhase } from './runtimeStartupService.js'; -import { ILanguageRuntimeSession, IRuntimeSessionMetadata, IRuntimeSessionService } from '../../runtimeSession/common/runtimeSessionService.js'; -import { Event } from '../../../../base/common/event.js'; -import { ISettableObservable, observableValue } from '../../../../base/common/observableInternal/base.js'; +import { ILanguageRuntimeExit, ILanguageRuntimeMetadata, ILanguageRuntimeService, LanguageRuntimeSessionLocation, LanguageRuntimeSessionMode, LanguageRuntimeStartupBehavior, RuntimeExitReason, RuntimeStartupPhase, RuntimeState, formatLanguageRuntimeMetadata } from '../../languageRuntime/common/languageRuntimeService.js'; +import { IRuntimeStartupService } from './runtimeStartupService.js'; +import { ILanguageRuntimeSession, IRuntimeSessionMetadata, IRuntimeSessionService, RuntimeStartMode } from '../../runtimeSession/common/runtimeSessionService.js'; import { ExtensionsRegistry } from '../../extensions/common/extensionsRegistry.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { ILifecycleService, ShutdownReason } from '../../lifecycle/common/lifecycle.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { URI } from '../../../../base/common/uri.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { IPositronNewProjectService } from '../../positronNewProject/common/positronNewProject.js'; @@ -31,12 +28,28 @@ interface ILanguageRuntimeProviderMetadata { languageId: string; } +/** + * The serialization format for affiliated runtime metadata. + */ +interface IAffiliatedRuntimeMetadata { + metadata: ILanguageRuntimeMetadata; + lastUsed: number; +} + /** * Metadata for serialized runtime sessions. */ interface SerializedSessionMetadata { + /// The metadata for the runtime session itself. metadata: IRuntimeSessionMetadata; + + /// The state of the runtime, at the time it was serialized. sessionState: RuntimeState; + + /// The time at which the session was last used, in milliseconds since the epoch. + lastUsed: number; + + /// The metadata of the runtime associated with the session. runtimeMetadata: ILanguageRuntimeMetadata; } @@ -72,7 +85,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // Needed for service branding in dependency injector. declare readonly _serviceBrand: undefined; - private readonly storageKey = 'positron.affiliatedRuntimeMetadata'; + private readonly storageKey = 'positron.affiliatedRuntimeMetadata.v1'; // The language packs; a map of language ID to a list of extensions that provide the language. private readonly _languagePacks: Map> = new Map(); @@ -90,17 +103,14 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // across all extension hosts. private readonly _discoveryCompleteByExtHostId = new Map(); - // The current startup phase; an observeable value. - private _startupPhase: ISettableObservable; + // The current startup phase + private _startupPhase: RuntimeStartupPhase; // Whether we are shutting down private _shuttingDown = false; - onDidChangeRuntimeStartupPhase: Event; - constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, - @ICommandService commandService: ICommandService, @IExtensionService private readonly _extensionService: IExtensionService, @ILanguageService private readonly _languageService: ILanguageService, @ILanguageRuntimeService private readonly _languageRuntimeService: ILanguageRuntimeService, @@ -124,13 +134,15 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup this._languageRuntimeService.onDidRegisterRuntime( this.onDidRegisterRuntime, this)); - this._startupPhase = observableValue( - 'runtime-startup-phase', RuntimeStartupPhase.Initializing); - this.onDidChangeRuntimeStartupPhase = Event.fromObservable(this._startupPhase); + // Register the startup phase event handler. + this._startupPhase = _languageRuntimeService.startupPhase; + this._register( + this._languageRuntimeService.onDidChangeRuntimeStartupPhase( + (phase) => { + this._logService.debug(`[Runtime startup] Phase changed to '${phase}'`); + this._startupPhase = phase; + })); - this._register(this.onDidChangeRuntimeStartupPhase(phase => { - this._logService.debug(`[Runtime startup] Phase changed to '${phase}'`); - })); this._register(this._runtimeSessionService.onWillStartSession(e => { this._register(e.session.onDidEncounterStartupFailure(_exit => { @@ -170,7 +182,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // When the discovery phase is complete, check to see if we need to // auto-start a runtime. - this._register(this.onDidChangeRuntimeStartupPhase(phase => { + this._register(this._languageRuntimeService.onDidChangeRuntimeStartupPhase(phase => { if (phase === RuntimeStartupPhase.Complete) { // if no runtimes were found, notify the user about the problem if (this._languageRuntimeService.registeredRuntimes.length === 0) { @@ -189,7 +201,8 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup metadata.startupBehavior === LanguageRuntimeStartupBehavior.Immediate); if (languageRuntimes.length) { this._runtimeSessionService.autoStartRuntime(languageRuntimes[0], - `An extension requested the runtime to be started immediately.`); + `An extension requested the runtime to be started immediately.`, + true); } } } @@ -209,11 +222,11 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // - We have completed the discovery phase of the language runtime // registration process. if (runtime.startupBehavior === LanguageRuntimeStartupBehavior.Immediate && - this.startupPhase === RuntimeStartupPhase.Complete && + this._startupPhase === RuntimeStartupPhase.Complete && !this._runtimeSessionService.hasStartingOrRunningConsole()) { this._runtimeSessionService.autoStartRuntime(runtime, - `An extension requested that the runtime start immediately after being registered.`); + `An extension requested that the runtime start immediately after being registered.`, true); } // Automatically start the language runtime under the following conditions: @@ -225,14 +238,14 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // - There's no runtime affiliated with the current workspace for this // language (if there is, we want that runtime to start, not this one) else if (this._encounteredLanguagesByLanguageId.has(runtime.languageId) && - this.startupPhase === RuntimeStartupPhase.Complete && + this._startupPhase === RuntimeStartupPhase.Complete && !this._runtimeSessionService.hasStartingOrRunningConsole(runtime.languageId) && runtime.startupBehavior === LanguageRuntimeStartupBehavior.Implicit && !this.getAffiliatedRuntimeMetadata(runtime.languageId)) { this._runtimeSessionService.autoStartRuntime(runtime, `A file with the language ID ${runtime.languageId} was open ` + - `when the runtime was registered.`); + `when the runtime was registered.`, true); } })); @@ -246,14 +259,14 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup } else { // If we are not in a trusted workspace, wait for the workspace to become // trusted before starting the startup sequence. - this._startupPhase.set(RuntimeStartupPhase.AwaitingTrust, undefined); + this.setStartupPhase(RuntimeStartupPhase.AwaitingTrust); this._register(this._workspaceTrustManagementService.onDidChangeTrust((trusted) => { if (!trusted) { return; } // If the workspace becomse trusted while we are awaiting trust, // move on to the startup sequence. - if (this.startupPhase === RuntimeStartupPhase.AwaitingTrust) { + if (this._startupPhase === RuntimeStartupPhase.AwaitingTrust) { this.startupSequence(); } })); @@ -280,12 +293,12 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // If we were awaiting trust, and we now have language packs, move on // to the discovery phase if we haven't already and there are now registered // language packs. - if (this.startupPhase === RuntimeStartupPhase.AwaitingTrust) { + if (this._startupPhase === RuntimeStartupPhase.AwaitingTrust) { if (this._languagePacks.size > 0) { this.discoverAllRuntimes(); } else { this._logService.debug(`[Runtime startup] No language packs were found.`); - this._startupPhase.set(RuntimeStartupPhase.Complete, undefined); + this.setStartupPhase(RuntimeStartupPhase.Complete); } } })); @@ -311,6 +324,14 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup })); } + /** + * Convenience method for setting the startup phase. + */ + private setStartupPhase(phase: RuntimeStartupPhase): void { + this._startupPhase = phase; + this._languageRuntimeService.setStartupPhase(phase); + } + /** * The main entry point for the runtime startup service. */ @@ -325,8 +346,12 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup await this._newProjectService.initTasksComplete.wait(); const newRuntime = this._newProjectService.newProjectRuntimeMetadata; if (newRuntime) { + const newAffiliation: IAffiliatedRuntimeMetadata = { + metadata: newRuntime, + lastUsed: Date.now() + }; this._storageService.store(this.storageKeyForRuntime(newRuntime), - JSON.stringify(newRuntime), + JSON.stringify(newAffiliation), StorageScope.WORKSPACE, StorageTarget.MACHINE); } @@ -343,12 +368,6 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup } /** - * Returns the current startup phase. - */ - get startupPhase(): RuntimeStartupPhase { - return this._startupPhase.get(); - } - /** * Signals that the runtime discovery phase is completed only after all * extension hosts have completed runtime discovery. @@ -372,7 +391,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // The 'Discovery' phase is considered complete only after all extension hosts // have signaled they have completed their own runtime discovery if (discoveryCompletedByAllExtensionHosts) { - this._startupPhase.set(RuntimeStartupPhase.Complete, undefined); + this.setStartupPhase(RuntimeStartupPhase.Complete); // Reset the discovery state for each ext host so we are ready // for possible re-discovery of runtimes this._discoveryCompleteByExtHostId.forEach((_, extHostId, m) => { @@ -427,8 +446,12 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup } // Save this runtime as the affiliated runtime for the current workspace. + const affiliated: IAffiliatedRuntimeMetadata = { + metadata: session.runtimeMetadata, + lastUsed: Date.now() + }; this._storageService.store(this.storageKeyForRuntime(session.runtimeMetadata), - JSON.stringify(session.runtimeMetadata), + JSON.stringify(affiliated), this.affiliationStorageScope(), StorageTarget.MACHINE); @@ -440,13 +463,14 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup if (newState === RuntimeState.Exiting) { // Just to be safe, check that the runtime is still affiliated // before removing the affiliation - const affiliatedRuntimeMetadata = this._storageService.get( + const serializedMetadata = this._storageService.get( this.storageKeyForRuntime(session.runtimeMetadata), this.affiliationStorageScope()); - if (!affiliatedRuntimeMetadata) { + if (!serializedMetadata) { return; } - const affiliatedRuntimeId = JSON.parse(affiliatedRuntimeMetadata).runtimeId; + const affiliated = JSON.parse(serializedMetadata) as IAffiliatedRuntimeMetadata; + const affiliatedRuntimeId = affiliated.metadata.runtimeId; if (session.runtimeMetadata.runtimeId === affiliatedRuntimeId) { // Remove the affiliation this._storageService.remove(this.storageKeyForRuntime(session.runtimeMetadata), @@ -466,7 +490,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // If we have no language packs yet, but were awaiting trust, we need to // wait until the language packs are reloaded with the new trust // settings before we can continue. - if (this.startupPhase === RuntimeStartupPhase.AwaitingTrust && + if (this._startupPhase === RuntimeStartupPhase.AwaitingTrust && this._languagePacks.size === 0) { // Wait up to 5 seconds for the language packs to be reloaded; @@ -476,8 +500,8 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // work to do; mark as complete so we don't hang in the // AwaitingTrust phase forever. setTimeout(() => { - if (this.startupPhase === RuntimeStartupPhase.AwaitingTrust) { - this._startupPhase.set(RuntimeStartupPhase.Complete, undefined); + if (this._startupPhase === RuntimeStartupPhase.AwaitingTrust) { + this.setStartupPhase(RuntimeStartupPhase.Complete); } }, 5000); return; @@ -492,7 +516,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // Enter the discovery phase; this triggers us to ask each extension for its // language runtime providers. - this._startupPhase.set(RuntimeStartupPhase.Discovering, undefined); + this.setStartupPhase(RuntimeStartupPhase.Discovering); } /** @@ -505,7 +529,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup private onDidRegisterRuntime(metadata: ILanguageRuntimeMetadata): void { // Ignore if we're not in the discovery phase. - if (this.startupPhase !== RuntimeStartupPhase.Discovering) { + if (this._startupPhase !== RuntimeStartupPhase.Discovering) { return; } @@ -520,8 +544,8 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup if (!affiliatedRuntimeMetadataStr) { return; } - const affiliatedRuntimeMetadata = JSON.parse(affiliatedRuntimeMetadataStr); - const affiliatedRuntimeId = affiliatedRuntimeMetadata.runtimeId; + const affiliated = JSON.parse(affiliatedRuntimeMetadataStr) as IAffiliatedRuntimeMetadata; + const affiliatedRuntimeId = affiliated.metadata.runtimeId; // If the runtime is affiliated with this workspace, start it. if (metadata.runtimeId === affiliatedRuntimeId) { @@ -532,7 +556,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup 'interpreters.automaticStartup'); if (!autoStart) { this._logService.info(`Language runtime ` + - `${formatLanguageRuntimeMetadata(affiliatedRuntimeMetadata)} ` + + `${formatLanguageRuntimeMetadata(affiliated.metadata)} ` + `is affiliated with this workspace, but won't be started because automatic ` + `startup is disabled in configuration.`); return; @@ -540,7 +564,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup if (metadata.startupBehavior === LanguageRuntimeStartupBehavior.Manual) { this._logService.info(`Language runtime ` + - `${formatLanguageRuntimeMetadata(affiliatedRuntimeMetadata)} ` + + `${formatLanguageRuntimeMetadata(affiliated.metadata)} ` + `is affiliated with this workspace, but won't be started because its ` + `startup behavior is manual.`); return; @@ -550,7 +574,9 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup metadata.runtimeName, LanguageRuntimeSessionMode.Console, undefined, // Console session - `Affiliated runtime for workspace`); + `Affiliated runtime for workspace`, + RuntimeStartMode.Starting, + true); } catch (e) { // This isn't necessarily an error; if another runtime took precedence and has // already started for this workspace, we don't want to start this one. @@ -569,13 +595,22 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup * @returns The runtime metadata. */ public getAffiliatedRuntimeMetadata(languageId: string): ILanguageRuntimeMetadata | undefined { + const affiliated = this.getAffiliatedRuntime(languageId); + if (!affiliated) { + return undefined; + } + return affiliated.metadata; + } + + private getAffiliatedRuntime(languageId: string): IAffiliatedRuntimeMetadata | undefined { const stored = this._storageService.get(`${this.storageKey}.${languageId}`, this.affiliationStorageScope()); if (!stored) { return undefined; } try { - return JSON.parse(stored) as ILanguageRuntimeMetadata; + const affiliated = JSON.parse(stored) as IAffiliatedRuntimeMetadata; + return affiliated; } catch (err) { this._logService.error(`Error parsing JSON for ${this.storageKey}: ${err}`); return undefined; @@ -665,14 +700,34 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup * Starts all affiliated runtimes for the workspace. */ private async startAffiliatedLanguageRuntimes(): Promise { - this._startupPhase.set(RuntimeStartupPhase.Starting, undefined); + this.setStartupPhase(RuntimeStartupPhase.Starting); const languageIds = this.getAffiliatedRuntimeLanguageIds(); - if (languageIds) { - // Activate all the extensions that provide language runtimes for the - // affiliated languages. - await this.activateExtensionsForLanguages(languageIds); - languageIds.map(languageId => this.startAffiliatedRuntime(languageId)); + + // No affiliated runtimes; move on to the next phase. + if (!languageIds) { + return; } + + // Activate all the extensions that provide language runtimes for the + // affiliated languages. + await this.activateExtensionsForLanguages(languageIds); + + // Start the affiliated runtimes. + languageIds.map(languageId => { + // Get the affiliated runtime metadata. + return this.getAffiliatedRuntime(languageId); + }).filter(affiliation => { + // Filter out any affiliations that didn't deserialize properly. + return affiliation !== undefined; + }).sort((a, b) => { + // Sort the affiliations by last used time, so that the most recently + // used runtime is started first + return b.lastUsed - a.lastUsed; + }).map((affiliation, idx) => { + // Start each runtime. Activate the first one as soon as it's + // ready; let the others start in the background. + this.startAffiliatedRuntime(affiliation.metadata, idx === 0); + }); } /** @@ -729,35 +784,43 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup /** * Starts an affiliated runtime for a single language. + * + * @param affiliatedRuntimeMetadata The metadata for the affiliated runtime. + * @param activate Whether to activate/focus the new session */ - private startAffiliatedRuntime(languageId: string): void { - const affiliatedRuntimeMetadata = - this.getAffiliatedRuntimeMetadata(languageId); + private startAffiliatedRuntime( + affiliatedRuntimeMetadata: ILanguageRuntimeMetadata, + activate: boolean + ): void { - if (affiliatedRuntimeMetadata) { - // Check the setting to see if we should be auto-starting. - const autoStart = this._configurationService.getValue( - 'interpreters.automaticStartup'); + // No-op if no affiliated runtime metadata. + if (!affiliatedRuntimeMetadata) { + return; + } - if (autoStart) { + // Check the setting to see if we should be auto-starting. + const autoStart = this._configurationService.getValue( + 'interpreters.automaticStartup'); - if (affiliatedRuntimeMetadata.startupBehavior === LanguageRuntimeStartupBehavior.Manual) { - this._logService.info(`Language runtime ` + - `${formatLanguageRuntimeMetadata(affiliatedRuntimeMetadata)} ` + - `is affiliated with this workspace, but won't be started because it's startup ` + - `behavior is manual.`); - return; - } + if (autoStart) { - this._runtimeSessionService.autoStartRuntime(affiliatedRuntimeMetadata, - `Affiliated ${languageId} runtime for workspace`); - } else { + if (affiliatedRuntimeMetadata.startupBehavior === LanguageRuntimeStartupBehavior.Manual) { this._logService.info(`Language runtime ` + `${formatLanguageRuntimeMetadata(affiliatedRuntimeMetadata)} ` + - `is affiliated with this workspace, but won't be started because automatic ` + - `startup is disabled in configuration.`); + `is affiliated with this workspace, but won't be started because it's startup ` + + `behavior is manual.`); return; } + + this._runtimeSessionService.autoStartRuntime(affiliatedRuntimeMetadata, + `Affiliated ${affiliatedRuntimeMetadata.languageName} runtime for workspace`, + activate); + } else { + this._logService.info(`Language runtime ` + + `${formatLanguageRuntimeMetadata(affiliatedRuntimeMetadata)} ` + + `is affiliated with this workspace, but won't be started because automatic ` + + `startup is disabled in configuration.`); + return; } } @@ -824,7 +887,8 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup * @param sessions The set of sessions to restore. */ private async restoreWorkspaceSessions(sessions: SerializedSessionMetadata[]) { - this._startupPhase.set(RuntimeStartupPhase.Reconnecting, undefined); + + this.setStartupPhase(RuntimeStartupPhase.Reconnecting); // Activate any extensions needed for the sessions that are persistent on the machine. const activatedExtensions: Array = []; @@ -885,27 +949,36 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // Remove all the sessions that are no longer valid. sessions = sessions.filter((_, i) => validSessions[i]); + // Sort the sessions by last used time, so that we reconnect to the + // most recently used sessions first. Default 0 so we can restore + // sessions that didn't persist this information. + sessions.sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0)); + // Reconnect to the remaining sessions. this._logService.debug(`Reconnecting to sessions: ` + sessions.map(session => session.metadata.sessionName).join(', ')); - await Promise.all(sessions.map(async session => { + await Promise.all(sessions.map(async (session, idx) => { + const marker = + `[Reconnect ${session.metadata.sessionId} (${idx + 1}/${sessions.length})]`; + // Activate the extension that provides the runtime. Note that this // waits for the extension service to signal the extension but does // not wait for the extension to activate. - this._logService.debug(`[Reconnect ${session.metadata.sessionId}]: ` + - `Activating extension ${session.runtimeMetadata.extensionId.value}`); - await this._extensionService.activateById(session.runtimeMetadata.extensionId, - { - extensionId: session.runtimeMetadata.extensionId, - activationEvent: `onLanguageRuntime:${session.runtimeMetadata.languageId}`, - startup: false - }); - - this._logService.debug(`[Reconnect ${session.metadata.sessionId}]: ` + - `Restoring session for ${session.metadata.sessionName}`); + if (!activatedExtensions.includes(session.runtimeMetadata.extensionId)) { + await this._extensionService.activateById(session.runtimeMetadata.extensionId, + { + extensionId: session.runtimeMetadata.extensionId, + activationEvent: `onLanguageRuntime:${session.runtimeMetadata.languageId}`, + startup: false + }); + } + + this._logService.debug(`${marker}: Restoring session for ${session.metadata.sessionName}`); + + // Reconnect to the session; activate it if it is the first session await this._runtimeSessionService.restoreRuntimeSession( - session.runtimeMetadata, session.metadata); + session.runtimeMetadata, session.metadata, idx === 0); })); } @@ -940,7 +1013,8 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup const metadata: SerializedSessionMetadata = { metadata: session.metadata, sessionState: session.getRuntimeState(), - runtimeMetadata: session.runtimeMetadata + runtimeMetadata: session.runtimeMetadata, + lastUsed: session.lastUsed, }; return metadata; }); @@ -978,7 +1052,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // Ignore if we are still starting up; if a runtime crashes or exits // during startup, we'll usually try to start a better one instead of // booting to a broken REPL. - if (this.startupPhase !== RuntimeStartupPhase.Complete) { + if (this._startupPhase !== RuntimeStartupPhase.Complete) { return; } @@ -1004,7 +1078,9 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup session.metadata.sessionName, session.metadata.sessionMode, session.metadata.notebookUri, - `The runtime exited unexpectedly and is being restarted automatically.`); + `The runtime exited unexpectedly and is being restarted automatically.`, + RuntimeStartMode.Restarting, + false); action = 'and was automatically restarted'; } else { action = 'and was not automatically restarted'; diff --git a/src/vs/workbench/services/runtimeStartup/common/runtimeStartupService.ts b/src/vs/workbench/services/runtimeStartup/common/runtimeStartupService.ts index 2c3e61849b7..11cf4fbce2e 100644 --- a/src/vs/workbench/services/runtimeStartup/common/runtimeStartupService.ts +++ b/src/vs/workbench/services/runtimeStartup/common/runtimeStartupService.ts @@ -1,61 +1,14 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ILanguageRuntimeMetadata } from '../../languageRuntime/common/languageRuntimeService.js'; -import { Event } from '../../../../base/common/event.js'; export const IRuntimeStartupService = createDecorator('runtimeStartupService'); -/** - * The phases through which the runtime startup service progresses as Positron - * starts. - */ -export enum RuntimeStartupPhase { - /** - * Phase 1: The startup sequence has not yet begun. - */ - Initializing = 'initializing', - - /** - * Phase 2: If the workspace is not trusted, we cannot proceed with startup, - * since many runtimes run arbitrary code at startup (often from the - * workspace contents) and we cannot trust them to do so safely. The startup - * sequence stays at `AwaitingTrust` until workspace trust is granted. - */ - AwaitingTrust = 'awaitingTrust', - - /** - * Phase 3: Positron is reconnecting to runtimes that are already running. - * We only enter this phase when reloading the UI, or when reopening a - * browser tab. - */ - Reconnecting = 'reconnecting', - - /** - * Phase 4: Positron is starting any runtimes that are affiliated with the - * workspace. We enter this phase on a fresh start of Positron, when no - * existing sessions are running. - */ - Starting = 'starting', - - /** - * Phase 5: Positron is discovering all the runtimes on the machine. This - * can take a while, but does precede startup for workspaces that have no - * affiliated runtimes (so we don't know what to start yet). - */ - Discovering = 'discovering', - - /** - * Phase 6: Startup is complete. In this phase, we start any runtimes - * recommended by extensions if nothing was started in previous phases. - */ - Complete = 'complete', -} - /** * The IRuntimeStartupService is responsible for coordinating the process by * which runtimes are automatically started when a workspace is opened, and @@ -65,16 +18,6 @@ export interface IRuntimeStartupService { // Needed for service branding in dependency injector. readonly _serviceBrand: undefined; - /** - * Event tracking the current startup phase. - */ - onDidChangeRuntimeStartupPhase: Event; - - /** - * The current startup phase. - */ - readonly startupPhase: RuntimeStartupPhase; - /** * Get the preferred runtime for a language. This approximates "the runtime * the user probably wants to start for the given language" and takes a