webbee 0.3.28__tar.gz → 0.3.29__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.28 → webbee-0.3.29}/CHANGELOG.md +7 -0
  2. {webbee-0.3.28 → webbee-0.3.29}/PKG-INFO +1 -1
  3. {webbee-0.3.28 → webbee-0.3.29}/pyproject.toml +1 -1
  4. webbee-0.3.29/src/webbee/__init__.py +1 -0
  5. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/home_view.py +67 -38
  6. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/render.py +13 -0
  7. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/repl.py +5 -0
  8. {webbee-0.3.28 → webbee-0.3.29}/tests/test_home_view.py +24 -3
  9. webbee-0.3.28/src/webbee/__init__.py +0 -1
  10. {webbee-0.3.28 → webbee-0.3.29}/.github/workflows/publish.yml +0 -0
  11. {webbee-0.3.28 → webbee-0.3.29}/.gitignore +0 -0
  12. {webbee-0.3.28 → webbee-0.3.29}/LICENSE +0 -0
  13. {webbee-0.3.28 → webbee-0.3.29}/README.md +0 -0
  14. {webbee-0.3.28 → webbee-0.3.29}/docs/superpowers/plans/2026-07-21-interactive-home-dashboard.md +0 -0
  15. {webbee-0.3.28 → webbee-0.3.29}/install.sh +0 -0
  16. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/account.py +0 -0
  17. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/active_sessions.py +0 -0
  18. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/banner_art.py +0 -0
  19. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/boot.py +0 -0
  20. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/checkpoints.py +0 -0
  21. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/cli.py +0 -0
  22. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/clipboard.py +0 -0
  23. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/coding_context.py +0 -0
  24. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/commands.py +0 -0
  25. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/config.py +0 -0
  26. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/consent.py +0 -0
  27. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/details.py +0 -0
  28. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/events.py +0 -0
  29. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/frames.py +0 -0
  30. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/home.py +0 -0
  31. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/http.py +0 -0
  32. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/instance_lock.py +0 -0
  33. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/__init__.py +0 -0
  34. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/chunker.py +0 -0
  35. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/embed.py +0 -0
  36. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/graph.py +0 -0
  37. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/indexer.py +0 -0
  38. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/models.py +0 -0
  39. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/query.py +0 -0
  40. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/service.py +0 -0
  41. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/store.py +0 -0
  42. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/vectors.py +0 -0
  43. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/watch.py +0 -0
  44. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/mode_store.py +0 -0
  45. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/newtab_mode.py +0 -0
  46. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/output_pane.py +0 -0
  47. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/queue_panel.py +0 -0
  48. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/reflow.py +0 -0
  49. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/remote.py +0 -0
  50. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/repo.py +0 -0
  51. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/selection.py +0 -0
  52. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/session.py +0 -0
  53. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/sessions.py +0 -0
  54. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/sizing.py +0 -0
  55. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/slots.py +0 -0
  56. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/steer.py +0 -0
  57. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/stream.py +0 -0
  58. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/tabs.py +0 -0
  59. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/thread.py +0 -0
  60. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/todo_panel.py +0 -0
  61. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/tokens.py +0 -0
  62. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/tools.py +0 -0
  63. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/tui.py +0 -0
  64. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/update.py +0 -0
  65. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/urlopen.py +0 -0
  66. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/wallet.py +0 -0
  67. {webbee-0.3.28 → webbee-0.3.29}/src/webbee/worktrees.py +0 -0
  68. {webbee-0.3.28 → webbee-0.3.29}/tests/__init__.py +0 -0
  69. {webbee-0.3.28 → webbee-0.3.29}/tests/conftest.py +0 -0
  70. {webbee-0.3.28 → webbee-0.3.29}/tests/test_account.py +0 -0
  71. {webbee-0.3.28 → webbee-0.3.29}/tests/test_active_sessions.py +0 -0
  72. {webbee-0.3.28 → webbee-0.3.29}/tests/test_checkpoints.py +0 -0
  73. {webbee-0.3.28 → webbee-0.3.29}/tests/test_cli.py +0 -0
  74. {webbee-0.3.28 → webbee-0.3.29}/tests/test_clipboard.py +0 -0
  75. {webbee-0.3.28 → webbee-0.3.29}/tests/test_commands.py +0 -0
  76. {webbee-0.3.28 → webbee-0.3.29}/tests/test_config.py +0 -0
  77. {webbee-0.3.28 → webbee-0.3.29}/tests/test_cpc_contract_stable.py +0 -0
  78. {webbee-0.3.28 → webbee-0.3.29}/tests/test_details.py +0 -0
  79. {webbee-0.3.28 → webbee-0.3.29}/tests/test_events.py +0 -0
  80. {webbee-0.3.28 → webbee-0.3.29}/tests/test_freeze_fix.py +0 -0
  81. {webbee-0.3.28 → webbee-0.3.29}/tests/test_home.py +0 -0
  82. {webbee-0.3.28 → webbee-0.3.29}/tests/test_instance_lock.py +0 -0
  83. {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_chunker.py +0 -0
  84. {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_embed.py +0 -0
  85. {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_graph.py +0 -0
  86. {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_indexer.py +0 -0
  87. {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_no_numpy.py +0 -0
  88. {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_query.py +0 -0
  89. {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_service.py +0 -0
  90. {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_store.py +0 -0
  91. {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_vectors.py +0 -0
  92. {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_watch.py +0 -0
  93. {webbee-0.3.28 → webbee-0.3.29}/tests/test_marathon.py +0 -0
  94. {webbee-0.3.28 → webbee-0.3.29}/tests/test_mode_store.py +0 -0
  95. {webbee-0.3.28 → webbee-0.3.29}/tests/test_newtab_mode.py +0 -0
  96. {webbee-0.3.28 → webbee-0.3.29}/tests/test_packaging.py +0 -0
  97. {webbee-0.3.28 → webbee-0.3.29}/tests/test_reflow.py +0 -0
  98. {webbee-0.3.28 → webbee-0.3.29}/tests/test_render.py +0 -0
  99. {webbee-0.3.28 → webbee-0.3.29}/tests/test_repl.py +0 -0
  100. {webbee-0.3.28 → webbee-0.3.29}/tests/test_repo.py +0 -0
  101. {webbee-0.3.28 → webbee-0.3.29}/tests/test_session.py +0 -0
  102. {webbee-0.3.28 → webbee-0.3.29}/tests/test_sessions.py +0 -0
  103. {webbee-0.3.28 → webbee-0.3.29}/tests/test_sizing.py +0 -0
  104. {webbee-0.3.28 → webbee-0.3.29}/tests/test_slots.py +0 -0
  105. {webbee-0.3.28 → webbee-0.3.29}/tests/test_steer.py +0 -0
  106. {webbee-0.3.28 → webbee-0.3.29}/tests/test_stream.py +0 -0
  107. {webbee-0.3.28 → webbee-0.3.29}/tests/test_tabs.py +0 -0
  108. {webbee-0.3.28 → webbee-0.3.29}/tests/test_thread.py +0 -0
  109. {webbee-0.3.28 → webbee-0.3.29}/tests/test_todo_panel.py +0 -0
  110. {webbee-0.3.28 → webbee-0.3.29}/tests/test_tokens.py +0 -0
  111. {webbee-0.3.28 → webbee-0.3.29}/tests/test_tools.py +0 -0
  112. {webbee-0.3.28 → webbee-0.3.29}/tests/test_tui.py +0 -0
  113. {webbee-0.3.28 → webbee-0.3.29}/tests/test_tui_hardening.py +0 -0
  114. {webbee-0.3.28 → webbee-0.3.29}/tests/test_update.py +0 -0
  115. {webbee-0.3.28 → webbee-0.3.29}/tests/test_urlopen.py +0 -0
  116. {webbee-0.3.28 → webbee-0.3.29}/tests/test_version.py +0 -0
  117. {webbee-0.3.28 → webbee-0.3.29}/tests/test_wallet.py +0 -0
  118. {webbee-0.3.28 → webbee-0.3.29}/tests/test_worktrees.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.29
4
+
5
+ - The Home screen scrolls for real now — mouse wheel and PageUp/PageDown move
6
+ through the whole page, however tall it gets.
7
+ - The Webbee Code logo now opens every new tab you start, and Home itself is
8
+ kept clean (the logo is no longer on Home).
9
+
3
10
  ## 0.3.28
4
11
 
5
12
  The Home screen scrolls now. When Home has more than fills the window — a tall
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webbee
3
- Version: 0.3.28
3
+ Version: 0.3.29
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.28"
3
+ version = "0.3.29"
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.29"
@@ -259,6 +259,16 @@ def _line_len(frags) -> int:
259
259
  return sum(len(f[1]) for f in frags)
260
260
 
261
261
 
262
+ def _flatten_lines(lines):
263
+ """Flatten a list of fragment-lines into one fragment list with a newline
264
+ fragment terminating each line (what a FormattedTextControl consumes)."""
265
+ out = []
266
+ for ln in lines:
267
+ out.extend(ln)
268
+ out.append(("", "\n"))
269
+ return out
270
+
271
+
262
272
  def _pad_line(frags, width: int):
263
273
  n = _line_len(frags)
264
274
  return list(frags) + ([("", " " * (width - n))] if n < width else [])
@@ -295,6 +305,7 @@ class HomeView:
295
305
  data: "HomeData | None" = None, width: int = 100, out_pane=None):
296
306
  from prompt_toolkit.layout.containers import Window
297
307
  from prompt_toolkit.layout.controls import FormattedTextControl
308
+ from prompt_toolkit.mouse_events import MouseEventType
298
309
  self.slots = slots
299
310
  self.actions = actions
300
311
  self.data = data if data is not None else HomeData()
@@ -319,7 +330,29 @@ class HomeView:
319
330
  self._focus_id: "str | None" = None
320
331
  self._hover_id: "str | None" = None
321
332
  self._model: "HomeModel | None" = None
322
- self.control = FormattedTextControl(self._fragments, focusable=True, show_cursor=False)
333
+ # Virtualized scroll (mirrors OutputPane): the control is fed ONLY the
334
+ # visible slice of lines, so it always fits the viewport and prompt_
335
+ # toolkit never fights an (absent) cursor to reset the scroll — the
336
+ # reason a plain FormattedTextControl window won't scroll. `_scroll` is
337
+ # the top visible line; `_view_h_val` is the last render's viewport
338
+ # height. Wheel adjusts `_scroll` here; PageUp/PageDown route via
339
+ # `scroll()`; `notify()` jumps to the bottom to reveal new output.
340
+ self._scroll = 0
341
+ self._view_h_val = 20
342
+ view = self
343
+
344
+ class _HomeControl(FormattedTextControl):
345
+ def mouse_handler(self, ev):
346
+ et = ev.event_type
347
+ if et == MouseEventType.SCROLL_DOWN:
348
+ view.scroll(3)
349
+ return None
350
+ if et == MouseEventType.SCROLL_UP:
351
+ view.scroll(-3)
352
+ return None
353
+ return super().mouse_handler(ev)
354
+
355
+ self.control = _HomeControl(self._visible_fragments, focusable=True, show_cursor=False)
323
356
  self.window = Window(content=self.control, wrap_lines=False, always_hide_cursor=True)
324
357
 
325
358
  # ---- OutputPane-compatible surface (ticker/layout duck-type) ----------
@@ -336,21 +369,16 @@ class HomeView:
336
369
  return self._out.flash()
337
370
 
338
371
  def scroll(self, delta: int) -> None:
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))
372
+ # Wheel (via the control) and PageUp/PageDown (tui routes here) move the
373
+ # top visible line; clamped so you can't scroll past the ends.
374
+ lines = self._all_lines()
375
+ max_off = max(0, len(lines) - max(1, self._view_h_val))
376
+ self._scroll = max(0, min(self._scroll + delta, max_off))
348
377
  self._invalidate()
349
378
 
350
379
  @property
351
380
  def _view_h(self) -> int:
352
- info = getattr(self.window, "render_info", None)
353
- return info.window_height if info is not None else 20
381
+ return self._view_h_val
354
382
 
355
383
  def forward_mouse(self, ev, clamp: str = "bottom") -> bool:
356
384
  return False
@@ -360,12 +388,9 @@ class HomeView:
360
388
 
361
389
  def notify(self) -> None:
362
390
  # 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
391
+ # it: jump to the bottom (clamped on the next render) so the answer is
392
+ # visible, then repaint.
393
+ self._scroll = 10 ** 7
369
394
  self._invalidate()
370
395
 
371
396
  def _invalidate(self) -> None:
@@ -434,7 +459,7 @@ class HomeView:
434
459
  return _h
435
460
 
436
461
  # ---- render -----------------------------------------------------------
437
- def _fragments(self):
462
+ def _all_lines(self):
438
463
  from webbee.home import _mask_email
439
464
  from webbee.render import _fmt_tokens
440
465
  m = self._build_model()
@@ -571,21 +596,10 @@ class HomeView:
571
596
  "· Ctrl+T new tab · Alt+N switch")])
572
597
  return L
573
598
 
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 [("", "")])
599
+ # A small centered "◆ Home" title. The Webbee Code logo now opens each
600
+ # NEW session tab instead (RichSink.banner), so Home itself stays clean.
586
601
  title_pad = max(0, (width - len("◆ Home")) // 2)
587
- lines.append([("", " " * title_pad), ("class:home.header", "◆ Home")])
588
- lines.append([("", "")])
602
+ lines = [[("", "")], [("", " " * title_pad), ("class:home.header", "◆ Home")], [("", "")]]
589
603
  if two_column(width):
590
604
  colw = max(20, (width // 2) - 3)
591
605
  lines += _side_by_side(you_lines(), wallet_lines(), colw)
@@ -609,9 +623,24 @@ class HomeView:
609
623
  lines.append([("", "")])
610
624
  lines.append([("class:home.header", " Output ")])
611
625
  lines.append(list(to_formatted_text(ANSI(dump.rstrip("\n")))))
626
+ return lines
612
627
 
613
- out = []
614
- for ln in lines:
615
- out.extend(ln)
616
- out.append(("", "\n"))
617
- return out
628
+ def _fragments(self):
629
+ """The FULL dashboard flattened (every line). The build helpers and
630
+ tests use this; the on-screen control uses `_visible_fragments`."""
631
+ return _flatten_lines(self._all_lines())
632
+
633
+ def _visible_fragments(self):
634
+ """Only the visible slice `[_scroll : _scroll+view_h]`, flattened — what
635
+ the control renders. Captures the live viewport height from the last
636
+ render (`render_info.window_height`) and clamps `_scroll` into range, so
637
+ the slice always fits and the window never needs (and never fights) a
638
+ cursor to scroll."""
639
+ ri = getattr(self.window, "render_info", None)
640
+ if ri is not None and getattr(ri, "window_height", 0):
641
+ self._view_h_val = ri.window_height
642
+ lines = self._all_lines()
643
+ h = max(1, self._view_h_val)
644
+ off = max(0, min(self._scroll, max(0, len(lines) - h)))
645
+ self._scroll = off
646
+ return _flatten_lines(lines[off:off + h])
@@ -214,6 +214,19 @@ class RichSink:
214
214
  self.console.print()
215
215
  self._nudge()
216
216
 
217
+ def banner(self) -> None:
218
+ """The Webbee Code brand logo, printed at the top of every NEW session
219
+ tab (0.3.29) so each tab opens with the logo the launch splash has
220
+ always carried — just the brand block, no identity/privacy (those stay
221
+ on the one-time boot `welcome`). Align.center = reflow-safe."""
222
+ self.console.print()
223
+ self.console.print(Align.center(Text(WEBBEE_CODE, style=f"bold {_BEE}")))
224
+ self.console.print(Align.center(Text("🐝", style=f"bold {_BEE}")))
225
+ self.console.print(Align.center(Text("ICNLI AI Cloud OS · Agent", style=f"bold {_ACCENT}")))
226
+ self.console.print(Align.center(Text("· i m p e r a l . i o ·", style="dim")))
227
+ self.console.print()
228
+ self._nudge()
229
+
217
230
  # ---- turn lifecycle -------------------------------------------------
218
231
  def begin_turn(self) -> None:
219
232
  self._tools = 0
@@ -419,6 +419,11 @@ async def _finish_slot(cfg, token_provider, workspace, mode, *, resources: Works
419
419
  # one kernel-owned row. Reference share — never a copy.
420
420
  sink.local_pending = slot.pending
421
421
  replayed = 0
422
+ if not first:
423
+ # Every NEW tab opens with the Webbee Code logo (0.3.29) — the brand
424
+ # block the launch splash carries — since the boot `welcome` (below)
425
+ # only runs for the first tab.
426
+ sink.banner()
422
427
  if first:
423
428
  if account is not None:
424
429
  sink.welcome(account, workspace, "terminal")
@@ -297,11 +297,30 @@ def test_you_tile_shows_expires_when_not_active():
297
297
  assert "expires Aug 3" in text
298
298
 
299
299
 
300
- def test_home_renders_webbee_code_logo_and_title():
300
+ def test_home_has_title_but_no_logo():
301
+ # 0.3.29: the Webbee Code logo moved to each NEW session tab; Home keeps
302
+ # only a small "◆ Home" title.
301
303
  hv, _ = _view()
302
304
  text = "".join(f[1] for f in hv._fragments())
303
- assert "_____" in text # a slice of the WEBBEE_CODE ascii banner
304
305
  assert "◆ Home" in text
306
+ assert "_____" not in text # the ascii logo is no longer on Home
307
+
308
+
309
+ def test_home_virtualized_scroll_slices_and_clamps():
310
+ # Home virtualizes like OutputPane: the control renders only a slice ≤ the
311
+ # viewport height, and scroll() moves/clamps the top line.
312
+ hv, _ = _view()
313
+ hv._view_h_val = 5
314
+ total = len(hv._all_lines())
315
+ assert total > 5 # taller than a 5-row viewport
316
+ visible_newlines = sum(1 for f in hv._visible_fragments() if f[1] == "\n")
317
+ assert visible_newlines <= 5
318
+ hv.scroll(3)
319
+ assert hv._scroll == 3
320
+ hv.scroll(10 ** 6)
321
+ assert hv._scroll == total - 5 # clamped to the bottom
322
+ hv.scroll(-10 ** 6)
323
+ assert hv._scroll == 0 # clamped to the top
305
324
 
306
325
 
307
326
  def test_captured_output_renders_inline_and_notify_reveals_it():
@@ -314,4 +333,6 @@ def test_captured_output_renders_inline_and_notify_reveals_it():
314
333
  assert "hello-inline-output" in text
315
334
  assert "Output" in text
316
335
  hv.notify()
317
- assert hv.window.vertical_scroll >= 10 ** 6
336
+ hv._visible_fragments() # clamps _scroll into range
337
+ total = len(hv._all_lines())
338
+ assert hv._scroll == max(0, total - hv._view_h_val) # notify jumped to the bottom
@@ -1 +0,0 @@
1
- __version__ = "0.3.28"
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
File without changes
File without changes