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.
- {webbee-0.3.28 → webbee-0.3.29}/CHANGELOG.md +7 -0
- {webbee-0.3.28 → webbee-0.3.29}/PKG-INFO +1 -1
- {webbee-0.3.28 → webbee-0.3.29}/pyproject.toml +1 -1
- webbee-0.3.29/src/webbee/__init__.py +1 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/home_view.py +67 -38
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/render.py +13 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/repl.py +5 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_home_view.py +24 -3
- webbee-0.3.28/src/webbee/__init__.py +0 -1
- {webbee-0.3.28 → webbee-0.3.29}/.github/workflows/publish.yml +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/.gitignore +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/LICENSE +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/README.md +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/docs/superpowers/plans/2026-07-21-interactive-home-dashboard.md +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/install.sh +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/account.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/active_sessions.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/banner_art.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/boot.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/checkpoints.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/cli.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/clipboard.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/coding_context.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/commands.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/config.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/consent.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/details.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/events.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/frames.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/home.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/http.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/instance_lock.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/chunker.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/embed.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/graph.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/indexer.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/models.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/query.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/service.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/store.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/intel/watch.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/mode_store.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/newtab_mode.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/output_pane.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/queue_panel.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/reflow.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/remote.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/repo.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/selection.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/session.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/sessions.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/sizing.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/slots.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/steer.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/stream.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/tabs.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/thread.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/todo_panel.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/tokens.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/tools.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/tui.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/update.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/urlopen.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/wallet.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/src/webbee/worktrees.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/__init__.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/conftest.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_account.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_active_sessions.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_checkpoints.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_cli.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_clipboard.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_commands.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_config.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_details.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_events.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_freeze_fix.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_home.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_instance_lock.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_chunker.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_embed.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_graph.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_indexer.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_query.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_service.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_store.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_vectors.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_intel_watch.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_marathon.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_mode_store.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_newtab_mode.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_packaging.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_reflow.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_render.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_repl.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_repo.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_session.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_sessions.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_sizing.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_slots.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_steer.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_stream.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_tabs.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_thread.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_todo_panel.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_tokens.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_tools.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_tui.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_tui_hardening.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_update.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_urlopen.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_version.py +0 -0
- {webbee-0.3.28 → webbee-0.3.29}/tests/test_wallet.py +0 -0
- {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
|
|
@@ -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
|
-
|
|
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
|
-
#
|
|
340
|
-
#
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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
|
|
364
|
-
#
|
|
365
|
-
|
|
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
|
|
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
|
-
#
|
|
575
|
-
#
|
|
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
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
|
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
|
-
|
|
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
|
{webbee-0.3.28 → webbee-0.3.29}/docs/superpowers/plans/2026-07-21-interactive-home-dashboard.md
RENAMED
|
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
|
|
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
|