webbee 0.3.27__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.27 → webbee-0.3.29}/CHANGELOG.md +14 -0
  2. {webbee-0.3.27 → webbee-0.3.29}/PKG-INFO +1 -1
  3. {webbee-0.3.27 → webbee-0.3.29}/pyproject.toml +1 -1
  4. webbee-0.3.29/src/webbee/__init__.py +1 -0
  5. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/home_view.py +101 -41
  6. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/render.py +13 -0
  7. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/repl.py +5 -0
  8. {webbee-0.3.27 → webbee-0.3.29}/tests/test_home_view.py +36 -2
  9. webbee-0.3.27/src/webbee/__init__.py +0 -1
  10. {webbee-0.3.27 → webbee-0.3.29}/.github/workflows/publish.yml +0 -0
  11. {webbee-0.3.27 → webbee-0.3.29}/.gitignore +0 -0
  12. {webbee-0.3.27 → webbee-0.3.29}/LICENSE +0 -0
  13. {webbee-0.3.27 → webbee-0.3.29}/README.md +0 -0
  14. {webbee-0.3.27 → webbee-0.3.29}/docs/superpowers/plans/2026-07-21-interactive-home-dashboard.md +0 -0
  15. {webbee-0.3.27 → webbee-0.3.29}/install.sh +0 -0
  16. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/account.py +0 -0
  17. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/active_sessions.py +0 -0
  18. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/banner_art.py +0 -0
  19. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/boot.py +0 -0
  20. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/checkpoints.py +0 -0
  21. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/cli.py +0 -0
  22. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/clipboard.py +0 -0
  23. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/coding_context.py +0 -0
  24. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/commands.py +0 -0
  25. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/config.py +0 -0
  26. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/consent.py +0 -0
  27. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/details.py +0 -0
  28. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/events.py +0 -0
  29. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/frames.py +0 -0
  30. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/home.py +0 -0
  31. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/http.py +0 -0
  32. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/instance_lock.py +0 -0
  33. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/intel/__init__.py +0 -0
  34. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/intel/chunker.py +0 -0
  35. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/intel/embed.py +0 -0
  36. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/intel/graph.py +0 -0
  37. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/intel/indexer.py +0 -0
  38. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/intel/models.py +0 -0
  39. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/intel/query.py +0 -0
  40. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/intel/service.py +0 -0
  41. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/intel/store.py +0 -0
  42. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/intel/vectors.py +0 -0
  43. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/intel/watch.py +0 -0
  44. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/mode_store.py +0 -0
  45. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/newtab_mode.py +0 -0
  46. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/output_pane.py +0 -0
  47. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/queue_panel.py +0 -0
  48. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/reflow.py +0 -0
  49. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/remote.py +0 -0
  50. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/repo.py +0 -0
  51. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/selection.py +0 -0
  52. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/session.py +0 -0
  53. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/sessions.py +0 -0
  54. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/sizing.py +0 -0
  55. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/slots.py +0 -0
  56. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/steer.py +0 -0
  57. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/stream.py +0 -0
  58. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/tabs.py +0 -0
  59. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/thread.py +0 -0
  60. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/todo_panel.py +0 -0
  61. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/tokens.py +0 -0
  62. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/tools.py +0 -0
  63. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/tui.py +0 -0
  64. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/update.py +0 -0
  65. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/urlopen.py +0 -0
  66. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/wallet.py +0 -0
  67. {webbee-0.3.27 → webbee-0.3.29}/src/webbee/worktrees.py +0 -0
  68. {webbee-0.3.27 → webbee-0.3.29}/tests/__init__.py +0 -0
  69. {webbee-0.3.27 → webbee-0.3.29}/tests/conftest.py +0 -0
  70. {webbee-0.3.27 → webbee-0.3.29}/tests/test_account.py +0 -0
  71. {webbee-0.3.27 → webbee-0.3.29}/tests/test_active_sessions.py +0 -0
  72. {webbee-0.3.27 → webbee-0.3.29}/tests/test_checkpoints.py +0 -0
  73. {webbee-0.3.27 → webbee-0.3.29}/tests/test_cli.py +0 -0
  74. {webbee-0.3.27 → webbee-0.3.29}/tests/test_clipboard.py +0 -0
  75. {webbee-0.3.27 → webbee-0.3.29}/tests/test_commands.py +0 -0
  76. {webbee-0.3.27 → webbee-0.3.29}/tests/test_config.py +0 -0
  77. {webbee-0.3.27 → webbee-0.3.29}/tests/test_cpc_contract_stable.py +0 -0
  78. {webbee-0.3.27 → webbee-0.3.29}/tests/test_details.py +0 -0
  79. {webbee-0.3.27 → webbee-0.3.29}/tests/test_events.py +0 -0
  80. {webbee-0.3.27 → webbee-0.3.29}/tests/test_freeze_fix.py +0 -0
  81. {webbee-0.3.27 → webbee-0.3.29}/tests/test_home.py +0 -0
  82. {webbee-0.3.27 → webbee-0.3.29}/tests/test_instance_lock.py +0 -0
  83. {webbee-0.3.27 → webbee-0.3.29}/tests/test_intel_chunker.py +0 -0
  84. {webbee-0.3.27 → webbee-0.3.29}/tests/test_intel_embed.py +0 -0
  85. {webbee-0.3.27 → webbee-0.3.29}/tests/test_intel_graph.py +0 -0
  86. {webbee-0.3.27 → webbee-0.3.29}/tests/test_intel_indexer.py +0 -0
  87. {webbee-0.3.27 → webbee-0.3.29}/tests/test_intel_no_numpy.py +0 -0
  88. {webbee-0.3.27 → webbee-0.3.29}/tests/test_intel_query.py +0 -0
  89. {webbee-0.3.27 → webbee-0.3.29}/tests/test_intel_service.py +0 -0
  90. {webbee-0.3.27 → webbee-0.3.29}/tests/test_intel_store.py +0 -0
  91. {webbee-0.3.27 → webbee-0.3.29}/tests/test_intel_vectors.py +0 -0
  92. {webbee-0.3.27 → webbee-0.3.29}/tests/test_intel_watch.py +0 -0
  93. {webbee-0.3.27 → webbee-0.3.29}/tests/test_marathon.py +0 -0
  94. {webbee-0.3.27 → webbee-0.3.29}/tests/test_mode_store.py +0 -0
  95. {webbee-0.3.27 → webbee-0.3.29}/tests/test_newtab_mode.py +0 -0
  96. {webbee-0.3.27 → webbee-0.3.29}/tests/test_packaging.py +0 -0
  97. {webbee-0.3.27 → webbee-0.3.29}/tests/test_reflow.py +0 -0
  98. {webbee-0.3.27 → webbee-0.3.29}/tests/test_render.py +0 -0
  99. {webbee-0.3.27 → webbee-0.3.29}/tests/test_repl.py +0 -0
  100. {webbee-0.3.27 → webbee-0.3.29}/tests/test_repo.py +0 -0
  101. {webbee-0.3.27 → webbee-0.3.29}/tests/test_session.py +0 -0
  102. {webbee-0.3.27 → webbee-0.3.29}/tests/test_sessions.py +0 -0
  103. {webbee-0.3.27 → webbee-0.3.29}/tests/test_sizing.py +0 -0
  104. {webbee-0.3.27 → webbee-0.3.29}/tests/test_slots.py +0 -0
  105. {webbee-0.3.27 → webbee-0.3.29}/tests/test_steer.py +0 -0
  106. {webbee-0.3.27 → webbee-0.3.29}/tests/test_stream.py +0 -0
  107. {webbee-0.3.27 → webbee-0.3.29}/tests/test_tabs.py +0 -0
  108. {webbee-0.3.27 → webbee-0.3.29}/tests/test_thread.py +0 -0
  109. {webbee-0.3.27 → webbee-0.3.29}/tests/test_todo_panel.py +0 -0
  110. {webbee-0.3.27 → webbee-0.3.29}/tests/test_tokens.py +0 -0
  111. {webbee-0.3.27 → webbee-0.3.29}/tests/test_tools.py +0 -0
  112. {webbee-0.3.27 → webbee-0.3.29}/tests/test_tui.py +0 -0
  113. {webbee-0.3.27 → webbee-0.3.29}/tests/test_tui_hardening.py +0 -0
  114. {webbee-0.3.27 → webbee-0.3.29}/tests/test_update.py +0 -0
  115. {webbee-0.3.27 → webbee-0.3.29}/tests/test_urlopen.py +0 -0
  116. {webbee-0.3.27 → webbee-0.3.29}/tests/test_version.py +0 -0
  117. {webbee-0.3.27 → webbee-0.3.29}/tests/test_wallet.py +0 -0
  118. {webbee-0.3.27 → webbee-0.3.29}/tests/test_worktrees.py +0 -0
