From 58c960d4ba690d10048901e86f88489333db0e09 Mon Sep 17 00:00:00 2001 From: "Yuandi Zhang (from Dev Box)" Date: Thu, 6 Aug 2026 13:28:22 +0800 Subject: [PATCH 1/5] Defer layout restore for COM activation Keep -Embedding launches headless without discarding persisted layouts, then restore them when a normal Terminal activation is handed off. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WindowsTerminal/WindowEmperor.cpp | 62 +++++++++++++------ src/cascadia/WindowsTerminal/WindowEmperor.h | 2 + 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index fdda5b3bff..5c0b690f0d 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -283,6 +283,7 @@ void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args _windowCount += 1; _windows.emplace_back(std::move(host)); + _deferPersistedLayoutRestore = false; // Wire the new window's TerminalPage::ProtocolVtSequenceReceived // into the COM fan-out so events emitted by panes in this window @@ -576,6 +577,10 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) _app = winrt::TerminalApp::App{}; _app.Logic().ReloadSettings(); + const auto args = commandlineToArgArray(GetCommandLineW()); + const auto isEmbedding = args.size() == 2 && args[1] == L"-Embedding"; + _deferPersistedLayoutRestore = isEmbedding; + _createMessageWindow(windowClassName.c_str()); _setupGlobalHotkeys(); _checkWindowsForNotificationIcon(); @@ -601,28 +606,14 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) const auto env = stringFromDoubleNullTerminated(envMem.get()); const auto cwd = wil::GetCurrentDirectoryW(); const auto showCmd = gsl::narrow_cast(nCmdShow); - - // Restore persisted windows. - const auto state = ApplicationState::SharedInstance(); - const auto layouts = state.PersistedWindowLayouts(); - if (layouts && layouts.Size() > 0) + if (!isEmbedding) { - _needsPersistenceCleanup = true; - - uint32_t startIdx = 0; - for (const auto layout : layouts) - { - hstring args[] = { L"wt", L"-w", L"new", L"-s", winrt::to_hstring(startIdx) }; - _dispatchCommandlineCommon(args, cwd, env, showCmd); - startIdx += 1; - } + _restorePersistedWindows(cwd, env, showCmd); } - const auto args = commandlineToArgArray(GetCommandLineW()); - - if (args.size() == 2 && args[1] == L"-Embedding") + if (isEmbedding) { - // We were launched for ConPTY handoff. We have no windows and also don't want to exit. + // We were launched as a COM server. We have no windows and also don't want to exit. // // TODO: Here we could start a timer and exit after, say, 5 seconds // if no windows are created. But that's a minor concern. @@ -1296,7 +1287,11 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c // toast's Activated event, so just ignore this handoff. if (argv.size() != 2 || argv[1] != L"--from-toast") { - _dispatchCommandlineCommon(argv, handoff.cwd, handoff.env, handoff.show); + const auto restoredPersistedWindows = _deferPersistedLayoutRestore && _restorePersistedWindows(handoff.cwd, handoff.env, handoff.show); + if (!restoredPersistedWindows || argv.size() != 1) + { + _dispatchCommandlineCommon(argv, handoff.cwd, handoff.env, handoff.show); + } } } return 0; @@ -1336,6 +1331,30 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c return DefWindowProcW(window, message, wParam, lParam); } +bool WindowEmperor::_restorePersistedWindows(wil::zwstring_view currentDirectory, wil::zwstring_view envString, uint32_t showWindowCommand) +{ + _deferPersistedLayoutRestore = false; + const auto previousWindowCount = _windows.size(); + + const auto state = ApplicationState::SharedInstance(); + const auto layouts = state.PersistedWindowLayouts(); + if (!layouts || layouts.Size() == 0) + { + return false; + } + + _needsPersistenceCleanup = true; + + uint32_t startIdx = 0; + for (const auto layout : layouts) + { + hstring args[] = { L"wt", L"-w", L"new", L"-s", winrt::to_hstring(startIdx) }; + _dispatchCommandlineCommon(args, currentDirectory, envString, showWindowCommand); + startIdx += 1; + } + return _windows.size() > previousWindowCount; +} + void WindowEmperor::_setupSessionPersistence(bool enabled) { if (!enabled) @@ -1352,6 +1371,11 @@ void WindowEmperor::_setupSessionPersistence(bool enabled) void WindowEmperor::_persistState(const ApplicationState& state) const { + if (_deferPersistedLayoutRestore) + { + return; + } + // Calling an `ApplicationState` setter triggers a write to state.json. // With this if condition we avoid an unnecessary write when persistence is disabled. if (state.PersistedWindowLayouts()) diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index 1caaea7f7c..98a1e9c785 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -89,6 +89,7 @@ class WindowEmperor void _registerHotKey(int index, const winrt::Microsoft::Terminal::Control::KeyChord& hotkey) noexcept; void _unregisterHotKey(int index) noexcept; void _setupGlobalHotkeys(); + bool _restorePersistedWindows(wil::zwstring_view currentDirectory, wil::zwstring_view envString, uint32_t showWindowCommand); void _setupSessionPersistence(bool enabled); void _persistState(const winrt::Microsoft::Terminal::Settings::Model::ApplicationState& state) const; void _finalizeSessionPersistence() const; @@ -109,6 +110,7 @@ class WindowEmperor bool _notificationIconShown = false; bool _skipPersistence = false; bool _needsPersistenceCleanup = false; + bool _deferPersistedLayoutRestore = false; SafeDispatcherTimer _persistStateTimer; std::optional _currentSystemThemeIsDark; int32_t _windowCount = 0; From e7a0010495ed77731937794cf3fef5a14cb1f958 Mon Sep 17 00:00:00 2001 From: "Yuandi Zhang (from Dev Box)" Date: Mon, 24 Aug 2026 22:01:41 +0800 Subject: [PATCH 2/5] Restore deferred layouts on every real activation The headless COM activation path deferred the saved-layout restore but only replayed it on a WM_COPYDATA handoff. A defterm ConPTY handoff or a named window request would create a window without ever restoring the layout, stranding it until the next process start. Route the deferred restore through _restoreDeferredPersistedLayouts() and call it from the ConPTY handoff and OpenWindow() paths as well. Capture the startup directory, environment and show command so those later entry points can supply the launch context. Also guard _finalizeSessionPersistence() and make the restore exception safe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 75cc51f7-c713-48cb-a8d7-c8e0d54b39d5 --- .../WindowsTerminal/WindowEmperor.cpp | 52 ++++++++++++++++--- src/cascadia/WindowsTerminal/WindowEmperor.h | 7 +++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index acde5eb6fc..5ab7481c42 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -340,6 +340,11 @@ void WindowEmperor::OpenWindow(const winrt::hstring& name) return; } + // A named-window request is a genuine UI activation too, so restore a layout + // deferred by a headless COM activation before deciding whether the + // requested window already exists. + _restoreDeferredPersistedLayouts(_startupCurrentDirectory, _startupEnvironment, _startupShowWindowCommand); + // If a window with this name is already live, just summon it. // This mirrors the summon behavior in AppHost::DispatchCommandline (which is // what the old `wt -w ` ShellExecute path effectively triggered). @@ -613,12 +618,13 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) { const wil::unique_environstrings_ptr envMem{ GetEnvironmentStringsW() }; - const auto env = stringFromDoubleNullTerminated(envMem.get()); - const auto cwd = wil::GetCurrentDirectoryW(); - const auto showCmd = gsl::narrow_cast(nCmdShow); + _startupEnvironment = stringFromDoubleNullTerminated(envMem.get()); + _startupCurrentDirectory = wil::GetCurrentDirectoryW(); + _startupShowWindowCommand = gsl::narrow_cast(nCmdShow); + if (!isEmbedding) { - _restorePersistedWindows(cwd, env, showCmd); + _restorePersistedWindows(_startupCurrentDirectory, _startupEnvironment, _startupShowWindowCommand); } if (isEmbedding) @@ -633,7 +639,7 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) // Create another window if needed: There aren't any yet, OR we got an explicit command line. if (_windows.empty() || args.size() != 1) { - _dispatchCommandlineCommon(args, cwd, env, showCmd); + _dispatchCommandlineCommon(args, _startupCurrentDirectory, _startupEnvironment, _startupShowWindowCommand); } // If we created no windows, e.g. because the args are "/?" we can just exit now. @@ -650,6 +656,11 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) { TerminalConnection::ConptyConnection::NewConnection([this](TerminalConnection::ConptyConnection conn) { + // A defterm handoff is a genuine UI activation: if we stayed + // headless for COM activation, the saved layout still needs to come + // back before we add the handoff window. + _restoreDeferredPersistedLayouts(_startupCurrentDirectory, _startupEnvironment, _startupShowWindowCommand); + TerminalApp::CommandlineArgs args; args.ShowWindowCommand(conn.ShowWindow()); args.Connection(std::move(conn)); @@ -1300,7 +1311,7 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c // toast's Activated event, so just ignore this handoff. if (argv.size() != 2 || argv[1] != L"--from-toast") { - const auto restoredPersistedWindows = _deferPersistedLayoutRestore && _restorePersistedWindows(handoff.cwd, handoff.env, handoff.show); + const auto restoredPersistedWindows = _restoreDeferredPersistedLayouts(handoff.cwd, handoff.env, handoff.show); if (!restoredPersistedWindows || argv.size() != 1) { _dispatchCommandlineCommon(argv, handoff.cwd, handoff.env, handoff.show); @@ -1345,6 +1356,7 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c } bool WindowEmperor::_restorePersistedWindows(wil::zwstring_view currentDirectory, wil::zwstring_view envString, uint32_t showWindowCommand) +try { _deferPersistedLayoutRestore = false; const auto previousWindowCount = _windows.size(); @@ -1367,6 +1379,26 @@ bool WindowEmperor::_restorePersistedWindows(wil::zwstring_view currentDirectory } return _windows.size() > previousWindowCount; } +catch (...) +{ + LOG_CAUGHT_EXCEPTION(); + return false; +} + +// Runs the layout restore that a headless COM activation skipped. Called from +// every entry point that turns this process into a visible Terminal, so the +// saved layout comes back with the first real window instead of being stranded +// until the next process start. Returns false when there was nothing deferred +// or nothing to restore, so callers can fall back to their usual behavior. +bool WindowEmperor::_restoreDeferredPersistedLayouts(wil::zwstring_view currentDirectory, wil::zwstring_view envString, uint32_t showWindowCommand) +{ + if (!_deferPersistedLayoutRestore) + { + return false; + } + + return _restorePersistedWindows(currentDirectory, envString, showWindowCommand); +} void WindowEmperor::_setupSessionPersistence(bool enabled) { @@ -1412,6 +1444,14 @@ void WindowEmperor::_finalizeSessionPersistence() const { using namespace std::string_view_literals; + if (_deferPersistedLayoutRestore) + { + // We never left the headless COM-activation state, so there is no + // arrangement of ours to persist and the saved layout still belongs to + // the session that wrote it. + return; + } + if (_skipPersistence) { // We received WM_ENDSESSION and persisted the state. diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index 0ff4efd961..c2ca391d88 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -92,6 +92,7 @@ class WindowEmperor void _unregisterHotKey(int index) noexcept; void _setupGlobalHotkeys(); bool _restorePersistedWindows(wil::zwstring_view currentDirectory, wil::zwstring_view envString, uint32_t showWindowCommand); + bool _restoreDeferredPersistedLayouts(wil::zwstring_view currentDirectory, wil::zwstring_view envString, uint32_t showWindowCommand); void _setupSessionPersistence(bool enabled); void _persistState(const winrt::Microsoft::Terminal::Settings::Model::ApplicationState& state) const; void _finalizeSessionPersistence() const; @@ -115,6 +116,12 @@ class WindowEmperor bool _needsPersistenceCleanup = false; bool _deferPersistedLayoutRestore = false; SafeDispatcherTimer _persistStateTimer; + // Captured at startup so a deferred layout restore, which can be triggered + // long after HandleCommandlineArgs() returned, still sees the environment + // the process was launched with. + std::wstring _startupCurrentDirectory; + std::wstring _startupEnvironment; + uint32_t _startupShowWindowCommand = SW_SHOWDEFAULT; std::optional _currentSystemThemeIsDark; int32_t _windowCount = 0; int32_t _messageBoxCount = 0; From 770f8716f9c76577e6b8e75adbda87a800e80460 Mon Sep 17 00:00:00 2001 From: "Yuandi Zhang (from Dev Box)" Date: Mon, 24 Aug 2026 22:10:17 +0800 Subject: [PATCH 3/5] Explain what -Embedding covers now Our ExeServer registers both the defterm handoff class and TerminalProtocolComServer, so an -Embedding launch is just as likely a wtcli protocol activation as a console handoff. Say so where we decide to stay headless. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 75cc51f7-c713-48cb-a8d7-c8e0d54b39d5 --- src/cascadia/WindowsTerminal/WindowEmperor.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 5ab7481c42..012baf1664 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -629,7 +629,11 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) if (isEmbedding) { - // We were launched as a COM server. We have no windows and also don't want to exit. + // We were launched as a COM server. Our ExeServer hosts both the + // defterm handoff class and TerminalProtocolComServer, so this is + // just as likely a wtcli protocol activation as a console handoff. + // Stay headless and leave the saved layout alone; whichever + // activation actually wants a window restores it. // // TODO: Here we could start a timer and exit after, say, 5 seconds // if no windows are created. But that's a minor concern. From 9902fe5363763c2243e986e1b88e5d7a764d9612 Mon Sep 17 00:00:00 2001 From: "Yuandi Zhang (from Dev Box)" Date: Tue, 25 Aug 2026 08:09:46 +0800 Subject: [PATCH 4/5] Restore deferred layouts from CreateNewWindow Hooking individual activation paths could never be exhaustive: a global summon hotkey on a headless COM instance reaches _dispatchCommandlineCommon and CreateNewWindow directly, creating the first window without restoring the deferred layout, which _persistState then overwrites on exit. The notification icon has the same shape. Every window creation funnels through CreateNewWindow, so do the deferred restore there and drop the ConPTY handoff and OpenWindow hooks. Keep _deferPersistedLayoutRestore set until a window actually joins _windows, so a restore that opens nothing still protects the saved layout, and guard the resulting recursion separately. In the WM_COPYDATA path only pre-restore for a bare wt, where the point is to suppress the extra default window; other command lines now restore only if they really create a window. Also iterate the layouts by index instead of binding an unused element. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 75cc51f7-c713-48cb-a8d7-c8e0d54b39d5 --- .../WindowsTerminal/WindowEmperor.cpp | 66 +++++++++++-------- src/cascadia/WindowsTerminal/WindowEmperor.h | 1 + 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index 012baf1664..f4e5b06adb 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -268,6 +268,14 @@ void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args { _assertIsMainThread(); + // Our first window makes this process the owner of the persisted layout: + // _persistState() will replace it with whatever we have open. So whatever + // brought us here — a defterm handoff, a global hotkey, the notification + // icon — a layout that a headless COM activation deferred has to come back + // first, or it is lost. Every window creation funnels through here, so this + // is the one place that can make that promise. + _restoreDeferredPersistedLayouts(_startupCurrentDirectory, _startupEnvironment, _startupShowWindowCommand); + uint64_t id = args.Id(); bool needsNewId = id == 0; uint64_t newId = 0; @@ -293,6 +301,8 @@ void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args std::lock_guard lock{ _windowsMutex }; _windows.emplace_back(std::move(host)); } + // A window exists now, so this process owns the persisted layout and there + // is nothing left to defer. _deferPersistedLayoutRestore = false; // Wire the new window's TerminalPage::ProtocolVtSequenceReceived @@ -340,11 +350,6 @@ void WindowEmperor::OpenWindow(const winrt::hstring& name) return; } - // A named-window request is a genuine UI activation too, so restore a layout - // deferred by a headless COM activation before deciding whether the - // requested window already exists. - _restoreDeferredPersistedLayouts(_startupCurrentDirectory, _startupEnvironment, _startupShowWindowCommand); - // If a window with this name is already live, just summon it. // This mirrors the summon behavior in AppHost::DispatchCommandline (which is // what the old `wt -w ` ShellExecute path effectively triggered). @@ -594,6 +599,14 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) const auto args = commandlineToArgArray(GetCommandLineW()); const auto isEmbedding = args.size() == 2 && args[1] == L"-Embedding"; + + { + const wil::unique_environstrings_ptr envMem{ GetEnvironmentStringsW() }; + _startupEnvironment = stringFromDoubleNullTerminated(envMem.get()); + } + _startupCurrentDirectory = wil::GetCurrentDirectoryW(); + _startupShowWindowCommand = gsl::narrow_cast(nCmdShow); + // Only arm the deferral once the context a later restore needs is captured. _deferPersistedLayoutRestore = isEmbedding; _createMessageWindow(windowClassName.c_str()); @@ -617,11 +630,6 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) }); { - const wil::unique_environstrings_ptr envMem{ GetEnvironmentStringsW() }; - _startupEnvironment = stringFromDoubleNullTerminated(envMem.get()); - _startupCurrentDirectory = wil::GetCurrentDirectoryW(); - _startupShowWindowCommand = gsl::narrow_cast(nCmdShow); - if (!isEmbedding) { _restorePersistedWindows(_startupCurrentDirectory, _startupEnvironment, _startupShowWindowCommand); @@ -660,11 +668,6 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow) { TerminalConnection::ConptyConnection::NewConnection([this](TerminalConnection::ConptyConnection conn) { - // A defterm handoff is a genuine UI activation: if we stayed - // headless for COM activation, the saved layout still needs to come - // back before we add the handoff window. - _restoreDeferredPersistedLayouts(_startupCurrentDirectory, _startupEnvironment, _startupShowWindowCommand); - TerminalApp::CommandlineArgs args; args.ShowWindowCommand(conn.ShowWindow()); args.Connection(std::move(conn)); @@ -1315,8 +1318,13 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c // toast's Activated event, so just ignore this handoff. if (argv.size() != 2 || argv[1] != L"--from-toast") { - const auto restoredPersistedWindows = _restoreDeferredPersistedLayouts(handoff.cwd, handoff.env, handoff.show); - if (!restoredPersistedWindows || argv.size() != 1) + // A bare `wt` handoff is a plain "open the Terminal" + // activation: bring the deferred layout back and, if that + // produced windows, don't stack a default one on top. + // Anything else — `wt /?`, `wt -w new cmd.exe` — falls + // through to the usual dispatch, which restores the layout + // only if it actually creates a window. + if (argv.size() != 1 || !_restoreDeferredPersistedLayouts(handoff.cwd, handoff.env, handoff.show)) { _dispatchCommandlineCommon(argv, handoff.cwd, handoff.env, handoff.show); } @@ -1362,7 +1370,6 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c bool WindowEmperor::_restorePersistedWindows(wil::zwstring_view currentDirectory, wil::zwstring_view envString, uint32_t showWindowCommand) try { - _deferPersistedLayoutRestore = false; const auto previousWindowCount = _windows.size(); const auto state = ApplicationState::SharedInstance(); @@ -1374,12 +1381,10 @@ try _needsPersistenceCleanup = true; - uint32_t startIdx = 0; - for (const auto layout : layouts) + for (uint32_t index = 0; index < layouts.Size(); ++index) { - hstring args[] = { L"wt", L"-w", L"new", L"-s", winrt::to_hstring(startIdx) }; + const hstring args[] = { L"wt", L"-w", L"new", L"-s", winrt::to_hstring(index) }; _dispatchCommandlineCommon(args, currentDirectory, envString, showWindowCommand); - startIdx += 1; } return _windows.size() > previousWindowCount; } @@ -1389,18 +1394,23 @@ catch (...) return false; } -// Runs the layout restore that a headless COM activation skipped. Called from -// every entry point that turns this process into a visible Terminal, so the -// saved layout comes back with the first real window instead of being stranded -// until the next process start. Returns false when there was nothing deferred -// or nothing to restore, so callers can fall back to their usual behavior. +// Runs the layout restore that a headless COM activation skipped. Returns false +// when there was nothing deferred or nothing came back, so callers can fall back +// to their usual behavior. +// +// Note that _deferPersistedLayoutRestore stays set until a window actually +// joins _windows, so that a restore which opens nothing leaves the saved layout +// protected. That means the restore below re-enters this function through +// CreateNewWindow(), hence the separate recursion guard. bool WindowEmperor::_restoreDeferredPersistedLayouts(wil::zwstring_view currentDirectory, wil::zwstring_view envString, uint32_t showWindowCommand) { - if (!_deferPersistedLayoutRestore) + if (!_deferPersistedLayoutRestore || _restoringPersistedLayouts) { return false; } + _restoringPersistedLayouts = true; + const auto clearGuard = wil::scope_exit([this]() noexcept { _restoringPersistedLayouts = false; }); return _restorePersistedWindows(currentDirectory, envString, showWindowCommand); } diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index c2ca391d88..5ccdf961b5 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -115,6 +115,7 @@ class WindowEmperor bool _skipPersistence = false; bool _needsPersistenceCleanup = false; bool _deferPersistedLayoutRestore = false; + bool _restoringPersistedLayouts = false; SafeDispatcherTimer _persistStateTimer; // Captured at startup so a deferred layout restore, which can be triggered // long after HandleCommandlineArgs() returned, still sees the environment From c8d109543507eb0f5e1e5cbf0fde79f1df5e6257 Mon Sep 17 00:00:00 2001 From: "Yuandi Zhang (from Dev Box)" Date: Tue, 25 Aug 2026 08:38:04 +0800 Subject: [PATCH 5/5] Restore layouts with the triggering activation's context The deferred restore ran with the directory, environment and show command COM started us with, which for an -Embedding process is arbitrary. TerminalWindow feeds those into SetInitialCwd/VirtualEnvVars and AppHost into _launchShowWindowCommand, so restored windows could inherit the wrong context. Prefer the context carried by the activation that is creating the first window. For an ordinary launch the startup context is the activation context, so this restores that parity. Fall back field by field rather than on a null Command: a defterm handoff fills in only the show command, and a dragged-out window has no Command at all. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 75cc51f7-c713-48cb-a8d7-c8e0d54b39d5 --- .../WindowsTerminal/WindowEmperor.cpp | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index f4e5b06adb..25f598d87f 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -274,7 +274,33 @@ void WindowEmperor::CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args // icon — a layout that a headless COM activation deferred has to come back // first, or it is lost. Every window creation funnels through here, so this // is the one place that can make that promise. - _restoreDeferredPersistedLayouts(_startupCurrentDirectory, _startupEnvironment, _startupShowWindowCommand); + if (_deferPersistedLayoutRestore) + { + // Restore with the context of the activation that is creating this + // window rather than the one COM started us with. For an ordinary + // launch the startup context *is* the activation context, and that + // parity is what the deferral exists to preserve. Fall back field by + // field: a window dragged out carries no Command at all, and a defterm + // handoff fills in only the show command. + std::wstring currentDirectory{ _startupCurrentDirectory }; + std::wstring environment{ _startupEnvironment }; + auto showWindowCommand = _startupShowWindowCommand; + + if (const auto command = args.Command()) + { + if (const std::wstring_view commandDirectory{ command.CurrentDirectory() }; !commandDirectory.empty()) + { + currentDirectory = commandDirectory; + } + if (const std::wstring_view commandEnvironment{ command.CurrentEnvironment() }; !commandEnvironment.empty()) + { + environment = commandEnvironment; + } + showWindowCommand = command.ShowWindowCommand(); + } + + _restoreDeferredPersistedLayouts(currentDirectory, environment, showWindowCommand); + } uint64_t id = args.Id(); bool needsNewId = id == 0;