Summary
StepperHostPlugin's constructor (src/web/stepper/src/StepperHostPlugin.tsx:77-78) calls:
tabService.registerTab(tab);
tabService.showTab(tab.id);
ITabService.showTab() doesn't just make a tab visible — per its implementation in frontend (SideContentManager.ts's TabService.showTab), it also dispatches visitSideContent, unconditionally changing the currently-selected tab. Since the plugin's constructor runs as soon as the language/evaluator loads (well before the student has actually used the stepper), this means every language switch to a language with the stepper available steals focus away from whatever tab the student had open (typically the welcome/introduction tab) and jumps straight to the Stepper tab, even though nothing has happened yet.
Where this was found
While migrating a Data Visualizer plugin for py-slang, I copied this exact registerTab+showTab pattern from StepperHostPlugin.tsx (it's the established precedent for a Conductor host plugin's tab registration) and it produced the identical bug: the Data Visualizer tab was auto-selected on every language switch, when the introduction tab should stay selected until the student navigates on their own.
Fix applied for Data Visualizer (same fix needed here)
Rather than patch showTab's behavior (which is intentional and correct for plugins that do want to steal focus the moment they have something to show, per its doc comment — e.g. a sound module the instant it starts play()/record()), I added a new method to ITabService:
/**
* Makes a registered tab visible in the tab bar without changing which tab is currently selected —
* the visible-but-unfocused counterpart to {@link showTab}.
*/
revealTab(id: string): void;
implemented in TabService/SideContentManager.ts (visibility only, no visitSideContent dispatch) and in DeferredConductorTabService (tracking visibility and "last explicitly focused tab" separately, so replay-on-activate() only re-selects a tab that was genuinely showTab'd, not merely revealed).
DataVisualizerHostPlugin.tsx now calls tabService.revealTab(tab.id) instead of showTab. StepperHostPlugin.tsx should get the same one-line change once revealTab is available on ITabService — I didn't make that change here since it's outside the scope of what I was asked to fix, but it's a straight swap of showTab → revealTab at StepperHostPlugin.tsx:78.
Note: revealTab isn't on main yet — it's part of the Data Visualizer PR I'm opening alongside this issue. This issue (and the corresponding stepper fix) is a follow-up, not blocking that PR.
Reproduction
- Load the Playground with any language, confirm the introduction/welcome tab is selected.
- Switch to a language for which the Stepper plugin is available (e.g. any Source chapter with
stepperEvaluatorId set, under the conductor).
- Observe the Stepper tab becomes selected automatically, even though
draw_data/stepping has never been invoked.
Summary
StepperHostPlugin's constructor (src/web/stepper/src/StepperHostPlugin.tsx:77-78) calls:ITabService.showTab()doesn't just make a tab visible — per its implementation infrontend(SideContentManager.ts'sTabService.showTab), it also dispatchesvisitSideContent, unconditionally changing the currently-selected tab. Since the plugin's constructor runs as soon as the language/evaluator loads (well before the student has actually used the stepper), this means every language switch to a language with the stepper available steals focus away from whatever tab the student had open (typically the welcome/introduction tab) and jumps straight to the Stepper tab, even though nothing has happened yet.Where this was found
While migrating a Data Visualizer plugin for py-slang, I copied this exact
registerTab+showTabpattern fromStepperHostPlugin.tsx(it's the established precedent for a Conductor host plugin's tab registration) and it produced the identical bug: the Data Visualizer tab was auto-selected on every language switch, when the introduction tab should stay selected until the student navigates on their own.Fix applied for Data Visualizer (same fix needed here)
Rather than patch
showTab's behavior (which is intentional and correct for plugins that do want to steal focus the moment they have something to show, per its doc comment — e.g. a sound module the instant it startsplay()/record()), I added a new method toITabService:implemented in
TabService/SideContentManager.ts(visibility only, novisitSideContentdispatch) and inDeferredConductorTabService(tracking visibility and "last explicitly focused tab" separately, so replay-on-activate()only re-selects a tab that was genuinelyshowTab'd, not merely revealed).DataVisualizerHostPlugin.tsxnow callstabService.revealTab(tab.id)instead ofshowTab.StepperHostPlugin.tsxshould get the same one-line change oncerevealTabis available onITabService— I didn't make that change here since it's outside the scope of what I was asked to fix, but it's a straight swap ofshowTab→revealTabatStepperHostPlugin.tsx:78.Note:
revealTabisn't onmainyet — it's part of the Data Visualizer PR I'm opening alongside this issue. This issue (and the corresponding stepper fix) is a follow-up, not blocking that PR.Reproduction
stepperEvaluatorIdset, under the conductor).draw_data/stepping has never been invoked.