diff --git a/README.ja.md b/README.ja.md index d9aad5f4a2f..cd415699cf3 100644 --- a/README.ja.md +++ b/README.ja.md @@ -72,6 +72,7 @@ VS Codeのフォーク。本家にはないUI/UX改善を追加した(してい | `coderm.quickOpen.localFiles` | `boolean` | `true` | SSH接続時にQuick Openで絶対パスのローカルファイルを候補に含める | | `coderm.updateDownloadProgress.enabled` | `boolean` | `true` | アップデートダウンロード時に進捗通知を表示 | | `coderm.terminal.closeEmptyPaneOnKill` | `boolean` | `true` | ターミナルkill時に空ペインを閉じてフォーカス復帰 | +| `coderm.terminal.persistSessionOnReload` | `boolean` | `true` | reload時にターミナルセッションを完全復元(プロセス・cwd・scrollback・ペイン構成)。`enablePersistentSessions: false`時も有効、close/quitは破棄 | | `coderm.titleBar.hideMoreActions` | `boolean` | `true` | タイトルバー右端の「More Actions (`...`)」オーバーフローボタンを非表示 | | `coderm.inactiveOverlay.mode` | `string` | `"on"` | 非アクティブ時のオーバーレイ表示モード(`on` / `off` / `blur-off`) | | `coderm.inactiveOverlay.delay` | `number` | `300` | 非アクティブ検知からオーバーレイ表示までの遅延(ms, 0–5000) | diff --git a/README.md b/README.md index 02b38411d7a..0431cd332ed 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Settings unique to Coderm that are not available in upstream VS Code. | `coderm.quickOpen.localFiles` | `boolean` | `true` | Include local files with absolute paths in Quick Open when connected via SSH | | `coderm.updateDownloadProgress.enabled` | `boolean` | `true` | Show progress notification during update download | | `coderm.terminal.closeEmptyPaneOnKill` | `boolean` | `true` | Close empty pane and restore focus on terminal kill | +| `coderm.terminal.persistSessionOnReload` | `boolean` | `true` | Fully restore terminal sessions (process, cwd, scrollback, pane layout) on reload even when `enablePersistentSessions` is off; close/quit still discards | | `coderm.titleBar.hideMoreActions` | `boolean` | `true` | Hide the trailing "More Actions" overflow button (`...`) in the title bar | | `coderm.inactiveOverlay.mode` | `string` | `"on"` | Inactive-window overlay mode (`on` / `off` / `blur-off`) | | `coderm.inactiveOverlay.delay` | `number` | `300` | Delay before showing overlay when inactive (ms, 0–5000) | diff --git a/src/vs/workbench/contrib/coderm/browser/coderm.contribution.ts b/src/vs/workbench/contrib/coderm/browser/coderm.contribution.ts index becaa936600..b2b893f09a9 100644 --- a/src/vs/workbench/contrib/coderm/browser/coderm.contribution.ts +++ b/src/vs/workbench/contrib/coderm/browser/coderm.contribution.ts @@ -30,4 +30,5 @@ import './remoteSSHGuard.js'; import './startupFocusGuard.js'; import './terminalHorizontalPadding.js'; import './terminalKillFocusRestore.js'; +import './terminalPersistSessionOnReload.js'; import './updateDownloadProgress.js'; diff --git a/src/vs/workbench/contrib/coderm/browser/terminalPersistSessionOnReload.ts b/src/vs/workbench/contrib/coderm/browser/terminalPersistSessionOnReload.ts new file mode 100644 index 00000000000..a6732903f18 --- /dev/null +++ b/src/vs/workbench/contrib/coderm/browser/terminalPersistSessionOnReload.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// CODEM: Persist terminal sessions on window reload only. +// Even when terminal.integrated.enablePersistentSessions is disabled, fully restore +// process, cwd, scrollback, and pane layout on reload window. close/quit still discards. +// +// Approach: Subclass TerminalConfigurationService via DI override to wrap +// config.enablePersistentSessions according to reload context, reusing the existing VSCode +// persistence paths (pty host detach/reattach, layout info, editor serializer). The wrapper +// logic is contained in this single file, plus an alignment fix in terminalEditorInput.ts +// to route the existing direct read through config. + +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { ShutdownReason, ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; +import { localize } from '../../../../nls.js'; +import type { ITerminalConfiguration } from '../../terminal/common/terminal.js'; +import { TerminalConfigurationService } from '../../terminal/browser/terminalConfigurationService.js'; +import { ITerminalConfigurationService } from '../../terminal/browser/terminal.js'; + +// #region Configuration + +export const CodermTerminalPersistSessionOnReloadSetting = 'coderm.terminal.persistSessionOnReload'; + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + id: 'coderm.terminalPersistSessionOnReload', + order: 105, + title: localize('codermConfigurationTitle', 'Coderm'), + type: 'object', + properties: { + [CodermTerminalPersistSessionOnReloadSetting]: { + type: 'boolean', + default: true, + scope: ConfigurationScope.APPLICATION, + description: localize('coderm.terminal.persistSessionOnReload', + 'Persist terminal sessions on window reload even when "terminal.integrated.enablePersistentSessions" is disabled. Processes, cwd, scrollback, and pane layout are fully restored on reload only; close/quit behavior follows the upstream setting.'), + }, + }, +}); + +// #endregion + +// #region Wrapper Service + +/** + * Coderm wrapper around {@link TerminalConfigurationService} that forces + * `enablePersistentSessions` to `true` in reload-relevant contexts, even when + * the user has set `terminal.integrated.enablePersistentSessions: false`. + * + * Behavior matrix (when the coderm setting is enabled AND the user setting is false): + * - Normal operation / reload startup: force `true` so processes are created + * with `shouldPersist=true` (reload detach requires this at process creation + * time — `shouldPersist` is baked in readonly) and reconnection is enabled. + * - Reload shutdown (`reason === RELOAD`): force `true` so processes detach. + * - Non-reload shutdown (`CLOSE`/`QUIT`/`LOAD`): pass through `false` so + * processes are disposed and buffer revival is skipped, matching the user's + * `enablePersistentSessions: false` intent. + * + * Race-free guarantee: this service IS `ITerminalConfigurationService`, so DI + * instantiates it before any consumer (e.g. TerminalService). The + * `onBeforeShutdown` listener here registers before TerminalService's, and + * `Emitter.fire()` dispatches synchronously, so `_shutdownReason` is set + * before any consumer reads `config` during shutdown. + * + * Registration order: `coderm.contribution.ts` is imported AFTER + * `terminal.contribution.ts` in `workbench.common.main.ts`, and + * `ServiceCollection.set()` is last-wins, so this `registerSingleton` + * replaces the upstream `TerminalConfigurationService` descriptor. + */ +export class CodermTerminalConfigurationService extends TerminalConfigurationService { + + private _shutdownReason: ShutdownReason | undefined; + private _codermEnabledCached: boolean | undefined; + private _wrappedConfigBase: Readonly | undefined; + private _wrappedConfig: Readonly | undefined; + + // Parent's _configurationService is private; inject our own to read the coderm setting. + constructor( + @IConfigurationService private readonly _codermConfigurationService: IConfigurationService, + @ILifecycleService lifecycleService: ILifecycleService, + ) { + super(_codermConfigurationService); + + // Track shutdown reason to gate the override during non-reload shutdown. + this._register(lifecycleService.onBeforeShutdown(e => { + this._shutdownReason = e.reason; + })); + + // Reset if shutdown is vetoed (window stays open). + this._register(lifecycleService.onShutdownVeto(() => { + this._shutdownReason = undefined; + })); + + // Invalidate caches when the coderm setting changes. + this._register(this._codermConfigurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(CodermTerminalPersistSessionOnReloadSetting)) { + this._codermEnabledCached = undefined; + this._wrappedConfigBase = undefined; + } + })); + } + + private _isCodermEnabled(): boolean { + if (this._codermEnabledCached === undefined) { + // Note: `?? true` mirrors the registered `default: true` (see configuration registration above) as a + // defensive fallback for the rare case getValue() returns undefined (e.g. before defaults are loaded). + this._codermEnabledCached = this._codermConfigurationService.getValue(CodermTerminalPersistSessionOnReloadSetting) ?? true; + } + return this._codermEnabledCached; + } + + override get config(): Readonly { + const base = super.config; + // No override needed if user already has persistence enabled. + if (base.enablePersistentSessions) { + return base; + } + // No override if the Coderm feature is disabled. + if (!this._isCodermEnabled()) { + return base; + } + // During non-reload shutdown, respect the user's setting (false) so that + // close/quit disposes processes and skips buffer revival. + if (this._shutdownReason !== undefined && this._shutdownReason !== ShutdownReason.RELOAD) { + return base; + } + // Force enablePersistentSessions: true (normal op, reload shutdown, reload startup). + if (this._wrappedConfigBase !== base) { + this._wrappedConfigBase = base; + this._wrappedConfig = Object.freeze({ ...base, enablePersistentSessions: true }); + } + return this._wrappedConfig!; + } +} + +// Override the upstream ITerminalConfigurationService registration. +registerSingleton(ITerminalConfigurationService, CodermTerminalConfigurationService, InstantiationType.Delayed); + +// #endregion diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts index f7ce1dab62b..bd2f8af6df5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts @@ -11,7 +11,7 @@ import { EditorInputCapabilities, IEditorIdentifier, IUntypedEditorInput } from import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { EditorInput, IEditorCloseHandler } from '../../../common/editor/editorInput.js'; -import { ITerminalInstance, ITerminalInstanceService, terminalEditorId } from './terminal.js'; +import { ITerminalInstance, ITerminalInstanceService, terminalEditorId, ITerminalConfigurationService } from './terminal.js'; import { getColorClass, getUriClasses } from './terminalIcon.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IShellLaunchConfig, TerminalExitReason, TerminalLocation, TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; @@ -131,7 +131,8 @@ export class TerminalEditorInput extends EditorInput implements IEditorCloseHand @IConfigurationService private readonly _configurationService: IConfigurationService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IContextKeyService private _contextKeyService: IContextKeyService, - @IDialogService private readonly _dialogService: IDialogService + @IDialogService private readonly _dialogService: IDialogService, + @ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService ) { super(); @@ -182,7 +183,13 @@ export class TerminalEditorInput extends EditorInput implements IEditorCloseHand dispose(disposeListeners); // Don't touch processes if the shutdown was a result of reload as they will be reattached - const shouldPersistTerminals = this._configurationService.getValue(TerminalSettingId.EnablePersistentSessions) && e.reason === ShutdownReason.RELOAD; + // Use ITerminalConfigurationService.config to respect Coderm's reload-context wrapping + // Constraint: Must keep reading via ITerminalConfigurationService.config, not IConfigurationService.getValue(). + // All other persistence checks under terminal/ (terminalService.ts, terminalProcessManager.ts) route through + // the same service, and the Coderm wrapper (coderm/browser/terminalPersistSessionOnReload.ts) only intercepts + // the config getter. A direct getValue() here would bypass it and make editor-hosted terminals discard + // processes on reload even when coderm.terminal.persistSessionOnReload is enabled. + const shouldPersistTerminals = this._terminalConfigurationService.config.enablePersistentSessions && e.reason === ShutdownReason.RELOAD; if (shouldPersistTerminals) { instance.detachProcessAndDispose(TerminalExitReason.Shutdown); } else {