webbee 0.3.26__tar.gz → 0.3.28__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.28}/CHANGELOG.md +17 -0
  2. {webbee-0.3.26 → webbee-0.3.28}/PKG-INFO +1 -1
  3. {webbee-0.3.26 → webbee-0.3.28}/pyproject.toml +1 -1
  4. webbee-0.3.28/src/webbee/__init__.py +1 -0
  5. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/home_view.py +74 -22
  6. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/repl.py +16 -8
  7. {webbee-0.3.26 → webbee-0.3.28}/tests/test_home.py +4 -4
  8. {webbee-0.3.26 → webbee-0.3.28}/tests/test_home_view.py +36 -0
  9. {webbee-0.3.26 → webbee-0.3.28}/tests/test_repl.py +27 -0
  10. {webbee-0.3.26 → webbee-0.3.28}/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.28}/.github/workflows/publish.yml +0 -0
  13. {webbee-0.3.26 → webbee-0.3.28}/.gitignore +0 -0
  14. {webbee-0.3.26 → webbee-0.3.28}/LICENSE +0 -0
  15. {webbee-0.3.26 → webbee-0.3.28}/README.md +0 -0
  16. {webbee-0.3.26 → webbee-0.3.28}/docs/superpowers/plans/2026-07-21-interactive-home-dashboard.md +0 -0
  17. {webbee-0.3.26 → webbee-0.3.28}/install.sh +0 -0
  18. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/account.py +0 -0
  19. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/active_sessions.py +0 -0
  20. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/banner_art.py +0 -0
  21. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/boot.py +0 -0
  22. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/checkpoints.py +0 -0
  23. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/cli.py +0 -0
  24. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/clipboard.py +0 -0
  25. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/coding_context.py +0 -0
  26. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/commands.py +0 -0
  27. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/config.py +0 -0
  28. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/consent.py +0 -0
  29. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/details.py +0 -0
  30. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/events.py +0 -0
  31. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/frames.py +0 -0
  32. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/home.py +0 -0
  33. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/http.py +0 -0
  34. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/instance_lock.py +0 -0
  35. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/intel/__init__.py +0 -0
  36. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/intel/chunker.py +0 -0
  37. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/intel/embed.py +0 -0
  38. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/intel/graph.py +0 -0
  39. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/intel/indexer.py +0 -0
  40. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/intel/models.py +0 -0
  41. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/intel/query.py +0 -0
  42. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/intel/service.py +0 -0
  43. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/intel/store.py +0 -0
  44. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/intel/vectors.py +0 -0
  45. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/intel/watch.py +0 -0
  46. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/mode_store.py +0 -0
  47. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/newtab_mode.py +0 -0
  48. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/output_pane.py +0 -0
  49. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/queue_panel.py +0 -0
  50. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/reflow.py +0 -0
  51. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/remote.py +0 -0
  52. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/render.py +0 -0
  53. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/repo.py +0 -0
  54. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/selection.py +0 -0
  55. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/session.py +0 -0
  56. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/sessions.py +0 -0
  57. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/sizing.py +0 -0
  58. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/slots.py +0 -0
  59. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/steer.py +0 -0
  60. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/stream.py +0 -0
  61. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/tabs.py +0 -0
  62. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/thread.py +0 -0
  63. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/todo_panel.py +0 -0
  64. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/tokens.py +0 -0
  65. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/tools.py +0 -0
  66. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/tui.py +0 -0
  67. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/update.py +0 -0
  68. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/urlopen.py +0 -0
  69. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/wallet.py +0 -0
  70. {webbee-0.3.26 → webbee-0.3.28}/src/webbee/worktrees.py +0 -0
  71. {webbee-0.3.26 → webbee-0.3.28}/tests/__init__.py +0 -0
  72. {webbee-0.3.26 → webbee-0.3.28}/tests/conftest.py +0 -0
  73. {webbee-0.3.26 → webbee-0.3.28}/tests/test_account.py +0 -0
  74. {webbee-0.3.26 → webbee-0.3.28}/tests/test_active_sessions.py +0 -0
  75. {webbee-0.3.26 → webbee-0.3.28}/tests/test_checkpoints.py +0 -0
  76. {webbee-0.3.26 → webbee-0.3.28}/tests/test_cli.py +0 -0
  77. {webbee-0.3.26 → webbee-0.3.28}/tests/test_clipboard.py +0 -0
  78. {webbee-0.3.26 → webbee-0.3.28}/tests/test_commands.py +0 -0
  79. {webbee-0.3.26 → webbee-0.3.28}/tests/test_config.py +0 -0
  80. {webbee-0.3.26 → webbee-0.3.28}/tests/test_cpc_contract_stable.py +0 -0
  81. {webbee-0.3.26 → webbee-0.3.28}/tests/test_details.py +0 -0
  82. {webbee-0.3.26 → webbee-0.3.28}/tests/test_events.py +0 -0
  83. {webbee-0.3.26 → webbee-0.3.28}/tests/test_freeze_fix.py +0 -0
  84. {webbee-0.3.26 → webbee-0.3.28}/tests/test_instance_lock.py +0 -0
  85. {webbee-0.3.26 → webbee-0.3.28}/tests/test_intel_chunker.py +0 -0
  86. {webbee-0.3.26 → webbee-0.3.28}/tests/test_intel_embed.py +0 -0
  87. {webbee-0.3.26 → webbee-0.3.28}/tests/test_intel_graph.py +0 -0
  88. {webbee-0.3.26 → webbee-0.3.28}/tests/test_intel_indexer.py +0 -0
  89. {webbee-0.3.26 → webbee-0.3.28}/tests/test_intel_no_numpy.py +0 -0
  90. {webbee-0.3.26 → webbee-0.3.28}/tests/test_intel_query.py +0 -0
  91. {webbee-0.3.26 → webbee-0.3.28}/tests/test_intel_service.py +0 -0
  92. {webbee-0.3.26 → webbee-0.3.28}/tests/test_intel_store.py +0 -0
  93. {webbee-0.3.26 → webbee-0.3.28}/tests/test_intel_vectors.py +0 -0
  94. {webbee-0.3.26 → webbee-0.3.28}/tests/test_intel_watch.py +0 -0
  95. {webbee-0.3.26 → webbee-0.3.28}/tests/test_marathon.py +0 -0
  96. {webbee-0.3.26 → webbee-0.3.28}/tests/test_mode_store.py +0 -0
  97. {webbee-0.3.26 → webbee-0.3.28}/tests/test_newtab_mode.py +0 -0
  98. {webbee-0.3.26 → webbee-0.3.28}/tests/test_packaging.py +0 -0
  99. {webbee-0.3.26 → webbee-0.3.28}/tests/test_reflow.py +0 -0
  100. {webbee-0.3.26 → webbee-0.3.28}/tests/test_render.py +0 -0
  101. {webbee-0.3.26 → webbee-0.3.28}/tests/test_repo.py +0 -0
  102. {webbee-0.3.26 → webbee-0.3.28}/tests/test_session.py +0 -0
  103. {webbee-0.3.26 → webbee-0.3.28}/tests/test_sessions.py +0 -0
  104. {webbee-0.3.26 → webbee-0.3.28}/tests/test_sizing.py +0 -0
  105. {webbee-0.3.26 → webbee-0.3.28}/tests/test_slots.py +0 -0
  106. {webbee-0.3.26 → webbee-0.3.28}/tests/test_steer.py +0 -0
  107. {webbee-0.3.26 → webbee-0.3.28}/tests/test_stream.py +0 -0
  108. {webbee-0.3.26 → webbee-0.3.28}/tests/test_tabs.py +0 -0
  109. {webbee-0.3.26 → webbee-0.3.28}/tests/test_thread.py +0 -0
  110. {webbee-0.3.26 → webbee-0.3.28}/tests/test_todo_panel.py +0 -0
  111. {webbee-0.3.26 → webbee-0.3.28}/tests/test_tokens.py +0 -0
  112. {webbee-0.3.26 → webbee-0.3.28}/tests/test_tools.py +0 -0
  113. {webbee-0.3.26 → webbee-0.3.28}/tests/test_tui_hardening.py +0 -0
  114. {webbee-0.3.26 → webbee-0.3.28}/tests/test_update.py +0 -0
  115. {webbee-0.3.26 → webbee-0.3.28}/tests/test_urlopen.py +0 -0
  116. {webbee-0.3.26 → webbee-0.3.28}/tests/test_version.py +0 -0
  117. {webbee-0.3.26 → webbee-0.3.28}/tests/test_wallet.py +0 -0
  118. {webbee-0.3.26 → webbee-0.3.28}/tests/test_worktrees.py +0 -0
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.28
4
+
5
+ The Home screen scrolls now. When Home has more than fills the window — a tall
6
+ stack of tabs, or command output — scroll it with the mouse wheel or
7
+ PageUp/PageDown, like any other view. Output from a command run on Home (for
8
+ example `/help`) shows inline at the bottom and the view jumps to it.
9
+
10
+ ## 0.3.27
11
+
12
+ Home dashboard polish.
13
+
14
+ - The Webbee Code logo is back at the top of Home.
15
+ - Home now shows when your plan renews (or expires).
16
+ - "Top up credits" opens the right billing page.
17
+ - The new-tab mode you pick on Home is now honored no matter how you open
18
+ the tab — typing, the "+" button, and Ctrl+T all agree.
19
+
3
20
  ## 0.3.26
4
21
 
5
22
  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.28
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.28"
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.28"
@@ -293,20 +293,24 @@ class HomeView:
293
293
 
294
294
  def __init__(self, *, slots, actions: "HomeActions",
295
295
  data: "HomeData | None" = None, width: int = 100, out_pane=None):
296
- from prompt_toolkit.layout.containers import HSplit, Window
296
+ from prompt_toolkit.layout.containers import Window
297
297
  from prompt_toolkit.layout.controls import FormattedTextControl
298
298
  self.slots = slots
299
299
  self.actions = actions
300
300
  self.data = data if data is not None else HomeData()
301
- # An OutputPane below the dashboard is Home's command/output region:
302
- # `_say` and slash-command output (e.g. /help, gated-action notes)
303
- # print into its captured console so a command run while Home is
304
- # active still shows an honest answer, and the dock's OutputPane spy +
305
- # ticker (dump/reflow/scroll/flash/edge_tick/forward_mouse) keep
306
- # working. The dock builds it via `tui.OutputPane` and passes it
307
- # (spy-visible, created first = Home); a bare fallback covers direct
308
- # unit use. `self.console` IS that pane's console it drives both the
309
- # command output AND the dashboard's column-width math.
301
+ # Home is ONE ordinary scrollable Window (NOT dont_extend_height, NOT
302
+ # an HSplit): when its content is taller than the viewport, prompt_
303
+ # toolkit's own Window mouse handler scrolls it on the wheel (content
304
+ # fragment handlers return NotImplemented for SCROLL, so it falls
305
+ # through to Window._mouse_handler), and pageup/pagedown route here via
306
+ # `scroll()`. `_say`/slash-command output is rendered INLINE at the
307
+ # bottom of the SAME scrollable content (see `_fragments`), so it
308
+ # scrolls with everything and never gets squeezed off a short terminal.
309
+ # `out_pane` (built via `tui.OutputPane` by the dock, created first =
310
+ # Home for the spy) stays purely as the CAPTURE backend: `_say` prints
311
+ # into its console and `dump()` reads it back; its own window is NOT in
312
+ # the layout. `self.console` IS that pane's console — it drives both the
313
+ # captured command output AND the dashboard's column-width math.
310
314
  if out_pane is None:
311
315
  from webbee.output_pane import OutputPane
312
316
  out_pane = OutputPane(width=width)
@@ -316,37 +320,53 @@ class HomeView:
316
320
  self._hover_id: "str | None" = None
317
321
  self._model: "HomeModel | None" = None
318
322
  self.control = FormattedTextControl(self._fragments, focusable=True, show_cursor=False)
319
- self._dash = Window(content=self.control, wrap_lines=False,
320
- always_hide_cursor=True, dont_extend_height=True)
321
- self.window = HSplit([self._dash, out_pane.window])
323
+ self.window = Window(content=self.control, wrap_lines=False, always_hide_cursor=True)
322
324
 
323
325
  # ---- OutputPane-compatible surface (ticker/layout duck-type) ----------
324
- # All delegate to the composed output pane so the dock's ticker/layout
325
- # drive Home's command-output region exactly as they do a session pane.
326
326
  def reflow(self, new_width: int) -> None:
327
+ # Keep the capture console's width in sync (drives the dashboard's
328
+ # column math) and repaint; the Window re-lays-out at the new size.
327
329
  self._out.reflow(new_width)
330
+ self._invalidate()
328
331
 
329
332
  def edge_tick(self) -> None:
330
- self._out.edge_tick()
333
+ return None
331
334
 
332
335
  def flash(self) -> str:
333
336
  return self._out.flash()
334
337
 
335
338
  def scroll(self, delta: int) -> None:
336
- self._out.scroll(delta)
339
+ # pageup/pagedown (tui routes here). Move the Window's own vertical
340
+ # scroll, clamped to the scrollable range; render_info is present once
341
+ # the window has been drawn at least one frame.
342
+ win = self.window
343
+ info = getattr(win, "render_info", None)
344
+ if info is None:
345
+ return
346
+ max_scroll = max(0, info.content_height - info.window_height)
347
+ win.vertical_scroll = min(max_scroll, max(0, win.vertical_scroll + delta))
348
+ self._invalidate()
337
349
 
338
350
  @property
339
351
  def _view_h(self) -> int:
340
- return self._out._view_h
352
+ info = getattr(self.window, "render_info", None)
353
+ return info.window_height if info is not None else 20
341
354
 
342
355
  def forward_mouse(self, ev, clamp: str = "bottom") -> bool:
343
- return self._out.forward_mouse(ev, clamp)
356
+ return False
344
357
 
345
358
  def dump(self) -> str:
346
359
  return self._out.dump()
347
360
 
348
361
  def notify(self) -> None:
349
- self._out.notify()
362
+ # New captured output landed (a `_say` note / command reply). Reveal
363
+ # it: jump the scroll to the bottom (PT clamps on the next render) so
364
+ # the answer is visible, then repaint.
365
+ try:
366
+ self.window.vertical_scroll = 10 ** 7
367
+ except Exception:
368
+ pass
369
+ self._invalidate()
350
370
 
351
371
  def _invalidate(self) -> None:
352
372
  try:
@@ -445,10 +465,17 @@ class HomeView:
445
465
  nick = getattr(a, "nickname", "") or ""
446
466
  L.append([("class:home.value", f" @{nick}" if nick else " (no nickname)")])
447
467
  plan = getattr(a, "plan", "") or ""
468
+ status = getattr(a, "plan_status", "") or ""
448
469
  if plan:
449
- status = getattr(a, "plan_status", "") or ""
450
470
  tag = f" ({status})" if status and status != "active" else ""
451
471
  L.append([("class:home.dim", f" {plan} plan{tag}")])
472
+ renews = getattr(a, "plan_renews", "") or ""
473
+ if renews:
474
+ # `plan_renews` is the formatted subscription expires_at: an
475
+ # auto-renewing (active/trialing) plan RENEWS then, a
476
+ # cancelled/lapsing one EXPIRES then.
477
+ verb = "renews" if status in ("active", "trialing", "") else "expires"
478
+ L.append([("class:home.dim", f" {verb} {renews}")])
452
479
  email = getattr(a, "email", "") or ""
453
480
  if email:
454
481
  L.append([("class:home.dim", f" {_mask_email(email)}")])
@@ -544,7 +571,21 @@ class HomeView:
544
571
  "· Ctrl+T new tab · Alt+N switch")])
545
572
  return L
546
573
 
547
- lines = [[("class:home.header", " Home ")], [("", "")]]
574
+ # Header: the Webbee Code ASCII logo (bee-yellow), then a centered
575
+ # "◆ Home" — restored (it was on the pre-dashboard Home and Valentin
576
+ # wants it kept). The whole art block is centered by ONE offset (max
577
+ # line width) so its shape is preserved; degrades to left-aligned when
578
+ # the terminal is narrower than the art.
579
+ from webbee.banner_art import WEBBEE_CODE
580
+ art = WEBBEE_CODE.rstrip("\n").split("\n")
581
+ art_w = max((len(ln) for ln in art), default=0)
582
+ art_pad = max(0, (width - art_w) // 2)
583
+ lines = [[("", "")]]
584
+ for ln in art:
585
+ lines.append([("", " " * art_pad), ("class:home.header", ln)] if ln else [("", "")])
586
+ title_pad = max(0, (width - len("◆ Home")) // 2)
587
+ lines.append([("", " " * title_pad), ("class:home.header", "◆ Home")])
588
+ lines.append([("", "")])
548
589
  if two_column(width):
549
590
  colw = max(20, (width // 2) - 3)
550
591
  lines += _side_by_side(you_lines(), wallet_lines(), colw)
@@ -558,6 +599,17 @@ class HomeView:
558
599
  lines += [[("", "")]] + security_lines()
559
600
  lines += footer_lines()
560
601
 
602
+ # `_say` / slash-command output, captured in the backend pane, rendered
603
+ # INLINE at the very bottom of the same scrollable content (ANSI →
604
+ # fragments, styling preserved). `notify()` jumps the scroll here so a
605
+ # command's answer is visible; it scrolls with the rest of Home.
606
+ dump = self._out.dump()
607
+ if dump.strip():
608
+ from prompt_toolkit.formatted_text import ANSI, to_formatted_text
609
+ lines.append([("", "")])
610
+ lines.append([("class:home.header", " Output ")])
611
+ lines.append(list(to_formatted_text(ANSI(dump.rstrip("\n")))))
612
+
561
613
  out = []
562
614
  for ln in lines:
563
615
  out.extend(ln)
@@ -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,39 @@ 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
305
+
306
+
307
+ def test_captured_output_renders_inline_and_notify_reveals_it():
308
+ # Home is one scrollable window; _say/command output is captured in the
309
+ # backend pane and rendered INLINE at the bottom, and notify() jumps the
310
+ # scroll to reveal it.
311
+ hv, _ = _view()
312
+ hv.console.print("hello-inline-output")
313
+ text = "".join(f[1] for f in hv._fragments())
314
+ assert "hello-inline-output" in text
315
+ assert "Output" in text
316
+ hv.notify()
317
+ assert hv.window.vertical_scroll >= 10 ** 6
@@ -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