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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ import './remoteSSHGuard.js';
import './startupFocusGuard.js';
import './terminalHorizontalPadding.js';
import './terminalKillFocusRestore.js';
import './terminalPersistSessionOnReload.js';
import './updateDownloadProgress.js';
Original file line number Diff line number Diff line change
@@ -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<IConfigurationRegistry>(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<ITerminalConfiguration> | undefined;
private _wrappedConfig: Readonly<ITerminalConfiguration> | 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<boolean>(CodermTerminalPersistSessionOnReloadSetting) ?? true;
}
return this._codermEnabledCached;
}

override get config(): Readonly<ITerminalConfiguration> {
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
13 changes: 10 additions & 3 deletions src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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<boolean>(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 {
Expand Down
Loading