@@ -277,6 +277,10 @@ def rstudio_eval(page: Page, expr: str, timeout: int = 30_000) -> str:
277277# selecting immediately can miss R and fall back to the (less reliable) Python
278278# console. Bounded so a genuinely R-less deployment settles quickly.
279279_POSITRON_R_GRACE_POLLS = 10
280+ # Polls to let .positron-console re-render after activating the Console tab. A
281+ # console hidden behind the Terminal is removed from the DOM on some Positron
282+ # builds, so we activate + briefly poll before concluding no console exists.
283+ _POSITRON_REACTIVATE_POLLS = 3
280284
281285
282286def ensure_positron_console (page : Page , timeout : int = 45_000 ) -> bool :
@@ -302,8 +306,24 @@ def ensure_positron_console(page: Page, timeout: int = 45_000) -> bool:
302306 Returns:
303307 ``True`` if a console is running, ``False`` otherwise.
304308 """
305- if page .locator (PositronSession .CONSOLE_PANEL ).count () > 0 :
306- return True
309+ # One poll budget is shared across every phase below so the total wait is
310+ # bounded by *timeout* rather than a multiple of it.
311+ remaining = max (1 , timeout // _POSITRON_POLL_MS )
312+
313+ # A console started earlier may be hidden behind the Terminal tab (a prior
314+ # terminal_run selects it), which removes .positron-console from the DOM on
315+ # some Positron builds. Activate the Console tab first and give the panel a
316+ # moment to re-render, so an existing console is detected instead of being
317+ # missed -- once a session exists its "Start New Console Session" button is
318+ # gone, so a missed console would otherwise look like "no console" and fail.
319+ _activate_positron_console (page )
320+ reactivate = min (remaining , _POSITRON_REACTIVATE_POLLS )
321+ while reactivate > 0 :
322+ if page .locator (PositronSession .CONSOLE_PANEL ).count () > 0 :
323+ return True
324+ page .wait_for_timeout (_POSITRON_POLL_MS )
325+ reactivate -= 1
326+ remaining -= 1
307327
308328 start = page .locator (_POSITRON_START_BUTTON )
309329 if start .count () == 0 :
@@ -313,10 +333,6 @@ def ensure_positron_console(page: Page, timeout: int = 45_000) -> bool:
313333 except Exception :
314334 return False
315335
316- # A single poll budget is shared across both phases below so the total wait
317- # is bounded by *timeout* rather than ~2x it.
318- remaining = max (1 , timeout // _POSITRON_POLL_MS )
319-
320336 # Phase 1: poll the interpreter quickpick — discovery lags the click on a
321337 # cold session (~10s live).
322338 while page .locator (_POSITRON_QUICKPICK_ROW ).count () == 0 and remaining > 0 :
0 commit comments