webbee 0.3.26__tar.gz → 0.3.27__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. {webbee-0.3.26 → webbee-0.3.27}/CHANGELOG.md +10 -0
  2. {webbee-0.3.26 → webbee-0.3.27}/PKG-INFO +1 -1
  3. {webbee-0.3.26 → webbee-0.3.27}/pyproject.toml +1 -1
  4. webbee-0.3.27/src/webbee/__init__.py +1 -0
  5. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/home_view.py +23 -2
  6. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/repl.py +16 -8
  7. {webbee-0.3.26 → webbee-0.3.27}/tests/test_home.py +4 -4
  8. {webbee-0.3.26 → webbee-0.3.27}/tests/test_home_view.py +23 -0
  9. {webbee-0.3.26 → webbee-0.3.27}/tests/test_repl.py +27 -0
  10. {webbee-0.3.26 → webbee-0.3.27}/tests/test_tui.py +1 -1
  11. webbee-0.3.26/src/webbee/__init__.py +0 -1
  12. {webbee-0.3.26 → webbee-0.3.27}/.github/workflows/publish.yml +0 -0
  13. {webbee-0.3.26 → webbee-0.3.27}/.gitignore +0 -0
  14. {webbee-0.3.26 → webbee-0.3.27}/LICENSE +0 -0
  15. {webbee-0.3.26 → webbee-0.3.27}/README.md +0 -0
  16. {webbee-0.3.26 → webbee-0.3.27}/docs/superpowers/plans/2026-07-21-interactive-home-dashboard.md +0 -0
  17. {webbee-0.3.26 → webbee-0.3.27}/install.sh +0 -0
  18. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/account.py +0 -0
  19. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/active_sessions.py +0 -0
  20. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/banner_art.py +0 -0
  21. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/boot.py +0 -0
  22. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/checkpoints.py +0 -0
  23. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/cli.py +0 -0
  24. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/clipboard.py +0 -0
  25. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/coding_context.py +0 -0
  26. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/commands.py +0 -0
  27. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/config.py +0 -0
  28. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/consent.py +0 -0
  29. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/details.py +0 -0
  30. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/events.py +0 -0
  31. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/frames.py +0 -0
  32. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/home.py +0 -0
  33. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/http.py +0 -0
  34. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/instance_lock.py +0 -0
  35. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/intel/__init__.py +0 -0
  36. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/intel/chunker.py +0 -0
  37. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/intel/embed.py +0 -0
  38. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/intel/graph.py +0 -0
  39. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/intel/indexer.py +0 -0
  40. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/intel/models.py +0 -0
  41. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/intel/query.py +0 -0
  42. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/intel/service.py +0 -0
  43. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/intel/store.py +0 -0
  44. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/intel/vectors.py +0 -0
  45. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/intel/watch.py +0 -0
  46. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/mode_store.py +0 -0
  47. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/newtab_mode.py +0 -0
  48. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/output_pane.py +0 -0
  49. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/queue_panel.py +0 -0
  50. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/reflow.py +0 -0
  51. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/remote.py +0 -0
  52. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/render.py +0 -0
  53. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/repo.py +0 -0
  54. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/selection.py +0 -0
  55. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/session.py +0 -0
  56. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/sessions.py +0 -0
  57. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/sizing.py +0 -0
  58. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/slots.py +0 -0
  59. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/steer.py +0 -0
  60. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/stream.py +0 -0
  61. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/tabs.py +0 -0
  62. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/thread.py +0 -0
  63. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/todo_panel.py +0 -0
  64. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/tokens.py +0 -0
  65. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/tools.py +0 -0
  66. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/tui.py +0 -0
  67. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/update.py +0 -0
  68. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/urlopen.py +0 -0
  69. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/wallet.py +0 -0
  70. {webbee-0.3.26 → webbee-0.3.27}/src/webbee/worktrees.py +0 -0
  71. {webbee-0.3.26 → webbee-0.3.27}/tests/__init__.py +0 -0
  72. {webbee-0.3.26 → webbee-0.3.27}/tests/conftest.py +0 -0
  73. {webbee-0.3.26 → webbee-0.3.27}/tests/test_account.py +0 -0
  74. {webbee-0.3.26 → webbee-0.3.27}/tests/test_active_sessions.py +0 -0
  75. {webbee-0.3.26 → webbee-0.3.27}/tests/test_checkpoints.py +0 -0
  76. {webbee-0.3.26 → webbee-0.3.27}/tests/test_cli.py +0 -0
  77. {webbee-0.3.26 → webbee-0.3.27}/tests/test_clipboard.py +0 -0
  78. {webbee-0.3.26 → webbee-0.3.27}/tests/test_commands.py +0 -0
  79. {webbee-0.3.26 → webbee-0.3.27}/tests/test_config.py +0 -0
  80. {webbee-0.3.26 → webbee-0.3.27}/tests/test_cpc_contract_stable.py +0 -0
  81. {webbee-0.3.26 → webbee-0.3.27}/tests/test_details.py +0 -0
  82. {webbee-0.3.26 → webbee-0.3.27}/tests/test_events.py +0 -0
  83. {webbee-0.3.26 → webbee-0.3.27}/tests/test_freeze_fix.py +0 -0
  84. {webbee-0.3.26 → webbee-0.3.27}/tests/test_instance_lock.py +0 -0
  85. {webbee-0.3.26 → webbee-0.3.27}/tests/test_intel_chunker.py +0 -0
  86. {webbee-0.3.26 → webbee-0.3.27}/tests/test_intel_embed.py +0 -0
  87. {webbee-0.3.26 → webbee-0.3.27}/tests/test_intel_graph.py +0 -0
  88. {webbee-0.3.26 → webbee-0.3.27}/tests/test_intel_indexer.py +0 -0
  89. {webbee-0.3.26 → webbee-0.3.27}/tests/test_intel_no_numpy.py +0 -0
  90. {webbee-0.3.26 → webbee-0.3.27}/tests/test_intel_query.py +0 -0
  91. {webbee-0.3.26 → webbee-0.3.27}/tests/test_intel_service.py +0 -0
  92. {webbee-0.3.26 → webbee-0.3.27}/tests/test_intel_store.py +0 -0
  93. {webbee-0.3.26 → webbee-0.3.27}/tests/test_intel_vectors.py +0 -0
  94. {webbee-0.3.26 → webbee-0.3.27}/tests/test_intel_watch.py +0 -0
  95. {webbee-0.3.26 → webbee-0.3.27}/tests/test_marathon.py +0 -0
  96. {webbee-0.3.26 → webbee-0.3.27}/tests/test_mode_store.py +0 -0
  97. {webbee-0.3.26 → webbee-0.3.27}/tests/test_newtab_mode.py +0 -0
  98. {webbee-0.3.26 → webbee-0.3.27}/tests/test_packaging.py +0 -0
  99. {webbee-0.3.26 → webbee-0.3.27}/tests/test_reflow.py +0 -0
  100. {webbee-0.3.26 → webbee-0.3.27}/tests/test_render.py +0 -0
  101. {webbee-0.3.26 → webbee-0.3.27}/tests/test_repo.py +0 -0
  102. {webbee-0.3.26 → webbee-0.3.27}/tests/test_session.py +0 -0
  103. {webbee-0.3.26 → webbee-0.3.27}/tests/test_sessions.py +0 -0
  104. {webbee-0.3.26 → webbee-0.3.27}/tests/test_sizing.py +0 -0
  105. {webbee-0.3.26 → webbee-0.3.27}/tests/test_slots.py +0 -0
  106. {webbee-0.3.26 → webbee-0.3.27}/tests/test_steer.py +0 -0
  107. {webbee-0.3.26 → webbee-0.3.27}/tests/test_stream.py +0 -0
  108. {webbee-0.3.26 → webbee-0.3.27}/tests/test_tabs.py +0 -0
  109. {webbee-0.3.26 → webbee-0.3.27}/tests/test_thread.py +0 -0
  110. {webbee-0.3.26 → webbee-0.3.27}/tests/test_todo_panel.py +0 -0
  111. {webbee-0.3.26 → webbee-0.3.27}/tests/test_tokens.py +0 -0
  112. {webbee-0.3.26 → webbee-0.3.27}/tests/test_tools.py +0 -0
  113. {webbee-0.3.26 → webbee-0.3.27}/tests/test_tui_hardening.py +0 -0
  114. {webbee-0.3.26 → webbee-0.3.27}/tests/test_update.py +0 -0
  115. {webbee-0.3.26 → webbee-0.3.27}/tests/test_urlopen.py +0 -0
  116. {webbee-0.3.26 → webbee-0.3.27}/tests/test_version.py +0 -0
  117. {webbee-0.3.26 → webbee-0.3.27}/tests/test_wallet.py +0 -0
  118. {webbee-0.3.26 → webbee-0.3.27}/tests/test_worktrees.py +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.27
