fix extra window and reorder/glom scenarios

This commit is contained in:
Carlos Zamora
2026-03-26 18:12:08 -07:00
parent 9df166efec
commit 5f3db13e5f
13 changed files with 121 additions and 34 deletions

View File

@@ -42,7 +42,7 @@ namespace winrt::TerminalApp::implementation
// - args: The title, message, and tab index to include in the notification.
// - activated: A callback invoked on the background thread when the
// toast is clicked. The uint32_t parameter is the tab index.
void DesktopNotification::SendNotification(const DesktopNotificationArgs& args, std::function<void(uint32_t tabIndex)> activatedFunc)
void DesktopNotification::SendNotification(const DesktopNotificationArgs& args, std::function<void()> activatedFunc)
{
try
{
@@ -83,17 +83,16 @@ namespace winrt::TerminalApp::implementation
auto toast = ToastNotification{ toastXml };
// Set the tag and group to enable notification replacement.
// Using the tab index as a tag means repeated output from the same tab
// replaces the previous notification rather than stacking.
toast.Tag(fmt::format(FMT_COMPILE(L"wt-tab-{}"), args.TabIndex));
// Repeated notifications with the same tag replace the previous one
// rather than stacking in the notification center.
toast.Tag(args.Tag);
toast.Group(L"WindowsTerminal");
// When the user activates (clicks) the toast, fire the callback.
if (activatedFunc)
{
const auto tabIndex = args.TabIndex;
toast.Activated([activatedFunc, tabIndex](const auto& /*sender*/, const auto& /*eventArgs*/) {
activatedFunc(tabIndex);
toast.Activated([activatedFunc](const auto& /*sender*/, const auto& /*eventArgs*/) {
activatedFunc();
});
}

View File

@@ -19,14 +19,14 @@ namespace winrt::TerminalApp::implementation
{
winrt::hstring Title;
winrt::hstring Message;
uint32_t TabIndex{ 0 };
winrt::hstring Tag;
};
class DesktopNotification
{
public:
static bool ShouldSendNotification();
static void SendNotification(const DesktopNotificationArgs& args, std::function<void(uint32_t tabIndex)> activatedFunc);
static void SendNotification(const DesktopNotificationArgs& args, std::function<void()> activatedFunc);
private:
static std::atomic<int64_t> _lastNotificationTime;

View File

@@ -1173,16 +1173,8 @@ namespace winrt::TerminalApp::implementation
co_await wil::resume_foreground(dispatcher);
if (const auto tab{ weakThisCopy.get() })
{
const auto notifTitle = notifArgs.Title();
const auto notifBody = notifArgs.Body();
if (!notifTitle.empty())
{
tab->TabToastNotificationRequested.raise(notifTitle, notifBody, tab->TabViewIndex());
}
else
{
tab->TabToastNotificationRequested.raise(tab->Title(), L"", tab->TabViewIndex());
}
const auto title = notifArgs.Title().empty() ? tab->Title() : notifArgs.Title();
tab->TabToastNotificationRequested.raise(title, notifArgs.Body());
}
});

View File

@@ -121,7 +121,7 @@ namespace winrt::TerminalApp::implementation
til::typed_event<TerminalApp::Tab, IInspectable> ActivePaneChanged;
til::event<winrt::delegate<>> TabRaiseVisualBell;
til::event<winrt::delegate<winrt::hstring /*title*/, winrt::hstring /*body*/, uint32_t /*tabIndex*/>> TabToastNotificationRequested;
til::event<winrt::delegate<winrt::hstring /*title*/, winrt::hstring /*body*/>> TabToastNotificationRequested;
til::typed_event<IInspectable, IInspectable> TaskbarProgressChanged;
// The TabViewIndex is the index this Tab object resides in TerminalPage's _tabs vector.

View File

@@ -153,10 +153,13 @@ namespace winrt::TerminalApp::implementation
// When a tab requests a desktop toast notification, send the toast
// and handle activation by summoning this window and switching to the tab.
newTabImpl->TabToastNotificationRequested([weakThis{ get_weak() }](const winrt::hstring& title, const winrt::hstring& body, uint32_t tabIndex) {
newTabImpl->TabToastNotificationRequested([weakThis{ get_weak() }, weakTab{ newTabImpl->get_weak() }](const winrt::hstring& title, const winrt::hstring& body) {
if (const auto page{ weakThis.get() })
{
page->_SendDesktopNotification(title, body, tabIndex);
if (const auto tab{ weakTab.get() })
{
page->_SendDesktopNotification(title, body, tab);
}
}
});
@@ -1196,15 +1199,31 @@ namespace winrt::TerminalApp::implementation
return _tabs.Size() > 1;
}
// Method Description:
// - Attempts to find and focus the given tab in this window.
// Arguments:
// - tab: The tab to focus.
// Return Value:
// - true if the tab was found and focused, false otherwise.
bool TerminalPage::FocusTab(const winrt::TerminalApp::Tab& tab)
{
if (const auto tabIndex{ _GetTabIndex(tab) })
{
_SelectTab(tabIndex.value());
return true;
}
return false;
}
// Method Description:
// - Sends a desktop toast notification with the given title and body.
// When the toast is activated (clicked), the window is summoned and
// the tab at tabIndex is focused.
// the originating tab is focused.
// Arguments:
// - tabTitle: The title to display in the notification.
// - body: The body text. If empty, a standard tab-activity message is built.
// - tabIndex: The index of the tab to switch to when the toast is activated.
void TerminalPage::_SendDesktopNotification(const winrt::hstring& tabTitle, const winrt::hstring& body, uint32_t tabIndex)
// - tab: The tab to switch to when the toast is activated.
void TerminalPage::_SendDesktopNotification(const winrt::hstring& tabTitle, const winrt::hstring& body, const winrt::com_ptr<Tab>& tab)
{
// Build the notification message.
// If a custom body is provided (e.g. from OSC 777), use the title/body directly.
@@ -1233,24 +1252,45 @@ namespace winrt::TerminalApp::implementation
notificationTitle = CascadiaSettings::ApplicationDisplayName();
}
// Use the Tab object's identity hash as a stable toast tag.
// This survives tab reordering and cross-window moves.
const auto tabHash = std::hash<winrt::Windows::Foundation::IUnknown>{}(*tab);
const hstring tabTag{ fmt::format(FMT_COMPILE(L"wt-tab-{:016x}"), tabHash) };
const implementation::DesktopNotificationArgs args{
.Title = notificationTitle,
.Message = message,
.TabIndex = tabIndex,
.Tag = tabTag
};
implementation::DesktopNotification::SendNotification(args, [weakThis{ get_weak() }](uint32_t idx) {
implementation::DesktopNotification::SendNotification(args, [weakThis{ get_weak() }, weakTab{ tab->get_weak() }]() {
if (const auto page{ weakThis.get() })
{
// The toast Activated callback runs on a background thread.
// Marshal both the summon and tab-switch to the UI thread.
page->Dispatcher().RunAsync(winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, [weakPage{ page->get_weak() }, idx]() {
// Marshal to the UI thread for tab focus and window summon.
page->Dispatcher().RunAsync(winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, [weakPage{ page->get_weak() }, weakTab]() {
if (const auto p{ weakPage.get() })
{
p->SummonWindowRequested.raise(nullptr, nullptr);
if (idx < p->_tabs.Size())
if (const auto t{ weakTab.get() })
{
p->_SelectTab(idx);
// Try to find and focus the tab in this window first.
if (const auto tabIndex{ p->_GetTabIndex(*t) })
{
p->SummonWindowRequested.raise(nullptr, nullptr);
p->_SelectTab(tabIndex.value());
}
else
{
// The tab may have moved to another window.
// Raise FocusTabRequested so the emperor can
// search all windows for it.
p->FocusTabRequested.raise(nullptr, *t);
}
}
else
{
// Tab was closed. Just summon this window.
p->SummonWindowRequested.raise(nullptr, nullptr);
}
}
});

View File

@@ -168,6 +168,7 @@ namespace winrt::TerminalApp::implementation
void OpenSettingsUI();
void WindowActivated(const bool activated);
bool FocusTab(const winrt::TerminalApp::Tab& tab);
bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down);
@@ -192,6 +193,7 @@ namespace winrt::TerminalApp::implementation
til::typed_event<IInspectable, IInspectable> IdentifyWindowsRequested;
til::typed_event<IInspectable, winrt::TerminalApp::RenameWindowRequestedArgs> RenameWindowRequested;
til::typed_event<IInspectable, IInspectable> SummonWindowRequested;
til::typed_event<IInspectable, winrt::TerminalApp::Tab> FocusTabRequested;
til::typed_event<IInspectable, winrt::Microsoft::Terminal::Control::WindowSizeChangedEventArgs> WindowSizeChanged;
til::typed_event<IInspectable, IInspectable> OpenSystemMenu;
@@ -570,7 +572,7 @@ namespace winrt::TerminalApp::implementation
void _activePaneChanged(winrt::TerminalApp::Tab tab, Windows::Foundation::IInspectable args);
safe_void_coroutine _doHandleSuggestions(Microsoft::Terminal::Settings::Model::SuggestionsArgs realArgs);
void _SendDesktopNotification(const winrt::hstring& tabTitle, const winrt::hstring& body, uint32_t tabIndex);
void _SendDesktopNotification(const winrt::hstring& tabTitle, const winrt::hstring& body, const winrt::com_ptr<Tab>& tab);
#pragma region ActionHandlers
// These are all defined in AppActionHandlers.cpp

View File

@@ -1205,6 +1205,15 @@ namespace winrt::TerminalApp::implementation
}
}
bool TerminalWindow::FocusTab(const winrt::TerminalApp::Tab& tab)
{
if (_root)
{
return _root->FocusTab(tab);
}
return false;
}
void TerminalWindow::WindowName(const winrt::hstring& name)
{
const auto oldIsQuakeMode = _WindowProperties->IsQuakeWindow();

View File

@@ -92,6 +92,7 @@ namespace winrt::TerminalApp::implementation
bool ShowTabsFullscreen() const;
bool AutoHideWindow();
void IdentifyWindow();
bool FocusTab(const winrt::TerminalApp::Tab& tab);
std::optional<uint32_t> LoadPersistedLayoutIdx() const;
winrt::Microsoft::Terminal::Settings::Model::WindowLayout LoadPersistedLayout();
@@ -221,6 +222,7 @@ namespace winrt::TerminalApp::implementation
FORWARDED_TYPED_EVENT(SetTaskbarProgress, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, _root, SetTaskbarProgress);
FORWARDED_TYPED_EVENT(IdentifyWindowsRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, IdentifyWindowsRequested);
FORWARDED_TYPED_EVENT(SummonWindowRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, SummonWindowRequested);
FORWARDED_TYPED_EVENT(FocusTabRequested, Windows::Foundation::IInspectable, winrt::TerminalApp::Tab, _root, FocusTabRequested);
FORWARDED_TYPED_EVENT(OpenSystemMenu, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, OpenSystemMenu);
FORWARDED_TYPED_EVENT(QuitRequested, Windows::Foundation::IInspectable, Windows::Foundation::IInspectable, _root, QuitRequested);
FORWARDED_TYPED_EVENT(ShowWindowChanged, Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Control::ShowWindowArgs, _root, ShowWindowChanged);

View File

@@ -4,6 +4,7 @@
import "IPaneContent.idl";
import "TerminalPage.idl";
import "ShortcutActionDispatch.idl";
import "Tab.idl";
namespace TerminalApp
{
@@ -74,6 +75,7 @@ namespace TerminalApp
Boolean ShowTabsFullscreen { get; };
void IdentifyWindow();
Boolean FocusTab(TerminalApp.Tab tab);
void SetPersistedLayoutIdx(UInt32 idx);
void RequestExitFullscreen();
@@ -126,6 +128,7 @@ namespace TerminalApp
event Windows.Foundation.TypedEventHandler<Object, Object> IdentifyWindowsRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> IsQuakeWindowChanged;
event Windows.Foundation.TypedEventHandler<Object, Object> SummonWindowRequested;
event Windows.Foundation.TypedEventHandler<Object, TerminalApp.Tab> FocusTabRequested;
event Windows.Foundation.TypedEventHandler<Object, Object> OpenSystemMenu;
event Windows.Foundation.TypedEventHandler<Object, Object> QuitRequested;
event Windows.Foundation.TypedEventHandler<Object, TerminalApp.SystemMenuChangeArgs> SystemMenuChangeRequested;

View File

@@ -265,6 +265,7 @@ void AppHost::Initialize()
_revokers.IsQuakeWindowChanged = _windowLogic.IsQuakeWindowChanged(winrt::auto_revoke, { this, &AppHost::_IsQuakeWindowChanged });
_revokers.SummonWindowRequested = _windowLogic.SummonWindowRequested(winrt::auto_revoke, { this, &AppHost::_SummonWindowRequested });
_revokers.FocusTabRequested = _windowLogic.FocusTabRequested(winrt::auto_revoke, { this, &AppHost::_FocusTabRequested });
_revokers.OpenSystemMenu = _windowLogic.OpenSystemMenu(winrt::auto_revoke, { this, &AppHost::_OpenSystemMenu });
_revokers.QuitRequested = _windowLogic.QuitRequested(winrt::auto_revoke, { this, &AppHost::_RequestQuitAll });
_revokers.ShowWindowChanged = _windowLogic.ShowWindowChanged(winrt::auto_revoke, { this, &AppHost::_ShowWindowChanged });
@@ -1064,6 +1065,14 @@ void AppHost::_SummonWindowRequested(const winrt::Windows::Foundation::IInspecta
HandleSummon(std::move(summonArgs));
}
void AppHost::_FocusTabRequested(const winrt::Windows::Foundation::IInspectable&,
const winrt::TerminalApp::Tab& tab)
{
// The tab may have moved to another window. Ask the emperor to
// search all windows and focus the tab wherever it currently lives.
_windowManager->FocusTabInAnyWindow(tab);
}
void AppHost::_OpenSystemMenu(const winrt::Windows::Foundation::IInspectable&,
const winrt::Windows::Foundation::IInspectable&)
{

View File

@@ -91,6 +91,9 @@ private:
void _SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args);
void _FocusTabRequested(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::TerminalApp::Tab& tab);
void _OpenSystemMenu(const winrt::Windows::Foundation::IInspectable& sender,
const winrt::Windows::Foundation::IInspectable& args);
@@ -151,6 +154,7 @@ private:
winrt::TerminalApp::TerminalWindow::IdentifyWindowsRequested_revoker IdentifyWindowsRequested;
winrt::TerminalApp::TerminalWindow::IsQuakeWindowChanged_revoker IsQuakeWindowChanged;
winrt::TerminalApp::TerminalWindow::SummonWindowRequested_revoker SummonWindowRequested;
winrt::TerminalApp::TerminalWindow::FocusTabRequested_revoker FocusTabRequested;
winrt::TerminalApp::TerminalWindow::OpenSystemMenu_revoker OpenSystemMenu;
winrt::TerminalApp::TerminalWindow::QuitRequested_revoker QuitRequested;
winrt::TerminalApp::TerminalWindow::ShowWindowChanged_revoker ShowWindowChanged;

View File

@@ -751,6 +751,25 @@ bool WindowEmperor::_summonWindow(const SummonWindowSelectionArgs& args) const
return true;
}
void WindowEmperor::FocusTabInAnyWindow(const winrt::TerminalApp::Tab& tab) const
{
_assertIsMainThread();
for (const auto& w : _windows)
{
if (w->Logic().FocusTab(tab))
{
winrt::TerminalApp::SummonWindowBehavior summonArgs;
summonArgs.MoveToCurrentDesktop(false);
summonArgs.DropdownDuration(0);
summonArgs.ToMonitor(winrt::TerminalApp::MonitorBehavior::InPlace);
summonArgs.ToggleVisibility(false);
w->HandleSummon(std::move(summonArgs));
return;
}
}
}
void WindowEmperor::_summonAllWindows() const
{
_assertIsMainThread();
@@ -1009,7 +1028,14 @@ LRESULT WindowEmperor::_messageHandler(HWND window, UINT const message, WPARAM c
{
const auto handoff = deserializeHandoffPayload(static_cast<const uint8_t*>(cds->lpData), static_cast<const uint8_t*>(cds->lpData) + cds->cbData);
const auto argv = commandlineToArgArray(handoff.args.c_str());
_dispatchCommandlineCommon(argv, handoff.cwd, handoff.env, handoff.show);
// When a toast notification is clicked, Windows launches a new
// wt.exe with "__fromToast". That instance hands off here via
// WM_COPYDATA. We already handle activation in-process via the
// toast's Activated event, so just ignore this handoff.
if (argv.size() != 2 || argv[1] != L"__fromToast")
{
_dispatchCommandlineCommon(argv, handoff.cwd, handoff.env, handoff.show);
}
}
return 0;
case WM_HOTKEY:

View File

@@ -35,6 +35,7 @@ public:
AppHost* GetWindowByName(std::wstring_view name) const noexcept;
void CreateNewWindow(winrt::TerminalApp::WindowRequestedArgs args);
void HandleCommandlineArgs(int nCmdShow);
void FocusTabInAnyWindow(const winrt::TerminalApp::Tab& tab) const;
private:
struct SummonWindowSelectionArgs