@@ -1,5 +1,19 @@
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
+
10
+ ## 0.3.28
11
+
12
+ The Home screen scrolls now. When Home has more than fills the window — a tall
13
+ stack of tabs, or command output — scroll it with the mouse wheel or
14
+ PageUp/PageDown, like any other view. Output from a command run on Home (for
15
+ example `/help`) shows inline at the bottom and the view jumps to it.
16
+
3
17
  ## 0.3.27
4
18
 
5
19
  Home dashboard polish.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webbee
3
- Version: 0.3.27
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.27"
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 [])
@@ -293,20 +303,25 @@ class HomeView:
293
303
 
294
304
  def __init__(self, *, slots, actions: "HomeActions",
295
305
  data: "HomeData | None" = None, width: int = 100, out_pane=None):
296
- from prompt_toolkit.layout.containers import HSplit, Window
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()
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.
312
+ # Home is ONE ordinary scrollable Window (NOT dont_extend_height, NOT
313
+ # an HSplit): when its content is taller than the viewport, prompt_
314
+ # toolkit's own Window mouse handler scrolls it on the wheel (content
315
+ # fragment handlers return NotImplemented for SCROLL, so it falls
316
+ # through to Window._mouse_handler), and pageup/pagedown route here via
317
+ # `scroll()`. `_say`/slash-command output is rendered INLINE at the
318
+ # bottom of the SAME scrollable content (see `_fragments`), so it
319
+ # scrolls with everything and never gets squeezed off a short terminal.
320
+ # `out_pane` (built via `tui.OutputPane` by the dock, created first =
321
+ # Home for the spy) stays purely as the CAPTURE backend: `_say` prints
322
+ # into its console and `dump()` reads it back; its own window is NOT in
323
+ # the layout. `self.console` IS that pane's console — it drives both the
324
+ # captured command output AND the dashboard's column-width math.
310
325
  if out_pane is None:
311
326
  from webbee.output_pane import OutputPane
312
327
  out_pane = OutputPane(width=width)
@@ -315,38 +330,68 @@ class HomeView:
315
330
  self._focus_id: "str | None" = None
316
331
  self._hover_id: "str | None" = None
317
332
  self._model: "HomeModel | None" = None
318
- 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])
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)
356
+ self.window = Window(content=self.control, wrap_lines=False, always_hide_cursor=True)
322
357
 
323
358
  # ---- 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
359
  def reflow(self, new_width: int) -> None:
360
+ # Keep the capture console's width in sync (drives the dashboard's
361
+ # column math) and repaint; the Window re-lays-out at the new size.
327
362
  self._out.reflow(new_width)
363
+ self._invalidate()
328
364
 
329
365
  def edge_tick(self) -> None:
330
- self._out.edge_tick()
366
+ return None
331
367
 
332
368
  def flash(self) -> str:
333
369
  return self._out.flash()
334
370
 
335
371
  def scroll(self, delta: int) -> None:
336
- self._out.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))
377
+ self._invalidate()
337
378
 
338
379
  @property
339
380
  def _view_h(self) -> int:
340
- return self._out._view_h
381
+ return self._view_h_val
341
382
 
342
383
  def forward_mouse(self, ev, clamp: str = "bottom") -> bool:
343
- return self._out.forward_mouse(ev, clamp)
384
+ return False
344
385
 
345
386
  def dump(self) -> str:
346
387
  return self._out.dump()
347
388
 
348
389
  def notify(self) -> None:
349
- self._out.notify()
390
+ # New captured output landed (a `_say` note / command reply). Reveal
391
+ # it: jump to the bottom (clamped on the next render) so the answer is
392
+ # visible, then repaint.
393
+ self._scroll = 10 ** 7
394
+ self._invalidate()
350
395
 
351
396
  def _invalidate(self) -> None:
352
397
  try:
@@ -414,7 +459,7 @@ class HomeView:
414
459
  return _h
415
460
 
416
461
  # ---- render -----------------------------------------------------------
417
- def _fragments(self):
462
+ def _all_lines(self):
418
463
  from webbee.home import _mask_email
419
464
  from webbee.render import _fmt_tokens
420
465
  m = self._build_model()
@@ -551,21 +596,10 @@ class HomeView:
551
596
  "· Ctrl+T new tab · Alt+N switch")])
552
597
  return L
553
598
 
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 [("", "")])
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.
566
601
  title_pad = max(0, (width - len("◆ Home")) // 2)
567
- lines.append([("", " " * title_pad), ("class:home.header", "◆ Home")])
568
- lines.append([("", "")])
602
+ lines = [[("", "")], [("", " " * title_pad), ("class:home.header", "◆ Home")], [("", "")]]
569
603
  if two_column(width):
570
604
  colw = max(20, (width // 2) - 3)
571
605
  lines += _side_by_side(you_lines(), wallet_lines(), colw)
@@ -579,8 +613,34 @@ class HomeView:
579
613
  lines += [[("", "")]] + security_lines()
580
614
  lines += footer_lines()
581
615
 
582
- out = []
583
- for ln in lines:
584
- out.extend(ln)
585
- out.append(("", "\n"))
586
- return out
616
+ # `_say` / slash-command output, captured in the backend pane, rendered
617
+ # INLINE at the very bottom of the same scrollable content (ANSI →
618
+ # fragments, styling preserved). `notify()` jumps the scroll here so a
619
+ # command's answer is visible; it scrolls with the rest of Home.
620
+ dump = self._out.dump()
621
+ if dump.strip():
622
+ from prompt_toolkit.formatted_text import ANSI, to_formatted_text
623
+ lines.append([("", "")])
624
+ lines.append([("class:home.header", " Output ")])
625
+ lines.append(list(to_formatted_text(ANSI(dump.rstrip("\n")))))
626
+ return lines
627
+
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,8 +297,42 @@ 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
324
+
325
+
326
+ def test_captured_output_renders_inline_and_notify_reveals_it():
327
+ # Home is one scrollable window; _say/command output is captured in the
328
+ # backend pane and rendered INLINE at the bottom, and notify() jumps the
329
+ # scroll to reveal it.
330
+ hv, _ = _view()
331
+ hv.console.print("hello-inline-output")
332
+ text = "".join(f[1] for f in hv._fragments())
333
+ assert "hello-inline-output" in text
334
+ assert "Output" in text
335
+ hv.notify()
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.27"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes