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