4
+
5
+ Home dashboard polish.
6
+
7
+ - The Webbee Code logo is back at the top of Home.
8
+ - Home now shows when your plan renews (or expires).
9
+ - "Top up credits" opens the right billing page.
10
+ - The new-tab mode you pick on Home is now honored no matter how you open
11
+ the tab — typing, the "+" button, and Ctrl+T all agree.
12
+
3
13
  ## 0.3.26
4
14
 
5
15
  The Home tab is now an interactive dashboard — a little website inside your
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webbee
3
- Version: 0.3.26
3
+ Version: 0.3.27
4
4
  Summary: Webbee 🐝 — the Imperal Cloud coding agent in your terminal
5
5
  Project-URL: Homepage, https://imperal.io
6
6
  Project-URL: Documentation, https://docs.imperal.io
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webbee"
3
- version = "0.3.26"
3
+ version = "0.3.27"
4
4
  description = "Webbee 🐝 — the Imperal Cloud coding agent in your terminal"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1 @@
1
+ __version__ = "0.3.27"
@@ -445,10 +445,17 @@ class HomeView:
445
445
  nick = getattr(a, "nickname", "") or ""
446
446
  L.append([("class:home.value", f" @{nick}" if nick else " (no nickname)")])
447
447
  plan = getattr(a, "plan", "") or ""
448
+ status = getattr(a, "plan_status", "") or ""
448
449
  if plan:
449
- status = getattr(a, "plan_status", "") or ""
450
450
  tag = f" ({status})" if status and status != "active" else ""
451
451
  L.append([("class:home.dim", f" {plan} plan{tag}")])
452
+ renews = getattr(a, "plan_renews", "") or ""
453
+ if renews:
454
+ # `plan_renews` is the formatted subscription expires_at: an
455
+ # auto-renewing (active/trialing) plan RENEWS then, a
456
+ # cancelled/lapsing one EXPIRES then.
457
+ verb = "renews" if status in ("active", "trialing", "") else "expires"
458
+ L.append([("class:home.dim", f" {verb} {renews}")])
452
459
  email = getattr(a, "email", "") or ""
453
460
  if email:
454
461
  L.append([("class:home.dim", f" {_mask_email(email)}")])
@@ -544,7 +551,21 @@ class HomeView:
544
551
  "· Ctrl+T new tab · Alt+N switch")])
545
552
  return L
546
553
 
547
- lines = [[("class:home.header", " Home ")], [("", "")]]
554
+ # Header: the Webbee Code ASCII logo (bee-yellow), then a centered
555
+ # "◆ Home" — restored (it was on the pre-dashboard Home and Valentin
556
+ # wants it kept). The whole art block is centered by ONE offset (max
557
+ # line width) so its shape is preserved; degrades to left-aligned when
558
+ # the terminal is narrower than the art.
559
+ from webbee.banner_art import WEBBEE_CODE
560
+ art = WEBBEE_CODE.rstrip("\n").split("\n")
561
+ art_w = max((len(ln) for ln in art), default=0)
562
+ art_pad = max(0, (width - art_w) // 2)
563
+ lines = [[("", "")]]
564
+ for ln in art:
565
+ lines.append([("", " " * art_pad), ("class:home.header", ln)] if ln else [("", "")])
566
+ title_pad = max(0, (width - len("◆ Home")) // 2)
567
+ lines.append([("", " " * title_pad), ("class:home.header", "◆ Home")])
568
+ lines.append([("", "")])
548
569
  if two_column(width):
549
570
  colw = max(20, (width // 2) - 3)
550
571
  lines += _side_by_side(you_lines(), wallet_lines(), colw)
@@ -465,7 +465,7 @@ async def _isolate_workspace(workspace: str, resources: WorkspaceResources, *,
465
465
  async def _make_session_slot(cfg, token_provider, workspace, mode, *, resources: WorkspaceResources,
466
466
  shared_client, agent_factory, intel_factory, shadow_factory,
467
467
  first: bool, account=None, _with_replayed: bool = False,
468
- slot_id: "str | None" = None):
468
+ slot_id: "str | None" = None, mode_pinned: bool = False):
469
469
  """Builds ONE dock tab's atomic {agent, sink, pane} triple (map §6 —
470
470
  created together, a sink must never point at another slot's pane/
471
471
  console). `shared_client` isn't consumed here yet — reserved for Task 3's
@@ -533,7 +533,12 @@ async def _make_session_slot(cfg, token_provider, workspace, mode, *, resources:
533
533
  return compute_repo_key(find_repo_root(workspace))
534
534
  repo_key_task = asyncio.ensure_future(asyncio.to_thread(_repo_key))
535
535
 
536
- effective_mode = await asyncio.to_thread(load_mode, workspace) or mode
536
+ # `mode_pinned` = the caller's mode is an EXPLICIT user choice (Home's
537
+ # "new-tab mode" setting, threaded through _open_new_tab/_home_input) and
538
+ # MUST win over this repo's remembered mode — otherwise a new tab opened
539
+ # from Home silently ignores the mode you just picked. Only the boot
540
+ # first-tab (mode_pinned=False) resumes the per-repo remembered mode.
541
+ effective_mode = mode if mode_pinned else (await asyncio.to_thread(load_mode, workspace) or mode)
537
542
  width, _height = get_size(None) # pre-app: same fallback tui.run_session's own sizing uses
538
543
  pane = tui.OutputPane(width=width)
539
544
  sink = RichSink(console=pane.console, on_output=pane.notify)
@@ -585,9 +590,11 @@ async def _home_input(text: str, *, slots: SlotManager, cfg, token_provider, mod
585
590
  """Home's Enter path (Task 6, wired into `tui.run_session` as
586
591
  `home_input=`): typing a task on Home opens a session tab in one motion
587
592
  — the SAME `_make_session_slot`/switch path the `/new` command uses
588
- (always `first=False`, always the process's BASELINE `mode`, per the
589
- replay landmine and the autopilot-never-inherited rule both documented
590
- on the `new_tab` action above) then runs the typed text as that NEW
593
+ (always `first=False`; the `mode` passed in is Home's explicit "new-tab
594
+ mode" setting and is PINNED `mode_pinned=True` — so it wins over this
595
+ repo's remembered mode, exactly like the Ctrl+T / + / `/new` path, and a
596
+ new tab honors the mode you picked on Home instead of silently resuming a
597
+ stale per-repo default) — then runs the typed text as that NEW
591
598
  slot's own first turn, explicitly against `new_slot` (FIX1: `run_turn` --
592
599
  repl's `_run_turn` -- is slot-explicit now, never resolves
593
600
  `slots.active()` internally). FIX3: the turn is started through
@@ -616,7 +623,8 @@ async def _home_input(text: str, *, slots: SlotManager, cfg, token_provider, mod
616
623
  new_slot = await _make_session_slot(
617
624
  cfg, token_provider, ws, mode, resources=resources,
618
625
  shared_client=shared_client, agent_factory=agent_factory,
619
- intel_factory=intel_factory, shadow_factory=shadow_factory, first=False)
626
+ intel_factory=intel_factory, shadow_factory=shadow_factory, first=False,
627
+ mode_pinned=True)
620
628
  idx = slots.add(new_slot)
621
629
  ui_hooks.get("switch", slots.switch)(idx)
622
630
  if spawn_poller is not None:
@@ -1194,7 +1202,7 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
1194
1202
  cfg, token_provider, ws, state.get("new_tab_mode") or mode, resources=resources,
1195
1203
  shared_client=shared_client, agent_factory=agent_factory,
1196
1204
  intel_factory=intel_factory, shadow_factory=shadow_factory,
1197
- first=False)
1205
+ first=False, mode_pinned=True)
1198
1206
  idx = slots.add(new_slot)
1199
1207
  ui_hooks.get("switch", slots.switch)(idx)
1200
1208
  _spawn_slot_poller(new_slot)
@@ -1256,7 +1264,7 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
1256
1264
  ui_hooks.get("switch", slots.switch)(idx)
1257
1265
 
1258
1266
  def _home_top_up() -> None:
1259
- url = urlopen.open_url(f"{cfg.panel_url}/billing")
1267
+ url = urlopen.open_url(f"{cfg.panel_url}/ext/billing")
1260
1268
  home_view.data.notice = f"top up at {url}"
1261
1269
  home_view.notify()
1262
1270
 
@@ -216,7 +216,7 @@ def test_home_input_creates_slot_switches_and_runs_first_turn():
216
216
  created = {}
217
217
 
218
218
  async def fake_make_session_slot(cfg, tp, ws, mode, *, resources, shared_client,
219
- agent_factory, intel_factory, shadow_factory, first):
219
+ agent_factory, intel_factory, shadow_factory, first, mode_pinned=False):
220
220
  created["ws"] = ws
221
221
  created["first"] = first
222
222
  slot = _session_slot(workspace=ws, label="new")
@@ -268,7 +268,7 @@ def test_home_input_new_slot_sink_records_echo(monkeypatch):
268
268
  slots.add(_home_slot(workspace="/cwd"))
269
269
 
270
270
  async def fake_make_session_slot(cfg, tp, ws, mode, *, resources, shared_client,
271
- agent_factory, intel_factory, shadow_factory, first):
271
+ agent_factory, intel_factory, shadow_factory, first, mode_pinned=False):
272
272
  s = _session_slot(workspace=ws)
273
273
  s.sink = _EchoSink()
274
274
  return s
@@ -303,7 +303,7 @@ def test_home_input_dock_path_echoes_exactly_once():
303
303
  slots.add(_home_slot(workspace="/cwd"))
304
304
 
305
305
  async def fake_make_session_slot(cfg, tp, ws, mode, *, resources, shared_client,
306
- agent_factory, intel_factory, shadow_factory, first):
306
+ agent_factory, intel_factory, shadow_factory, first, mode_pinned=False):
307
307
  s = _session_slot(workspace=ws)
308
308
  s.sink = _EchoSink()
309
309
  return s
@@ -381,7 +381,7 @@ def test_home_input_uses_new_tab_mode_when_provided():
381
381
  seen = {}
382
382
 
383
383
  async def fake_make_session_slot(cfg, tp, ws, mode, *, resources, shared_client,
384
- agent_factory, intel_factory, shadow_factory, first):
384
+ agent_factory, intel_factory, shadow_factory, first, mode_pinned=False):
385
385
  seen["mode"] = mode
386
386
  s = _session_slot(workspace=ws)
387
387
  class _E:
@@ -279,3 +279,26 @@ def test_outputpane_compat_surface_delegates_to_output_pane():
279
279
  # _say prints into HomeView.console (the output pane) and is dump()-visible
280
280
  hv.console.print("hello-from-say")
281
281
  assert "hello-from-say" in hv.dump()
282
+
283
+
284
+ def test_you_tile_shows_plan_renews_when_active():
285
+ data = HomeData(account=Account(signed_in=True, nickname="v", plan="pro",
286
+ plan_status="active", plan_renews="Aug 3"))
287
+ hv, _ = _view(data=data)
288
+ text = "".join(f[1] for f in hv._fragments())
289
+ assert "renews Aug 3" in text
290
+
291
+
292
+ def test_you_tile_shows_expires_when_not_active():
293
+ data = HomeData(account=Account(signed_in=True, nickname="v", plan="pro",
294
+ plan_status="canceled", plan_renews="Aug 3"))
295
+ hv, _ = _view(data=data)
296
+ text = "".join(f[1] for f in hv._fragments())
297
+ assert "expires Aug 3" in text
298
+
299
+
300
+ def test_home_renders_webbee_code_logo_and_title():
301
+ hv, _ = _view()
302
+ text = "".join(f[1] for f in hv._fragments())
303
+ assert "_____" in text # a slice of the WEBBEE_CODE ascii banner
304
+ assert "◆ Home" in text
@@ -1881,6 +1881,33 @@ def test_make_session_slot_uses_remembered_mode_over_process_baseline(monkeypatc
1881
1881
  assert captured["mode"] == "plan" # the agent itself is built with it too
1882
1882
 
1883
1883
 
1884
+ def test_make_session_slot_mode_pinned_beats_remembered_mode(monkeypatch):
1885
+ # W5 fix: a new tab opened from Home carries the user's EXPLICIT "new-tab
1886
+ # mode" (mode_pinned=True) which must win over this repo's remembered
1887
+ # mode. Before this, load_mode silently overrode the picked mode —
1888
+ # inconsistently by target repo — so Ctrl+T opened with the wrong mode
1889
+ # while typing on Home happened to keep it. This is the shared chokepoint
1890
+ # both _open_new_tab (Ctrl+T / + / /new) and _home_input (typing) use.
1891
+ import webbee.mode_store as MS
1892
+ monkeypatch.setattr(MS, "load_mode", lambda ws: "plan")
1893
+ captured = {}
1894
+
1895
+ def agent_factory(c, tp, ws, m):
1896
+ captured["mode"] = m
1897
+ return FakeAgent()
1898
+
1899
+ async def _drive():
1900
+ return await _make_session_slot(
1901
+ _mk_cfg(), _noop_token_provider, os.getcwd(), "autopilot",
1902
+ resources=WorkspaceResources(), shared_client=None,
1903
+ agent_factory=agent_factory, intel_factory=lambda cfg, ws: _NoopIntel(),
1904
+ shadow_factory=lambda cfg, ws: None, first=False, mode_pinned=True)
1905
+
1906
+ slot = asyncio.run(_drive())
1907
+ assert slot.mode == "autopilot" # pinned explicit choice beats remembered "plan"
1908
+ assert captured["mode"] == "autopilot"
1909
+
1910
+
1884
1911
  def test_make_session_slot_falls_back_to_baseline_when_nothing_remembered(monkeypatch):
1885
1912
  import webbee.mode_store as MS
1886
1913
  monkeypatch.setattr(MS, "load_mode", lambda ws: None)
@@ -3979,7 +3979,7 @@ def test_home_input_first_turn_task_is_visible_and_esc_cancels_it():
3979
3979
  user_echo=lambda t: None)
3980
3980
 
3981
3981
  async def fake_make_session_slot(cfg, tp, ws, mode, *, resources, shared_client,
3982
- agent_factory, intel_factory, shadow_factory, first):
3982
+ agent_factory, intel_factory, shadow_factory, first, mode_pinned=False):
3983
3983
  return SessionSlot(kind="session", workspace=ws, label="new",
3984
3984
  pane=tui.OutputPane(width=80), sink=new_sink, agent=None)
3985
3985
 
@@ -1 +0,0 @@
1
- __version__ = "0.3.26"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes