Skip to content

Commit 65961ae

Browse files
committed
Add --session option to focus-tab command
Adds the ability to focus a tab by its WT_SESSION GUID, enabling external processes (like AI coding agents) to programmatically return users to the correct terminal tab. Usage: wt focus-tab --session <guid> or: wt ft -s <guid> Changes: - Added --session/-s option to focus-tab CLI parser - Extended SwitchToTabArgs with SessionId property - Added FocusPaneBySessionId() to Tab class to walk pane tree - Modified _HandleSwitchToTab to lookup by session ID - Auto-routes to existing window (implicit -w 0) for session targeting - Added unit tests for --session parsing Fixes #19783
1 parent 71409f8 commit 65961ae

File tree

10 files changed

+174
-6
lines changed

10 files changed

+174
-6
lines changed

src/cascadia/LocalTests_TerminalApp/CommandlineTest.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,58 @@ namespace TerminalAppLocalTests
11181118
VERIFY_ARE_EQUAL(4u, commandlines.at(0).Argc());
11191119
VERIFY_ARE_EQUAL("wt.exe", commandlines.at(0).Args().at(0));
11201120

1121+
for (auto& cmdBlob : commandlines)
1122+
{
1123+
const auto result = appArgs.ParseCommand(cmdBlob);
1124+
Log::Comment(NoThrowString().Format(
1125+
L"Exit Message:\n%hs",
1126+
appArgs._exitMessage.c_str()));
1127+
VERIFY_ARE_NOT_EQUAL(0, result);
1128+
VERIFY_ARE_NOT_EQUAL("", appArgs._exitMessage);
1129+
}
1130+
}
1131+
{
1132+
Log::Comment(NoThrowString().Format(
1133+
L"Test focus-tab with --session flag"));
1134+
AppCommandlineArgs appArgs{};
1135+
std::vector<const wchar_t*> rawCommands{ L"wt.exe", subcommand, L"-s", L"12345678-1234-1234-1234-123456789abc" };
1136+
_buildCommandlinesHelper(appArgs, 1u, rawCommands);
1137+
1138+
// With --session, there should be NO NewTab prepended (it routes to existing window)
1139+
VERIFY_ARE_EQUAL(1u, appArgs._startupActions.size());
1140+
1141+
auto actionAndArgs = appArgs._startupActions.at(0);
1142+
VERIFY_ARE_EQUAL(ShortcutAction::SwitchToTab, actionAndArgs.Action());
1143+
VERIFY_IS_NOT_NULL(actionAndArgs.Args());
1144+
auto myArgs = actionAndArgs.Args().try_as<SwitchToTabArgs>();
1145+
VERIFY_IS_NOT_NULL(myArgs);
1146+
// Verify SessionId is set (non-empty GUID)
1147+
VERIFY_ARE_NOT_EQUAL(winrt::guid{}, myArgs.SessionId());
1148+
}
1149+
{
1150+
Log::Comment(NoThrowString().Format(
1151+
L"Test focus-tab with --session long form flag"));
1152+
AppCommandlineArgs appArgs{};
1153+
std::vector<const wchar_t*> rawCommands{ L"wt.exe", subcommand, L"--session", L"12345678-1234-1234-1234-123456789abc" };
1154+
_buildCommandlinesHelper(appArgs, 1u, rawCommands);
1155+
1156+
VERIFY_ARE_EQUAL(1u, appArgs._startupActions.size());
1157+
1158+
auto actionAndArgs = appArgs._startupActions.at(0);
1159+
VERIFY_ARE_EQUAL(ShortcutAction::SwitchToTab, actionAndArgs.Action());
1160+
auto myArgs = actionAndArgs.Args().try_as<SwitchToTabArgs>();
1161+
VERIFY_IS_NOT_NULL(myArgs);
1162+
VERIFY_ARE_NOT_EQUAL(winrt::guid{}, myArgs.SessionId());
1163+
}
1164+
{
1165+
Log::Comment(NoThrowString().Format(
1166+
L"Test that --session excludes -t (target index)"));
1167+
AppCommandlineArgs appArgs{};
1168+
std::vector<const wchar_t*> rawCommands{ L"wt.exe", subcommand, L"-s", L"12345678-1234-1234-1234-123456789abc", L"-t", L"2" };
1169+
1170+
auto commandlines = AppCommandlineArgs::BuildCommands(rawCommands);
1171+
VERIFY_ARE_EQUAL(1u, commandlines.size());
1172+
11211173
for (auto& cmdBlob : commandlines)
11221174
{
11231175
const auto result = appArgs.ParseCommand(cmdBlob);

src/cascadia/TerminalApp/AppActionHandlers.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,31 @@ namespace winrt::TerminalApp::implementation
482482
{
483483
if (const auto& realArgs = args.ActionArgs().try_as<SwitchToTabArgs>())
484484
{
485+
// Check if we're targeting by session ID
486+
if (realArgs.SessionId() != winrt::guid{})
487+
{
488+
// Find the tab containing this session
489+
const auto sessionId = realArgs.SessionId();
490+
uint32_t tabIndex = 0;
491+
492+
for (const auto& tab : _tabs)
493+
{
494+
auto tabImpl = winrt::get_self<implementation::Tab>(tab);
495+
if (tabImpl && tabImpl->FocusPaneBySessionId(sessionId))
496+
{
497+
_SelectTab(tabIndex);
498+
args.Handled(true);
499+
return;
500+
}
501+
tabIndex++;
502+
}
503+
504+
// Session not found in this window
505+
args.Handled(false);
506+
return;
507+
}
508+
509+
// Fall back to tab index
485510
_SelectTab({ realArgs.TabIndex() });
486511
args.Handled(true);
487512
}

src/cascadia/TerminalApp/AppCommandlineArgs.cpp

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,9 +372,15 @@ void AppCommandlineArgs::_buildFocusTabParser()
372372
auto* prevOpt = subcommand->add_flag("-p,--previous",
373373
_focusPrevTab,
374374
RS_A(L"CmdFocusTabPrevArgDesc"));
375+
auto* sessionOpt = subcommand->add_option("-s,--session",
376+
_focusTabSession,
377+
RS_A(L"CmdFocusTabSessionArgDesc"));
375378
nextOpt->excludes(prevOpt);
376379
indexOpt->excludes(prevOpt);
377380
indexOpt->excludes(nextOpt);
381+
sessionOpt->excludes(indexOpt);
382+
sessionOpt->excludes(nextOpt);
383+
sessionOpt->excludes(prevOpt);
378384

379385
// When ParseCommand is called, if this subcommand was provided, this
380386
// callback function will be triggered on the same thread. We can be sure
@@ -384,10 +390,40 @@ void AppCommandlineArgs::_buildFocusTabParser()
384390
// Build the action from the values we've parsed on the commandline.
385391
ActionAndArgs focusTabAction{};
386392

387-
if (_focusTabIndex >= 0)
393+
if (!_focusTabSession.empty())
388394
{
395+
// Focus tab by session GUID
389396
focusTabAction.Action(ShortcutAction::SwitchToTab);
390-
SwitchToTabArgs args{ static_cast<unsigned int>(_focusTabIndex) };
397+
SwitchToTabArgs args{};
398+
const auto str = winrt::to_hstring(_focusTabSession);
399+
// GuidFromPlainString handles GUIDs without braces (e.g. "12345678-...")
400+
// GuidFromString handles GUIDs with braces (e.g. "{12345678-...}")
401+
// Try plain first (most common from WT_SESSION), fall back to braced
402+
winrt::guid id{};
403+
try
404+
{
405+
id = ::Microsoft::Console::Utils::GuidFromPlainString(str.c_str());
406+
}
407+
catch (...)
408+
{
409+
id = ::Microsoft::Console::Utils::GuidFromString(str.c_str());
410+
}
411+
args.SessionId(id);
412+
focusTabAction.Args(args);
413+
_startupActions.push_back(focusTabAction);
414+
415+
// When targeting by session ID, route to an existing window on the
416+
// current desktop (if user didn't specify -w explicitly).
417+
// The session ID uniquely identifies a pane in some window.
418+
if (_windowTarget.empty())
419+
{
420+
_windowTarget = "0";
421+
}
422+
}
423+
else if (_focusTabIndex >= 0)
424+
{
425+
focusTabAction.Action(ShortcutAction::SwitchToTab);
426+
SwitchToTabArgs args{ static_cast<unsigned int>(_focusTabIndex), winrt::guid{} };
391427
focusTabAction.Args(args);
392428
_startupActions.push_back(focusTabAction);
393429
}
@@ -808,6 +844,7 @@ void AppCommandlineArgs::_resetStateToDefault()
808844
_focusTabIndex = -1;
809845
_focusNextTab = false;
810846
_focusPrevTab = false;
847+
_focusTabSession.clear();
811848

812849
_moveFocusDirection = FocusDirection::None;
813850
_swapPaneDirection = FocusDirection::None;
@@ -1014,9 +1051,14 @@ void AppCommandlineArgs::ValidateStartupCommands()
10141051
// If we parsed no commands, or the first command we've parsed is not a new
10151052
// tab action, prepend a new-tab command to the front of the list.
10161053
// (also, we don't need to do this if the only action is a x-save)
1054+
// Note: SwitchToTab with a SessionId is excluded because focus-tab --session
1055+
// targets an existing tab and doesn't need a new tab to be created.
1056+
// We check for non-empty _windowTarget as a proxy for --session being used,
1057+
// since --session sets _windowTarget = "0".
10171058
else if (_startupActions.empty() ||
10181059
(_startupActions.front().Action() != ShortcutAction::NewTab &&
1019-
_startupActions.front().Action() != ShortcutAction::SaveSnippet))
1060+
_startupActions.front().Action() != ShortcutAction::SaveSnippet &&
1061+
!(!_windowTarget.empty() && _startupActions.front().Action() == ShortcutAction::SwitchToTab)))
10201062
{
10211063
// Build the NewTab action from the values we've parsed on the commandline.
10221064
NewTerminalArgs newTerminalArgs{};

src/cascadia/TerminalApp/AppCommandlineArgs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class TerminalApp::AppCommandlineArgs final
121121
int _focusTabIndex{ -1 };
122122
bool _focusNextTab{ false };
123123
bool _focusPrevTab{ false };
124+
std::string _focusTabSession; // GUID string for --session targeting
124125

125126
int _focusPaneTarget{ -1 };
126127
std::string _saveInputName;

src/cascadia/TerminalApp/Resources/en-US/Resources.resw

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,9 @@
318318
<data name="CmdFocusTabTargetArgDesc" xml:space="preserve">
319319
<value>Move focus the tab at the given index</value>
320320
</data>
321+
<data name="CmdFocusTabSessionArgDesc" xml:space="preserve">
322+
<value>Move focus to the tab containing the given WT_SESSION GUID</value>
323+
</data>
321324
<data name="CmdMovePaneTabArgDesc" xml:space="preserve">
322325
<value>Move focused pane to the tab at the given index</value>
323326
</data>

src/cascadia/TerminalApp/Tab.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,47 @@ namespace winrt::TerminalApp::implementation
933933
return res;
934934
}
935935

936+
// Method Description:
937+
// - Attempts to find and focus a pane within this tab by its WT_SESSION GUID.
938+
// Arguments:
939+
// - sessionId: The session GUID to search for.
940+
// Return Value:
941+
// - true if the session was found and focused, false otherwise.
942+
bool Tab::FocusPaneBySessionId(const winrt::guid& sessionId)
943+
{
944+
ASSERT_UI_THREAD();
945+
946+
if (_rootPane == nullptr)
947+
{
948+
return false;
949+
}
950+
951+
bool found = false;
952+
_changingActivePane = true;
953+
_rootPane->WalkTree([&](const auto& pane) {
954+
if (const auto content = pane->GetContent())
955+
{
956+
if (const auto termContent = content.try_as<winrt::TerminalApp::TerminalPaneContent>())
957+
{
958+
const auto control = termContent.GetTermControl();
959+
if (control)
960+
{
961+
const auto connection = control.Connection();
962+
if (connection && connection.SessionId() == sessionId)
963+
{
964+
_rootPane->FocusPane(pane);
965+
found = true;
966+
return true; // stop walking
967+
}
968+
}
969+
}
970+
}
971+
return false; // keep walking
972+
});
973+
_changingActivePane = false;
974+
return found;
975+
}
976+
936977
void Tab::Close()
937978
{
938979
ASSERT_UI_THREAD();

src/cascadia/TerminalApp/Tab.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ namespace winrt::TerminalApp::implementation
5757
bool NavigateFocus(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction);
5858
bool SwapPane(const winrt::Microsoft::Terminal::Settings::Model::FocusDirection& direction);
5959
bool FocusPane(const uint32_t id);
60+
bool FocusPaneBySessionId(const winrt::guid& sessionId);
6061

6162
void UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings& settings);
6263
void UpdateTitle();

src/cascadia/TerminalApp/TerminalPage.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2258,7 +2258,7 @@ namespace winrt::TerminalApp::implementation
22582258
{
22592259
ActionAndArgs action;
22602260
action.Action(ShortcutAction::SwitchToTab);
2261-
SwitchToTabArgs switchToTabArgs{ idx.value() };
2261+
SwitchToTabArgs switchToTabArgs{ idx.value(), winrt::guid{} };
22622262
action.Args(switchToTabArgs);
22632263

22642264
actions.emplace_back(std::move(action));

src/cascadia/TerminalSettingsModel/ActionArgs.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ protected: \
118118

119119
////////////////////////////////////////////////////////////////////////////////
120120
#define SWITCH_TO_TAB_ARGS(X) \
121-
X(uint32_t, TabIndex, "index", false, ArgTypeHint::None, 0)
121+
X(uint32_t, TabIndex, "index", false, ArgTypeHint::None, 0) \
122+
X(winrt::guid, SessionId, "sessionId", false, ArgTypeHint::None, winrt::guid{})
122123

123124
////////////////////////////////////////////////////////////////////////////////
124125
#define RESIZE_PANE_ARGS(X) \

src/cascadia/TerminalSettingsModel/ActionArgs.idl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,10 @@ namespace Microsoft.Terminal.Settings.Model
226226

227227
[default_interface] runtimeclass SwitchToTabArgs : IActionArgs, IActionArgsDescriptorAccess
228228
{
229-
SwitchToTabArgs(UInt32 tabIndex);
229+
SwitchToTabArgs();
230+
SwitchToTabArgs(UInt32 tabIndex, Guid sessionId);
230231
UInt32 TabIndex;
232+
Guid SessionId; // WT_SESSION GUID to focus - if set, takes precedence over TabIndex
231233
};
232234

233235
[default_interface] runtimeclass ResizePaneArgs : IActionArgs, IActionArgsDescriptorAccess

0 commit comments

Comments
 (0)