webbee 0.3.43__tar.gz → 0.3.45__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.43 → webbee-0.3.45}/CHANGELOG.md +42 -0
- {webbee-0.3.43 → webbee-0.3.45}/PKG-INFO +1 -1
- {webbee-0.3.43 → webbee-0.3.45}/pyproject.toml +1 -1
- webbee-0.3.45/src/webbee/__init__.py +1 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/home.py +49 -1
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/render.py +51 -7
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/repl.py +18 -2
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/tui.py +13 -2
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_home.py +49 -2
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_render.py +43 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_tui.py +37 -0
- webbee-0.3.43/src/webbee/__init__.py +0 -1
- {webbee-0.3.43 → webbee-0.3.45}/.github/workflows/publish.yml +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/.gitignore +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/LICENSE +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/README.md +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/install.sh +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/account.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/active_sessions.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/banner_art.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/boot.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/checkpoints.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/cli.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/clipboard.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/clipboard_read.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/coding_context.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/commands.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/config.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/consent.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/details.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/events.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/frames.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/home_view.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/http.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/instance_lock.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/intel/chunker.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/intel/embed.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/intel/graph.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/intel/indexer.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/intel/models.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/intel/query.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/intel/service.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/intel/store.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/intel/watch.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/mode_store.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/newtab_mode.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/output_pane.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/queue_panel.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/reflow.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/remote.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/repo.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/selection.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/session.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/sessions.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/sizing.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/slots.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/steer.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/stream.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/tab_store.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/tabs.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/thread.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/tier_store.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/todo_panel.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/tokens.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/tools.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/update.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/urlopen.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/wallet.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/src/webbee/worktrees.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/__init__.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/conftest.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_account.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_active_sessions.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_audit_2026_07_25_findings.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_checkpoints.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_cli.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_clipboard.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_clipboard_read.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_coding_context.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_commands.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_config.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_details.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_events.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_freeze_fix.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_home_view.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_instance_lock.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_intel_chunker.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_intel_embed.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_intel_graph.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_intel_indexer.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_intel_query.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_intel_service.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_intel_store.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_intel_vectors.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_intel_watch.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_marathon.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_mode_store.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_newtab_mode.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_packaging.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_queue_manage_0337.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_reflow.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_repl.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_repo.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_session.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_sessions.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_sizing.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_slots.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_steer.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_stream.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_tab_restore_0337.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_tabs.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_terminal_ux_0336.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_thread.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_tier_store.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_todo_panel.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_tokens.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_tools.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_tui_hardening.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_update.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_update_badge_0340.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_urlopen.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_version.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_version_badge_corner.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_wallet.py +0 -0
- {webbee-0.3.43 → webbee-0.3.45}/tests/test_worktrees.py +0 -0
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.45
|
|
4
|
+
|
|
5
|
+
Consent prompts stop being an easy-to-miss detail:
|
|
6
|
+
|
|
7
|
+
- **Both the kernel consent (`? approve ...`) and the local remote-control
|
|
8
|
+
yes/no ask now print as a bordered card, right in the conversation, the
|
|
9
|
+
moment they're asked** -- the same proven Panel this terminal already
|
|
10
|
+
uses for the billing and login prompts, so it reflows correctly to
|
|
11
|
+
whatever width the terminal actually has (a phone's SSH client, a narrow
|
|
12
|
+
tmux pane, a full window). The question and the "how to answer"
|
|
13
|
+
instructions (`y/yes -> do it`, `n/no -> skip it`, or type your own
|
|
14
|
+
reply) now live in the SAME box -- nothing to go hunting for in a
|
|
15
|
+
toolbar hint several rows away. The underlying protocol is unchanged:
|
|
16
|
+
still a bare raw string relayed verbatim for the kernel to interpret
|
|
17
|
+
(ICNLI, safe-by-default) -- this is a display-only change.
|
|
18
|
+
|
|
19
|
+
## 0.3.44
|
|
20
|
+
|
|
21
|
+
Three real Home-dashboard bugs, found and fixed:
|
|
22
|
+
|
|
23
|
+
- **Each open tab's elapsed time now actually ticks live, per tab.** The
|
|
24
|
+
numbers themselves were always computed correctly per-tab, but Home only
|
|
25
|
+
ever redrew while a turn was busy or a toast was fresh -- sitting idle on
|
|
26
|
+
Home (the normal way to look at your tabs) froze every age at whatever it
|
|
27
|
+
was the moment you switched there, which looked exactly like "one shared
|
|
28
|
+
time for all tabs" even though it wasn't. Home now redraws on the plain
|
|
29
|
+
1s idle tick too, so every tab's own age genuinely counts up in place.
|
|
30
|
+
- **"Your devices" no longer lists dead terminal sessions.** A crashed
|
|
31
|
+
terminal or a laptop that lost network never calls the gateway's revoke
|
|
32
|
+
endpoint on its way out, so the raw session listing accumulated stale
|
|
33
|
+
entries forever. The list now hides a row only once its own last-activity
|
|
34
|
+
timestamp proves it's actually gone (>24h) -- your current device always
|
|
35
|
+
shows, and a row with no timestamp at all (older data) still shows rather
|
|
36
|
+
than risk hiding something real.
|
|
37
|
+
- **The notification toggle now reflects what the server actually
|
|
38
|
+
confirmed, not just what you clicked.** The old code echoed the click's
|
|
39
|
+
raw argument straight into the display the instant the request was sent,
|
|
40
|
+
so a write the server silently rejected or normalized still looked like
|
|
41
|
+
it had taken effect. It now reads back the server's own state after the
|
|
42
|
+
write, and clicking with no session tab open tells you plainly instead of
|
|
43
|
+
doing nothing silently.
|
|
44
|
+
|
|
3
45
|
## 0.3.43
|
|
4
46
|
|
|
5
47
|
Honest fix-up of three things 0.3.42 got wrong or left unfinished:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.45"
|
|
@@ -100,6 +100,49 @@ def _device_label(s: dict) -> str:
|
|
|
100
100
|
return "session"
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
DEVICE_STALE_TTL_S = 24 * 3600 # webbee-code-home-live-devices-v1
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _is_recently_active(s: dict, now: float, ttl: float = DEVICE_STALE_TTL_S) -> bool:
|
|
107
|
+
"""PURE. Whether a `/v1/auth/sessions` row is worth showing on Home's
|
|
108
|
+
device list. `/v1/auth/sessions` lists every session token the gateway
|
|
109
|
+
has ever ISSUED that hasn't been explicitly revoked -- a terminal that
|
|
110
|
+
crashed or a laptop that lost network never calls revoke on its way out,
|
|
111
|
+
so the raw listing accumulates dead entries forever (Valentin, live
|
|
112
|
+
2026-07-31: "a bunch of dead terminal sessions"). The CURRENT session
|
|
113
|
+
(`current`/`is_current`) always counts as active regardless of its own
|
|
114
|
+
timestamp -- it's the one you're looking at right now. Every other row
|
|
115
|
+
is checked against `last_seen_at` (or `updated_at`/`created_at` as a
|
|
116
|
+
fallback) when the gateway actually SENDS one of those fields: only a
|
|
117
|
+
successfully-parsed timestamp older than `ttl` hides the row. A row with
|
|
118
|
+
NONE of those fields at all (an older gateway shape that never sends
|
|
119
|
+
them) degrades the OTHER way -- unknown is shown, never hidden, since
|
|
120
|
+
hiding a device we simply have no data on risks hiding a real one; only
|
|
121
|
+
a row we can actually PROVE is stale gets filtered out."""
|
|
122
|
+
if bool(s.get("current") or s.get("is_current")):
|
|
123
|
+
return True
|
|
124
|
+
for k in ("last_seen_at", "updated_at", "created_at"):
|
|
125
|
+
iso = s.get(k)
|
|
126
|
+
if not iso:
|
|
127
|
+
continue
|
|
128
|
+
try:
|
|
129
|
+
from datetime import datetime, timezone
|
|
130
|
+
ss = str(iso).replace("Z", "+00:00")
|
|
131
|
+
dt = datetime.fromisoformat(ss)
|
|
132
|
+
if dt.tzinfo is None:
|
|
133
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
134
|
+
age = (datetime.now(timezone.utc) - dt).total_seconds()
|
|
135
|
+
if age < 0:
|
|
136
|
+
continue # malformed (future) timestamp -- try the next field
|
|
137
|
+
return age <= ttl
|
|
138
|
+
except Exception:
|
|
139
|
+
continue
|
|
140
|
+
# every timestamp field was absent, unparseable, or in the future:
|
|
141
|
+
# unknown, not PROVEN stale -- show it (see docstring) rather than
|
|
142
|
+
# silently hide a real device we simply have no good data on.
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
|
|
103
146
|
async def fill_home(slot, *, cfg, token_provider, slots, account_fetcher,
|
|
104
147
|
sessions_client, resources, version, wallet_fetcher=None) -> None:
|
|
105
148
|
"""Populate the Home dashboard's HomeData, best-effort, staged (a repaint
|
|
@@ -161,9 +204,14 @@ async def fill_home(slot, *, cfg, token_provider, slots, account_fetcher,
|
|
|
161
204
|
|
|
162
205
|
try:
|
|
163
206
|
listing = await sessions_client.list_sessions(cfg, token_provider)
|
|
207
|
+
# webbee-code-home-live-devices-v1: drop rows the gateway never
|
|
208
|
+
# got a chance to revoke (crashed terminal, dead laptop) so the
|
|
209
|
+
# list reads as "your real devices", not a graveyard of old
|
|
210
|
+
# terminal sessions -- see _is_recently_active's own docstring.
|
|
211
|
+
now = time.time()
|
|
164
212
|
data.devices = [DeviceRow(label=_device_label(s),
|
|
165
213
|
current=bool(s.get("current") or s.get("is_current")))
|
|
166
|
-
for s in (listing or [])]
|
|
214
|
+
for s in (listing or []) if _is_recently_active(s, now)]
|
|
167
215
|
except Exception:
|
|
168
216
|
data.devices = []
|
|
169
217
|
_repaint()
|
|
@@ -614,12 +614,38 @@ class RichSink:
|
|
|
614
614
|
NEVER interpret (the kernel decides, ICNLI). When the dock is running
|
|
615
615
|
the reply comes through the pinned box via an asyncio.Future (no
|
|
616
616
|
blocking input on the event loop); otherwise fall back to the injected
|
|
617
|
-
sync reader (tests / non-tty).
|
|
617
|
+
sync reader (tests / non-tty).
|
|
618
|
+
|
|
619
|
+
webbee-code-consent-card-v1: this used to be one dim, easy-to-miss
|
|
620
|
+
line in the feed plus a SEPARATE hint buried in the toolbar below --
|
|
621
|
+
several users said it wasn't obvious what was even being asked, let
|
|
622
|
+
alone how to answer it. Now it's a bordered card printed RIGHT IN
|
|
623
|
+
THE CONVERSATION, at the moment it's asked -- same proven Panel this
|
|
624
|
+
module already uses for the billing/login prompts, so it reflows to
|
|
625
|
+
whatever width the terminal actually has (phone SSH client, a narrow
|
|
626
|
+
tmux pane, a full-width window -- all get a readable card, not a
|
|
627
|
+
clipped line). The question AND the "how to answer" instructions
|
|
628
|
+
live in the SAME box, so there's nothing to hunt for elsewhere on
|
|
629
|
+
screen. The protocol contract is unchanged: still a bare raw string
|
|
630
|
+
relayed verbatim for the kernel to interpret (ICNLI, safe-by-default)."""
|
|
618
631
|
label = _clean(f"{app_id}·{tool}" if app_id else tool)
|
|
619
|
-
sal = _clean(_salient_arg(args))
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
632
|
+
sal = _clean(_salient_arg(args))[:500]
|
|
633
|
+
body = Text()
|
|
634
|
+
body.append(label, style=f"bold {_ACCENT}")
|
|
635
|
+
if sal:
|
|
636
|
+
body.append("\n" + sal, style="white")
|
|
637
|
+
body.append("\n\n")
|
|
638
|
+
body.append("y", style="bold green")
|
|
639
|
+
body.append(" / ", style="dim")
|
|
640
|
+
body.append("yes", style="bold green")
|
|
641
|
+
body.append(" → do it ", style="dim")
|
|
642
|
+
body.append("n", style="bold red")
|
|
643
|
+
body.append(" / ", style="dim")
|
|
644
|
+
body.append("no", style="bold red")
|
|
645
|
+
body.append(" → skip it\n", style="dim")
|
|
646
|
+
body.append("or just type your own reply in any language — Enter to send", style="dim")
|
|
647
|
+
self.console.print(_pad(Panel(body, title="🔐 wants to do this — approve?",
|
|
648
|
+
border_style=_BEE)))
|
|
623
649
|
fut = self._arm_consent(label, sal)
|
|
624
650
|
if fut is None: # non-tty / no running app
|
|
625
651
|
raw = self._input(" ")
|
|
@@ -643,8 +669,26 @@ class RichSink:
|
|
|
643
669
|
all return False, so the caller keeps the current mode. This is a
|
|
644
670
|
LOCAL policy decision, not a kernel consent, so the reply is
|
|
645
671
|
interpreted here rather than relayed (ICNLI raw-relay applies to
|
|
646
|
-
kernel consents only).
|
|
647
|
-
|
|
672
|
+
kernel consents only).
|
|
673
|
+
|
|
674
|
+
webbee-code-consent-card-v1: same bordered-card treatment as
|
|
675
|
+
ask_consent -- a bare "⚠ ..." line was just as easy to miss as the
|
|
676
|
+
old one-line kernel consent was. The card reflows to the terminal's
|
|
677
|
+
real width on any device, and states the y/n instructions right
|
|
678
|
+
where the question is instead of leaving them implicit."""
|
|
679
|
+
body = Text()
|
|
680
|
+
body.append(_clean(question), style=f"bold {_BEE}")
|
|
681
|
+
body.append("\n\n")
|
|
682
|
+
body.append("y", style="bold green")
|
|
683
|
+
body.append(" / ", style="dim")
|
|
684
|
+
body.append("yes", style="bold green")
|
|
685
|
+
body.append(" → allow ", style="dim")
|
|
686
|
+
body.append("n", style="bold red")
|
|
687
|
+
body.append(" / ", style="dim")
|
|
688
|
+
body.append("no", style="bold red")
|
|
689
|
+
body.append(" → stay as-is (default)", style="dim")
|
|
690
|
+
self.console.print(_pad(Panel(body, title="⚠ another surface is asking",
|
|
691
|
+
border_style=_BEE)))
|
|
648
692
|
fut = self._arm_consent(question, question)
|
|
649
693
|
try:
|
|
650
694
|
if fut is None: # non-tty / no dock → sync reader
|
|
@@ -1727,17 +1727,33 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
|
|
|
1727
1727
|
state["new_tab_mode"] = newtab_mode.load_newtab_mode() or mode
|
|
1728
1728
|
|
|
1729
1729
|
def _home_set_notify(arg: str) -> None:
|
|
1730
|
+
# webbee-code-home-live-notify-v1: resolve the TARGET
|
|
1731
|
+
# session ONCE, up front, and thread that SAME sid through
|
|
1732
|
+
# to the write below -- the old code re-resolved
|
|
1733
|
+
# "whichever session Home currently points at" only inside
|
|
1734
|
+
# the click handler, which is fine, but never told the
|
|
1735
|
+
# user anything when there was no session tab to apply it
|
|
1736
|
+
# to (silent no-op looked exactly like "did nothing" --
|
|
1737
|
+
# Valentin, live 2026-07-31: "doesn't show real values").
|
|
1730
1738
|
picked = _pick_session_slot(slots)
|
|
1731
1739
|
sid = getattr(getattr(picked, "agent", None), "session_id", "") if picked else ""
|
|
1732
1740
|
if not sid:
|
|
1741
|
+
home_view.data.notice = "open a session tab first — notifications are per-session"
|
|
1742
|
+
home_view.notify()
|
|
1733
1743
|
return
|
|
1734
1744
|
async def _do():
|
|
1735
1745
|
try:
|
|
1736
1746
|
st = await _remote_mod.set_remote(cfg, token_provider, sid, arg)
|
|
1737
|
-
|
|
1747
|
+
# webbee-code-home-live-notify-v1: show what the
|
|
1748
|
+
# SERVER actually confirmed (from_notify_state_from
|
|
1749
|
+
# its own state), never the raw click argument --
|
|
1750
|
+
# the old code trusted `arg` unconditionally, so a
|
|
1751
|
+
# write the server silently rejected/normalized
|
|
1752
|
+
# still displayed as if it had taken effect.
|
|
1753
|
+
home_view.data.notify_state = home._notify_state_from(st)
|
|
1738
1754
|
home_view.data.remote_desc = _remote_mod.describe(st)
|
|
1739
1755
|
except Exception:
|
|
1740
|
-
|
|
1756
|
+
home_view.data.notice = "couldn't update notifications — try again"
|
|
1741
1757
|
home_view.notify()
|
|
1742
1758
|
get_app().create_background_task(_do())
|
|
1743
1759
|
|
|
@@ -477,7 +477,8 @@ def _tick_once(slots, app, is_busy, breathing=None) -> None:
|
|
|
477
477
|
when a turn is running OR the copy-flash toast is still fresh, so the
|
|
478
478
|
spinner/elapsed-clock/flash-expiry all animate without redrawing on every
|
|
479
479
|
idle tick for nothing."""
|
|
480
|
-
|
|
480
|
+
active = slots.active()
|
|
481
|
+
pane = active.pane
|
|
481
482
|
_width_watch(pane, app)
|
|
482
483
|
try:
|
|
483
484
|
pane.edge_tick()
|
|
@@ -488,7 +489,17 @@ def _tick_once(slots, app, is_busy, breathing=None) -> None:
|
|
|
488
489
|
# state right now -- if so the idle 1.0s-cadence tick still has to
|
|
489
490
|
# actually REDRAW so the alternating shade is ever seen, not just
|
|
490
491
|
# computed and silently discarded on every tick the same as a no-op one.
|
|
491
|
-
|
|
492
|
+
# webbee-code-home-live-ages-v1: Home's own tab-list ages/durations are
|
|
493
|
+
# computed FRESH every render (home_view.tab_rows reads time.monotonic()
|
|
494
|
+
# each call) -- but a render only happens on an app.invalidate(), and the
|
|
495
|
+
# busy/flash/breathing checks above are ALL false while you're simply
|
|
496
|
+
# sitting on Home doing nothing. That froze every age at whatever it was
|
|
497
|
+
# the moment you switched to Home -- looking like one shared, wrong
|
|
498
|
+
# number instead of each tab's own live-ticking age. Home is cheap to
|
|
499
|
+
# redraw (same virtualized-slice render every other pane already does),
|
|
500
|
+
# so it gets the plain 1s idle cadence's invalidate too, unconditionally.
|
|
501
|
+
if (is_busy() or pane.flash() or (breathing is not None and breathing())
|
|
502
|
+
or getattr(active, "kind", "") == "home"):
|
|
492
503
|
app.invalidate()
|
|
493
504
|
|
|
494
505
|
|
|
@@ -4,8 +4,8 @@ tiles, async best-effort fill, is_stale/refill scheduling, and home_input's
|
|
|
4
4
|
import asyncio
|
|
5
5
|
|
|
6
6
|
from webbee.account import Account
|
|
7
|
-
from webbee.home import (_mask_email, _notify_state_from,
|
|
8
|
-
fill_home, is_stale)
|
|
7
|
+
from webbee.home import (_is_recently_active, _mask_email, _notify_state_from,
|
|
8
|
+
_pick_session_slot, fill_home, is_stale)
|
|
9
9
|
from webbee.repl import _home_input, _home_target_workspace, _schedule_home_refill
|
|
10
10
|
from webbee.slots import SessionSlot, SlotManager, WorkspaceResources
|
|
11
11
|
from webbee.wallet import Wallet
|
|
@@ -175,6 +175,53 @@ def test_fill_home_builds_device_rows_without_pii():
|
|
|
175
175
|
assert not any("1.2.3.4" in lbl for lbl in labels) # raw IP never surfaced
|
|
176
176
|
|
|
177
177
|
|
|
178
|
+
def test_is_recently_active_keeps_current_regardless_of_timestamp():
|
|
179
|
+
# webbee-code-home-live-devices-v1: the row you're looking at right now
|
|
180
|
+
# always counts as active, even with a wildly stale/missing timestamp.
|
|
181
|
+
assert _is_recently_active({"current": True}, now=1_000_000.0) is True
|
|
182
|
+
assert _is_recently_active({"is_current": True, "last_seen_at": "2000-01-01T00:00:00Z"},
|
|
183
|
+
now=1_000_000.0) is True
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_is_recently_active_hides_a_proven_stale_row():
|
|
187
|
+
from datetime import datetime, timedelta, timezone
|
|
188
|
+
old_ts = (datetime.now(timezone.utc) - timedelta(days=40)).isoformat()
|
|
189
|
+
assert _is_recently_active({"last_seen_at": old_ts}, now=0.0) is False
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_is_recently_active_shows_a_recent_row():
|
|
193
|
+
from datetime import datetime, timezone
|
|
194
|
+
fresh_ts = datetime.now(timezone.utc).isoformat()
|
|
195
|
+
assert _is_recently_active({"last_seen_at": fresh_ts}, now=0.0) is True
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_is_recently_active_shows_unknown_rows_never_proven_stale():
|
|
199
|
+
# No timestamp field at all (older gateway shape) -- unknown, not
|
|
200
|
+
# PROVEN stale, so it must still show rather than silently vanish.
|
|
201
|
+
assert _is_recently_active({"label": "old client"}, now=0.0) is True
|
|
202
|
+
# An unparseable timestamp degrades the same safe way.
|
|
203
|
+
assert _is_recently_active({"last_seen_at": "not-a-date"}, now=0.0) is True
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_fill_home_filters_stale_devices():
|
|
207
|
+
from datetime import datetime, timedelta, timezone
|
|
208
|
+
async def acct(cfg, tp):
|
|
209
|
+
return Account(signed_in=False)
|
|
210
|
+
fresh_ts = datetime.now(timezone.utc).isoformat()
|
|
211
|
+
stale_ts = (datetime.now(timezone.utc) - timedelta(days=40)).isoformat()
|
|
212
|
+
listing = [{"label": "this laptop", "current": True},
|
|
213
|
+
{"label": "dead terminal", "last_seen_at": stale_ts},
|
|
214
|
+
{"label": "phone, recently used", "last_seen_at": fresh_ts}]
|
|
215
|
+
home = _home_slot()
|
|
216
|
+
slots = SlotManager(); slots.add(home)
|
|
217
|
+
asyncio.run(fill_home(home, cfg=_Cfg(), token_provider=_tok, slots=slots,
|
|
218
|
+
account_fetcher=acct, sessions_client=_FakeSessions(listing),
|
|
219
|
+
resources=WorkspaceResources(), version="1.0.0", wallet_fetcher=None))
|
|
220
|
+
labels = [r.label for r in home.pane.data.devices]
|
|
221
|
+
assert labels == ["this laptop", "phone, recently used"]
|
|
222
|
+
assert "dead terminal" not in labels
|
|
223
|
+
|
|
224
|
+
|
|
178
225
|
def test_fill_home_reentrancy_guard():
|
|
179
226
|
calls = []
|
|
180
227
|
async def counting(cfg, tp):
|
|
@@ -299,6 +299,49 @@ def test_consent_shows_human_summary_not_dict():
|
|
|
299
299
|
assert "notes·delete_note" in out
|
|
300
300
|
|
|
301
301
|
|
|
302
|
+
# ── webbee-code-consent-card-v1: the consent card is a real, hard-to-miss
|
|
303
|
+
# bordered block printed IN the conversation, with the y/n instructions
|
|
304
|
+
# living right next to the question -- not a dim one-liner + a separate
|
|
305
|
+
# toolbar hint the user has to go hunting for. ────────────────────────────
|
|
306
|
+
|
|
307
|
+
def test_consent_is_a_bordered_card_with_inline_instructions():
|
|
308
|
+
console = Console(record=True, width=90)
|
|
309
|
+
s = RichSink(console=console, live_enabled=False,
|
|
310
|
+
input_fn=lambda p: "yes", clock=lambda: 0.0)
|
|
311
|
+
asyncio.run(s.ask_consent("notes", "delete_note", {"title": "Q3 budget"}))
|
|
312
|
+
out = console.export_text()
|
|
313
|
+
# a real Panel border, not a bare printed line
|
|
314
|
+
assert "\u256d" in out or "\u2500" in out # rounded-corner or horizontal rule glyph
|
|
315
|
+
assert "approve?" in out
|
|
316
|
+
# the how-to-answer instructions are IN the card, not somewhere else
|
|
317
|
+
assert "yes" in out and "no" in out
|
|
318
|
+
assert "do it" in out and "skip it" in out
|
|
319
|
+
assert "notes·delete_note" in out
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_consent_card_reflows_to_narrow_terminal_width():
|
|
323
|
+
# A phone SSH client / narrow tmux pane: the card must not crash or
|
|
324
|
+
# silently drop the question -- Panel reflows, same as billing/login.
|
|
325
|
+
console = Console(record=True, width=28)
|
|
326
|
+
s = RichSink(console=console, live_enabled=False,
|
|
327
|
+
input_fn=lambda p: "y", clock=lambda: 0.0)
|
|
328
|
+
asyncio.run(s.ask_consent("mail", "delete", {"query": "old newsletters"}))
|
|
329
|
+
out = console.export_text()
|
|
330
|
+
assert "mail" in out and "delete" in out
|
|
331
|
+
assert "yes" in out and "no" in out
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_ask_yes_no_is_also_a_bordered_card_with_instructions():
|
|
335
|
+
console = Console(record=True, width=90)
|
|
336
|
+
s = RichSink(console=console, live_enabled=False,
|
|
337
|
+
input_fn=lambda p: "y", clock=lambda: 0.0)
|
|
338
|
+
asyncio.run(s.ask_yes_no("switch to autopilot — allow? [y/n]"))
|
|
339
|
+
out = console.export_text()
|
|
340
|
+
assert "\u256d" in out or "\u2500" in out
|
|
341
|
+
assert "allow" in out.lower()
|
|
342
|
+
assert "yes" in out and "no" in out
|
|
343
|
+
|
|
344
|
+
|
|
302
345
|
def test_user_echo_is_unboxed_line():
|
|
303
346
|
s = _sink()
|
|
304
347
|
s.user_echo("delete my Q3 note")
|
|
@@ -1548,6 +1548,43 @@ def test_tick_once_fires_width_watch_edge_tick_and_invalidate(monkeypatch):
|
|
|
1548
1548
|
assert calls["invalidate"] == 2 # is_busy() True -> app.invalidate()
|
|
1549
1549
|
|
|
1550
1550
|
|
|
1551
|
+
def test_tick_once_always_invalidates_on_home_even_when_idle(monkeypatch):
|
|
1552
|
+
# webbee-code-home-live-ages-v1: Home's tab list computes each tab's own
|
|
1553
|
+
# age fresh every render, but a render only happens on invalidate() --
|
|
1554
|
+
# so sitting idle on Home (not busy, no flash, not breathing) must NOT
|
|
1555
|
+
# skip the redraw the way every other idle pane correctly does, or every
|
|
1556
|
+
# tab's elapsed time freezes at whatever it was when you switched here.
|
|
1557
|
+
from webbee.tui import _tick_once
|
|
1558
|
+
|
|
1559
|
+
class _Pane:
|
|
1560
|
+
def __init__(self):
|
|
1561
|
+
self.console = type("Console", (), {"width": 72})()
|
|
1562
|
+
def reflow(self, cols):
|
|
1563
|
+
pass
|
|
1564
|
+
def edge_tick(self):
|
|
1565
|
+
pass
|
|
1566
|
+
def flash(self):
|
|
1567
|
+
return ""
|
|
1568
|
+
|
|
1569
|
+
class _App:
|
|
1570
|
+
def __init__(self):
|
|
1571
|
+
self.n = 0
|
|
1572
|
+
def invalidate(self):
|
|
1573
|
+
self.n += 1
|
|
1574
|
+
|
|
1575
|
+
monkeypatch.setattr("webbee.sizing.get_size", lambda app: (72, 24))
|
|
1576
|
+
slots = mk_slots(pane=_Pane())
|
|
1577
|
+
slots.slots[0].kind = "home"
|
|
1578
|
+
app = _App()
|
|
1579
|
+
_tick_once(slots, app, lambda: False) # idle, no breathing
|
|
1580
|
+
assert app.n == 1 # still invalidated because the active slot is Home
|
|
1581
|
+
|
|
1582
|
+
slots.slots[0].kind = "session"
|
|
1583
|
+
app2 = _App()
|
|
1584
|
+
_tick_once(slots, app2, lambda: False)
|
|
1585
|
+
assert app2.n == 0 # a genuinely idle SESSION tab still skips the redraw
|
|
1586
|
+
|
|
1587
|
+
|
|
1551
1588
|
def test_tick_once_swallows_edge_tick_error(monkeypatch):
|
|
1552
1589
|
from webbee.tui import _tick_once
|
|
1553
1590
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.43"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|