webbee 0.3.35__tar.gz → 0.3.37__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.35 → webbee-0.3.37}/CHANGELOG.md +60 -0
- {webbee-0.3.35 → webbee-0.3.37}/PKG-INFO +1 -1
- {webbee-0.3.35 → webbee-0.3.37}/pyproject.toml +1 -1
- webbee-0.3.37/src/webbee/__init__.py +1 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/active_sessions.py +79 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/commands.py +25 -2
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/home.py +8 -4
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/home_view.py +104 -4
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/queue_panel.py +50 -3
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/repl.py +315 -11
- webbee-0.3.37/src/webbee/tab_store.py +123 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/tui.py +225 -16
- webbee-0.3.37/src/webbee/update.py +82 -0
- webbee-0.3.37/tests/test_queue_manage_0337.py +138 -0
- webbee-0.3.37/tests/test_tab_restore_0337.py +248 -0
- webbee-0.3.37/tests/test_terminal_ux_0336.py +481 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_tui.py +2 -1
- webbee-0.3.37/tests/test_version_badge_corner.py +71 -0
- webbee-0.3.35/src/webbee/__init__.py +0 -1
- webbee-0.3.35/src/webbee/update.py +0 -52
- {webbee-0.3.35 → webbee-0.3.37}/.github/workflows/publish.yml +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/.gitignore +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/LICENSE +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/README.md +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/docs/superpowers/plans/2026-07-21-interactive-home-dashboard.md +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/install.sh +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/account.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/banner_art.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/boot.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/checkpoints.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/cli.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/clipboard.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/clipboard_read.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/coding_context.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/config.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/consent.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/details.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/events.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/frames.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/http.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/instance_lock.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/intel/chunker.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/intel/embed.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/intel/graph.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/intel/indexer.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/intel/models.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/intel/query.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/intel/service.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/intel/store.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/intel/watch.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/mode_store.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/newtab_mode.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/output_pane.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/reflow.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/remote.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/render.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/repo.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/selection.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/session.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/sessions.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/sizing.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/slots.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/steer.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/stream.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/tabs.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/thread.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/todo_panel.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/tokens.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/tools.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/urlopen.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/wallet.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/src/webbee/worktrees.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/__init__.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/conftest.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_account.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_active_sessions.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_audit_2026_07_25_findings.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_checkpoints.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_cli.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_clipboard.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_clipboard_read.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_coding_context.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_commands.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_config.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_details.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_events.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_freeze_fix.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_home.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_home_view.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_instance_lock.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_intel_chunker.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_intel_embed.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_intel_graph.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_intel_indexer.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_intel_query.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_intel_service.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_intel_store.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_intel_vectors.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_intel_watch.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_marathon.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_mode_store.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_newtab_mode.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_packaging.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_reflow.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_render.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_repl.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_repo.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_session.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_sessions.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_sizing.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_slots.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_steer.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_stream.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_tabs.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_thread.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_todo_panel.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_tokens.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_tools.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_tui_hardening.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_update.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_urlopen.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_version.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_wallet.py +0 -0
- {webbee-0.3.35 → webbee-0.3.37}/tests/test_worktrees.py +0 -0
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.37
|
|
4
|
+
|
|
5
|
+
Your tabs come back, the queue does what you tell it, and the version sits
|
|
6
|
+
where you asked for it.
|
|
7
|
+
|
|
8
|
+
- **Closing the terminal no longer throws your work away.** Three tabs of live
|
|
9
|
+
work used to become one empty tab on the next start. Your layout is
|
|
10
|
+
remembered per repo and restored on boot — and it is reconciled against what
|
|
11
|
+
is *actually* still running, so a tab that really ended comes back as a
|
|
12
|
+
labelled tab rather than a dead one pretending to be alive. An accidental
|
|
13
|
+
exit, Ctrl-D and a crash all remember equally. Autopilot is deliberately
|
|
14
|
+
never remembered: a mode that acts on its own has to be turned on each time.
|
|
15
|
+
- **A queue you can actually manage.** Every queued line now has its own **✕**,
|
|
16
|
+
so you can remove *any* item — previously the only way out was ↑, which
|
|
17
|
+
hijacked your input and could only ever reach the newest row, leaving item 1
|
|
18
|
+
of 3 stuck. `/queue drop N` and `/queue edit N` do the same from the keyboard
|
|
19
|
+
when your terminal reports no mouse. Editing an item *removes* it from the
|
|
20
|
+
queue as it moves into your input, so a line can never both sit in the queue
|
|
21
|
+
and still fire.
|
|
22
|
+
- **Version badge, bottom-right of the window.** Pinned to the corner of the
|
|
23
|
+
whole window rather than floating inside the toolbar's content. On a narrow
|
|
24
|
+
terminal the badge is dropped instead of truncating real information.
|
|
25
|
+
- **A live-session indicator that keeps telling you the truth.** The old boot
|
|
26
|
+
note fired once and scrolled away. The toolbar now shows "● live",
|
|
27
|
+
"● live · needs approval" or "○ N elsewhere", refreshed every 30 seconds from
|
|
28
|
+
what the server actually reports — and it stays empty when nothing is
|
|
29
|
+
running, so an idle dock gains no permanent noise.
|
|
30
|
+
- **✕, Ctrl+W and `/close` all really do close a tab** — they always shared one
|
|
31
|
+
teardown path; what was missing was saying so, and the busy-close note now
|
|
32
|
+
names all three ways out.
|
|
33
|
+
- Fixed two latent crashes that would only have fired on a click (a queue panel
|
|
34
|
+
and a row renderer were each used without being imported).
|
|
35
|
+
|
|
36
|
+
## 0.3.36
|
|
37
|
+
|
|
38
|
+
Write on more than one line, and see what a session actually costs.
|
|
39
|
+
|
|
40
|
+
- **New line inside your prompt.** Press **Alt+Enter** — or **Shift+Enter** on
|
|
41
|
+
a modern terminal, or **Ctrl+J** anywhere at all — to add a line without
|
|
42
|
+
sending. Enter still sends, exactly as before. Three chords because no single
|
|
43
|
+
one is available in every terminal: whichever your setup supports, one of
|
|
44
|
+
them works. Continuation lines are marked with a dim ┊ gutter, so a
|
|
45
|
+
half-written message never looks like something already sent. Pasting
|
|
46
|
+
multi-line text now keeps its line breaks instead of firing off each line.
|
|
47
|
+
- **Spend is visible from every tab.** The status line at the bottom shows the
|
|
48
|
+
tokens and credits used in this session, in every tab. Home — which has no
|
|
49
|
+
session of its own — shows the running TOTAL across all your open tabs, both
|
|
50
|
+
in its status line and in the Wallet tile ("spent this session").
|
|
51
|
+
- **Version and freshness, bottom right of Home.** Your Webbee version sits in
|
|
52
|
+
the bottom-right corner. It says "up to date" only when we actually managed
|
|
53
|
+
to check; when we could not reach the release server it shows the plain
|
|
54
|
+
version rather than a reassurance we cannot back up. A newer release is
|
|
55
|
+
called out in bee-yellow with the version to upgrade to.
|
|
56
|
+
- **`/login` now works on Home.** Previously `/login`, `/logout`, `/sessions`,
|
|
57
|
+
`/logout-others` and `/cost` only worked in a session tab, which made no
|
|
58
|
+
sense — they act on your account, not on a session. They all work on Home
|
|
59
|
+
now, and Home shows a "Sign in to Imperal" button when you are signed out
|
|
60
|
+
instead of a blank space. `/cost` on Home reports the whole session's total.
|
|
61
|
+
- `/help` lists the keyboard shortcuts, including the new-line chords.
|
|
62
|
+
|
|
3
63
|
## 0.3.35
|
|
4
64
|
|
|
5
65
|
Reliability and speed of the terminal itself.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.37"
|
|
@@ -39,6 +39,85 @@ async def fetch_active_sessions(cfg, token_provider, *, client=None) -> list[dic
|
|
|
39
39
|
return []
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def live_session_ids(sessions: list[dict]) -> set:
|
|
43
|
+
"""PURE. The set of session ids the gateway currently reports as RUNNING.
|
|
44
|
+
One helper so the tab-restore reconciliation and the status indicator read
|
|
45
|
+
liveness from the SAME shape (0.3.37)."""
|
|
46
|
+
return {str(s.get("session_id") or "") for s in (sessions or [])
|
|
47
|
+
if str(s.get("session_id") or "")}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def session_indicator(sessions: list[dict], repo_key: str) -> str:
|
|
51
|
+
"""PURE. The PERSISTENT live-session indicator for the toolbar (0.3.37).
|
|
52
|
+
|
|
53
|
+
Before this, a running Temporal workflow was mentioned exactly once, in a
|
|
54
|
+
boot note that scrolled away -- so after an accidental exit and reopen the
|
|
55
|
+
terminal silently re-attached with nothing on screen ever confirming that
|
|
56
|
+
it had seen a running session, or that one existed at all.
|
|
57
|
+
|
|
58
|
+
Returns a SHORT string for the status line, or "" when there is nothing
|
|
59
|
+
to say (no running sessions at all -- silence is correct then, the dock
|
|
60
|
+
must not grow permanent noise):
|
|
61
|
+
* this repo has a live session -> "● live"
|
|
62
|
+
* ...and it is parked on an approval -> "● live · needs approval"
|
|
63
|
+
* only OTHER repos have live ones -> "○ 2 elsewhere"
|
|
64
|
+
Honest by construction: it reports what the gateway's Temporal listing
|
|
65
|
+
actually says, never a guess.
|
|
66
|
+
"""
|
|
67
|
+
suffix = f"-r{repo_key}"
|
|
68
|
+
own, own_parked, others = False, False, 0
|
|
69
|
+
for s in (sessions or []):
|
|
70
|
+
sid = str(s.get("session_id") or "")
|
|
71
|
+
if not sid:
|
|
72
|
+
continue
|
|
73
|
+
if sid.endswith(suffix):
|
|
74
|
+
own = True
|
|
75
|
+
if s.get("pending_consent"):
|
|
76
|
+
own_parked = True
|
|
77
|
+
else:
|
|
78
|
+
others += 1
|
|
79
|
+
if own:
|
|
80
|
+
return "● live · needs approval" if own_parked else "● live"
|
|
81
|
+
if others:
|
|
82
|
+
return f"○ {others} elsewhere"
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def plan_tab_restore(saved: list, sessions: list[dict]) -> list:
|
|
87
|
+
"""PURE. Reconcile the REMEMBERED tab layout (tab_store.load_tabs) against
|
|
88
|
+
the sessions Temporal reports as Running, and decide per tab what reopening
|
|
89
|
+
it means (0.3.37).
|
|
90
|
+
|
|
91
|
+
Each returned dict is the saved record plus:
|
|
92
|
+
* `attach`: True -> its workflow is STILL RUNNING; the tab must re-attach
|
|
93
|
+
and pick the live run up where it left off.
|
|
94
|
+
`attach`: False -> nothing is running under that id any more; restore
|
|
95
|
+
the tab's state (label/mode/draft) only.
|
|
96
|
+
* `pending_consent`: carried through from the live listing so a restored
|
|
97
|
+
tab can immediately show it is parked on an approval.
|
|
98
|
+
|
|
99
|
+
A saved tab with no session_id is legal (a tab you never sent anything in)
|
|
100
|
+
and simply restores as state-only. Never raises; unknown/extra keys on the
|
|
101
|
+
saved record are ignored rather than trusted.
|
|
102
|
+
"""
|
|
103
|
+
live = {}
|
|
104
|
+
for s in (sessions or []):
|
|
105
|
+
sid = str(s.get("session_id") or "")
|
|
106
|
+
if sid:
|
|
107
|
+
live[sid] = s
|
|
108
|
+
out = []
|
|
109
|
+
for rec in (saved or []):
|
|
110
|
+
if not isinstance(rec, dict):
|
|
111
|
+
continue
|
|
112
|
+
sid = str(rec.get("session_id") or "")
|
|
113
|
+
s = live.get(sid)
|
|
114
|
+
item = dict(rec)
|
|
115
|
+
item["attach"] = bool(sid and s is not None)
|
|
116
|
+
item["pending_consent"] = bool((s or {}).get("pending_consent"))
|
|
117
|
+
out.append(item)
|
|
118
|
+
return out
|
|
119
|
+
|
|
120
|
+
|
|
42
121
|
def boot_reattach_notice(sessions: list[dict], repo_key: str) -> list[str]:
|
|
43
122
|
"""Decide what, if anything, to tell the user about their OTHER running
|
|
44
123
|
sessions once THIS terminal's own boot replay has finished. `sessions`
|
|
@@ -23,7 +23,12 @@ _HELP = """Commands:
|
|
|
23
23
|
/sessions revoke <#> revoke a session by its number
|
|
24
24
|
/logout-others sign out every session except this one
|
|
25
25
|
/notify [tg|panel|both|off] mirror this session to Telegram/panel & steer it back
|
|
26
|
-
/exit (=/quit) quit
|
|
26
|
+
/exit (=/quit) quit
|
|
27
|
+
|
|
28
|
+
Keys:
|
|
29
|
+
Alt+↵ (or Shift+↵, or Ctrl+J) new line in the prompt — Enter still sends
|
|
30
|
+
Shift+TAB switch mode Ctrl+T new tab
|
|
31
|
+
Alt+№ switch to tab № Ctrl+W close tab"""
|
|
27
32
|
|
|
28
33
|
|
|
29
34
|
@dataclass(frozen=True)
|
|
@@ -106,6 +111,23 @@ def dispatch(line: str, ctx: CommandContext) -> SlashResult:
|
|
|
106
111
|
# same way /status reads session state); the repl clears the actual
|
|
107
112
|
# deque on action="queue_clear" — the message here already carries
|
|
108
113
|
# the honest drop count.
|
|
114
|
+
# 0.3.37: drop/edit ONE queued item by its 1-based /queue number --
|
|
115
|
+
# the KEYBOARD twin of the panel's per-row ✕ and click-to-edit, so
|
|
116
|
+
# managing a 3-deep queue works on terminals with no mouse reporting
|
|
117
|
+
# (tmux without mouse on, restrictive SSH clients) too. The repl owns
|
|
118
|
+
# the actual deque mutation; this layer only validates + routes.
|
|
119
|
+
if args and args[0].lower() in ("drop", "remove", "rm", "edit"):
|
|
120
|
+
verb = "queue_drop" if args[0].lower() != "edit" else "queue_edit"
|
|
121
|
+
if len(args) < 2:
|
|
122
|
+
return SlashResult(handled=True, action="queue",
|
|
123
|
+
message=f"Usage: /queue {args[0].lower()} <number> "
|
|
124
|
+
f"(see /queue for the numbers).")
|
|
125
|
+
raw = args[1]
|
|
126
|
+
if not raw.isdigit() or int(raw) < 1 or int(raw) > len(ctx.queued):
|
|
127
|
+
return SlashResult(handled=True, action="queue",
|
|
128
|
+
message=(f"No queued item #{raw}. "
|
|
129
|
+
f"Queue holds {len(ctx.queued)}."))
|
|
130
|
+
return SlashResult(handled=True, action=verb, arg=str(int(raw)))
|
|
109
131
|
if args and args[0].lower() == "clear":
|
|
110
132
|
n = len(ctx.queued)
|
|
111
133
|
return SlashResult(handled=True, action="queue_clear",
|
|
@@ -117,7 +139,8 @@ def dispatch(line: str, ctx: CommandContext) -> SlashResult:
|
|
|
117
139
|
lines = "\n".join(f" {i}. {t}" for i, t in enumerate(ctx.queued, 1))
|
|
118
140
|
return SlashResult(handled=True, action="queue",
|
|
119
141
|
message=(f"Queued ({len(ctx.queued)}) — runs in order, "
|
|
120
|
-
f"one per finished turn:\n{lines}"
|
|
142
|
+
f"one per finished turn:\n{lines}\n"
|
|
143
|
+
f" /queue drop <n> · /queue edit <n> · /queue clear"))
|
|
121
144
|
if cmd == "/steps":
|
|
122
145
|
if args:
|
|
123
146
|
return SlashResult(handled=True, action="step_detail", arg=args[0])
|
|
@@ -181,13 +181,17 @@ async def fill_home(slot, *, cfg, token_provider, slots, account_fetcher,
|
|
|
181
181
|
try:
|
|
182
182
|
from pathlib import Path
|
|
183
183
|
|
|
184
|
-
from webbee.update import
|
|
184
|
+
from webbee.update import check_update_status, default_fetch
|
|
185
185
|
cache = Path(os.path.expanduser("~/.cache/webbee/update.json"))
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
# 0.3.36: the (notice, checked) variant -- Home's version badge
|
|
187
|
+
# only claims "up to date" when the check actually resolved a
|
|
188
|
+
# latest version; offline shows the bare version, no false claim.
|
|
189
|
+
data.update_notice, data.update_checked = await asyncio.to_thread(
|
|
190
|
+
check_update_status, version, cache_path=cache, now=time.time(),
|
|
191
|
+
fetch=default_fetch)
|
|
189
192
|
except Exception:
|
|
190
193
|
data.update_notice = ""
|
|
194
|
+
data.update_checked = False
|
|
191
195
|
_repaint()
|
|
192
196
|
finally:
|
|
193
197
|
slot._last_fill = time.monotonic()
|
|
@@ -64,6 +64,9 @@ class HomeData:
|
|
|
64
64
|
endpoint: str = ""
|
|
65
65
|
version: str = ""
|
|
66
66
|
update_notice: str = ""
|
|
67
|
+
# 0.3.36: did the freshness check actually COMPLETE? False = we could not
|
|
68
|
+
# reach PyPI (offline/timeout), so the footer must not claim "up to date".
|
|
69
|
+
update_checked: bool = False
|
|
67
70
|
notice: str = "" # transient one-line note (top-up/security URL)
|
|
68
71
|
|
|
69
72
|
|
|
@@ -90,6 +93,11 @@ class HomeActions:
|
|
|
90
93
|
set_new_tab_mode: "Callable[[str], None]"
|
|
91
94
|
top_up: "Callable[[], None]"
|
|
92
95
|
open_security_docs: "Callable[[], None]"
|
|
96
|
+
# 0.3.36 -- signing in is now possible FROM Home (/login was un-gated), so
|
|
97
|
+
# the dashboard offers it as a real action instead of leaving a bare "…"
|
|
98
|
+
# where the account should be. Optional: None -> the item is not offered
|
|
99
|
+
# (every pre-0.3.36 caller/test keeps working unchanged).
|
|
100
|
+
sign_in: "Callable[[], None] | None" = None
|
|
93
101
|
|
|
94
102
|
|
|
95
103
|
class HomeModel:
|
|
@@ -159,11 +167,64 @@ def _cycle(options, current: str, delta: int) -> str:
|
|
|
159
167
|
return options[(i + delta) % len(options)]
|
|
160
168
|
|
|
161
169
|
|
|
170
|
+
def version_badge(version: str, update_notice: str = "", checked: bool = True) -> "tuple[str, str]":
|
|
171
|
+
"""(text, style_class) for Home's bottom-right version badge (0.3.36).
|
|
172
|
+
|
|
173
|
+
THREE honest states, never a fake reassurance:
|
|
174
|
+
* a newer release is known -> "v0.3.35 → 0.3.36 available" (accent)
|
|
175
|
+
* checked, nothing newer -> "v0.3.36 · up to date" (dim)
|
|
176
|
+
* not checked (offline/first run, `checked=False`) -> "v0.3.36"
|
|
177
|
+
with NO freshness claim at all -- we do not say "up to date" when we
|
|
178
|
+
could not reach PyPI.
|
|
179
|
+
|
|
180
|
+
`update_notice` is `update.check_for_update`'s own sentence; the newer
|
|
181
|
+
version is parsed out of it for the compact badge and the full upgrade
|
|
182
|
+
command stays in the Settings tile, so the footer never wraps.
|
|
183
|
+
"""
|
|
184
|
+
v = (version or "").strip()
|
|
185
|
+
vtxt = f"v{v}" if v else "v?"
|
|
186
|
+
notice = (update_notice or "").strip()
|
|
187
|
+
if notice:
|
|
188
|
+
latest = ""
|
|
189
|
+
for tok in notice.replace("—", " ").split():
|
|
190
|
+
if tok.startswith("v") and tok[1:2].isdigit():
|
|
191
|
+
latest = tok[1:]
|
|
192
|
+
return ((f"{vtxt} → {latest} available" if latest else f"{vtxt} · update available"),
|
|
193
|
+
"class:home.update")
|
|
194
|
+
if checked:
|
|
195
|
+
return (f"{vtxt} · up to date", "class:home.fresh")
|
|
196
|
+
return (vtxt, "class:home.dim")
|
|
197
|
+
|
|
198
|
+
|
|
162
199
|
def two_column(width: int, threshold: int = 100) -> bool:
|
|
163
200
|
"""Wide terminal -> You + Wallet render side-by-side; narrow -> stacked."""
|
|
164
201
|
return width >= threshold
|
|
165
202
|
|
|
166
203
|
|
|
204
|
+
def session_totals(slots) -> "tuple[int, int]":
|
|
205
|
+
"""(tokens, credits) summed across EVERY open session tab (0.3.36).
|
|
206
|
+
|
|
207
|
+
Home has no sink of its own, so "what has this session cost me?" can only
|
|
208
|
+
be answered by adding up the live per-tab counters. Pure + total: any slot
|
|
209
|
+
whose sink is missing or whose status() misbehaves contributes 0 rather
|
|
210
|
+
than breaking the toolbar, and Home's own sink-less slot is skipped
|
|
211
|
+
naturally (no sink -> no numbers). Reuses tab_rows so the numbers on Home
|
|
212
|
+
and in the toolbar can never disagree.
|
|
213
|
+
"""
|
|
214
|
+
tokens = credits = 0
|
|
215
|
+
try:
|
|
216
|
+
rows = tab_rows(slots)
|
|
217
|
+
except Exception:
|
|
218
|
+
return (0, 0)
|
|
219
|
+
for r in rows:
|
|
220
|
+
try:
|
|
221
|
+
tokens += int(r.tokens or 0)
|
|
222
|
+
credits += int(r.credits or 0)
|
|
223
|
+
except (TypeError, ValueError):
|
|
224
|
+
pass
|
|
225
|
+
return (tokens, credits)
|
|
226
|
+
|
|
227
|
+
|
|
167
228
|
def tab_rows(slots) -> "list[TabRow]":
|
|
168
229
|
"""PURE transform of the live SlotManager into Home's per-tab rows
|
|
169
230
|
(session tabs only -- Home never lists itself). Per-tab spend comes from
|
|
@@ -197,6 +258,16 @@ def build_home_model(data: "HomeData", tabs: "list[TabRow]",
|
|
|
197
258
|
enabled (they need no live session)."""
|
|
198
259
|
items: "list[ActionItem]" = []
|
|
199
260
|
|
|
261
|
+
# Signed out? Then signing in is THE next step -- offer it first, before
|
|
262
|
+
# anything that would just fail without an account (0.3.36).
|
|
263
|
+
acct = getattr(data, "account", None)
|
|
264
|
+
signed_in = bool(getattr(acct, "signed_in", False)) if acct is not None else False
|
|
265
|
+
if not signed_in and actions.sign_in is not None:
|
|
266
|
+
items.append(ActionItem(
|
|
267
|
+
id="sign-in", label="→ Sign in to Imperal",
|
|
268
|
+
hint="sign in with your browser (same as typing /login)",
|
|
269
|
+
activate=actions.sign_in))
|
|
270
|
+
|
|
200
271
|
items.append(ActionItem(
|
|
201
272
|
id="new-session", label="+ New session",
|
|
202
273
|
hint="open a new session tab (Ctrl+T)",
|
|
@@ -489,6 +560,13 @@ class HomeView:
|
|
|
489
560
|
a = self.data.account
|
|
490
561
|
L = [hdr("You")]
|
|
491
562
|
if a is None or not getattr(a, "signed_in", False):
|
|
563
|
+
# Not signed in (or not fetched yet): say so plainly and offer
|
|
564
|
+
# the fix right here (0.3.36) instead of an opaque "…".
|
|
565
|
+
if "sign-in" in by_id:
|
|
566
|
+
it = by_id["sign-in"]
|
|
567
|
+
return L + [[("class:home.dim", " not signed in")],
|
|
568
|
+
[("class:home.dim", " "), act(it, "→ Sign in to Imperal")],
|
|
569
|
+
[("class:home.dim", " or type /login")]]
|
|
492
570
|
return L + [[("class:home.dim", " …")]]
|
|
493
571
|
nick = getattr(a, "nickname", "") or ""
|
|
494
572
|
L.append([("class:home.value", f" @{nick}" if nick else " (no nickname)")])
|
|
@@ -516,7 +594,16 @@ class HomeView:
|
|
|
516
594
|
L.append([("class:home.dim", " credits —")])
|
|
517
595
|
else:
|
|
518
596
|
cap = f" / {_fmt_tokens(w.cap)}" if w.cap else ""
|
|
519
|
-
L.append([("class:home.value", f" {_fmt_tokens(w.balance)}{cap} credits")
|
|
597
|
+
L.append([("class:home.value", f" {_fmt_tokens(w.balance)}{cap} credits"),
|
|
598
|
+
("class:home.dim", " left")])
|
|
599
|
+
# This session's TOTAL spend across every open tab (0.3.36) --
|
|
600
|
+
# Home is the summary page, so the figure belongs here and not
|
|
601
|
+
# only in each tab's own toolbar. Same `session_totals` the
|
|
602
|
+
# toolbar uses, so the two can never disagree.
|
|
603
|
+
stk, scr = session_totals(self.slots)
|
|
604
|
+
L.append([("class:home.dim", " spent this session "),
|
|
605
|
+
("class:home.spend", f"{_fmt_tokens(scr)} credits"),
|
|
606
|
+
("class:home.dim", f" ({_fmt_tokens(stk)} tokens)")])
|
|
520
607
|
it = by_id["top-up"]
|
|
521
608
|
L.append([("class:home.dim", " "), act(it, "Top up credits")])
|
|
522
609
|
return L
|
|
@@ -594,9 +681,22 @@ class HomeView:
|
|
|
594
681
|
L = [[("", "")], [("class:home.hint", f" {hint}")]]
|
|
595
682
|
if self.data.notice:
|
|
596
683
|
L.append([("class:home.dim", f" {self.data.notice}")])
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
684
|
+
# Key legend on the LEFT, version badge hard-right on the SAME row
|
|
685
|
+
# (0.3.36) -- the bottom-right corner of Home, as asked. When the
|
|
686
|
+
# window is too narrow to hold both, the badge drops to its own
|
|
687
|
+
# right-aligned row instead of being truncated.
|
|
688
|
+
keys = (" ↑↓ move · ↵ open · ←→ change "
|
|
689
|
+
"· Alt+↵ newline · Ctrl+T new tab · Alt+N switch")
|
|
690
|
+
btxt, bstyle = version_badge(self.data.version, self.data.update_notice,
|
|
691
|
+
checked=self.data.update_checked)
|
|
692
|
+
pad = width - len(keys) - len(btxt) - 2
|
|
693
|
+
if pad >= 1:
|
|
694
|
+
L.append([("class:home.dim", keys), ("", " " * pad),
|
|
695
|
+
(bstyle, btxt), ("", " ")])
|
|
696
|
+
else:
|
|
697
|
+
L.append([("class:home.dim", keys)])
|
|
698
|
+
rpad = max(0, width - len(btxt) - 2)
|
|
699
|
+
L.append([("", " " * rpad), (bstyle, btxt), ("", " ")])
|
|
600
700
|
return L
|
|
601
701
|
|
|
602
702
|
# No center title — the tab bar's ◆ Home chip already names the tab,
|
|
@@ -68,6 +68,43 @@ def pull_item(pending, buf, index: int):
|
|
|
68
68
|
return item
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def drop_item(pending, index: int):
|
|
72
|
+
"""The ONE queue-REMOVE implementation (0.3.37) — serves the per-row ✕ in
|
|
73
|
+
the panel. Deletes pending[index] WITHOUT touching the input buffer, so
|
|
74
|
+
removing item #1 of 3 no longer forces you to pull it into the input just
|
|
75
|
+
to get rid of it (before this, the only exit from the queue was
|
|
76
|
+
`pull_item`, which is edit-the-line, hijacks the buffer and — bound to ↑ —
|
|
77
|
+
only ever reached the NEWEST row).
|
|
78
|
+
|
|
79
|
+
Guards mirror `pull_item`: a stale index (the queue drained between render
|
|
80
|
+
and click) is ignored rather than raising or eating the wrong row. Returns
|
|
81
|
+
the removed item, or None when nothing was removed.
|
|
82
|
+
"""
|
|
83
|
+
if not (0 <= index < len(pending)):
|
|
84
|
+
return None
|
|
85
|
+
item = pending[index]
|
|
86
|
+
del pending[index]
|
|
87
|
+
return item
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _drop_handler(drop, index: int, forward=None):
|
|
91
|
+
"""One row's ✕ mouse handler: MOUSE_UP removes THAT queued item via
|
|
92
|
+
`drop(index)`. Same event discipline as `_item_handler` (MOUSE_UP only,
|
|
93
|
+
`forward` gets first refusal so a drag released here is not mistaken for
|
|
94
|
+
a click) — the difference is purely WHICH action fires, so a ✕ click can
|
|
95
|
+
never be confused with the row-body click that pulls for editing.
|
|
96
|
+
"""
|
|
97
|
+
def _h(mouse_event):
|
|
98
|
+
from prompt_toolkit.mouse_events import MouseEventType
|
|
99
|
+
if forward is not None and forward(mouse_event):
|
|
100
|
+
return None
|
|
101
|
+
if mouse_event.event_type == MouseEventType.MOUSE_UP:
|
|
102
|
+
drop(index)
|
|
103
|
+
return None
|
|
104
|
+
return NotImplemented
|
|
105
|
+
return _h
|
|
106
|
+
|
|
107
|
+
|
|
71
108
|
def _item_handler(pull, index: int, forward=None):
|
|
72
109
|
"""One row's mouse handler: MOUSE_UP (a click, not a drag/press) pulls
|
|
73
110
|
THAT queued item into the input via `pull(index)`; every other event
|
|
@@ -109,7 +146,7 @@ def _toggle_handler(toggle, forward=None):
|
|
|
109
146
|
|
|
110
147
|
def queue_fragments(pending, pull=None, width: int = 0, remote=None,
|
|
111
148
|
collapsed=False, toggle=None, max_items=QP_MAX_ITEMS,
|
|
112
|
-
forward=None):
|
|
149
|
+
forward=None, drop=None):
|
|
113
150
|
"""PURE builder: the panel as prompt_toolkit formatted text, re-invoked
|
|
114
151
|
every redraw (same live mechanics as the toolbar) so every queue
|
|
115
152
|
add/edit/drain shows at once. Layout, top→bottom = drain order (FIFO —
|
|
@@ -159,7 +196,9 @@ def queue_fragments(pending, pull=None, width: int = 0, remote=None,
|
|
|
159
196
|
if collapsed:
|
|
160
197
|
return frags
|
|
161
198
|
if n:
|
|
162
|
-
|
|
199
|
+
hint = (" · ↑ edit last · click to edit · ✕ remove" if drop is not None
|
|
200
|
+
else " · ↑ edit last · click to edit")
|
|
201
|
+
frags.append(("class:qp.item", hint))
|
|
163
202
|
rstart = max(0, len(rem) - max_items)
|
|
164
203
|
if rstart:
|
|
165
204
|
frags.append(("class:qp.remote", f"\n … +{rstart} more"))
|
|
@@ -177,9 +216,17 @@ def queue_fragments(pending, pull=None, width: int = 0, remote=None,
|
|
|
177
216
|
frags.append(("class:qp.item", f"\n … +{start} more"))
|
|
178
217
|
for i in range(start, n):
|
|
179
218
|
style = "class:qp.last" if i == n - 1 else "class:qp.item"
|
|
180
|
-
|
|
219
|
+
# 0.3.37: reserve the ✕ column so the row text truncates BEFORE the
|
|
220
|
+
# button rather than shoving it off-screen on a narrow terminal.
|
|
221
|
+
avail = (width - 4 - (2 if drop is not None else 0)) if width > 0 else 0
|
|
222
|
+
row = "\n " + one_line(items[i], avail)
|
|
181
223
|
if pull is None:
|
|
182
224
|
frags.append((style, row))
|
|
183
225
|
else:
|
|
184
226
|
frags.append((style, row, _item_handler(pull, i, forward)))
|
|
227
|
+
if drop is not None:
|
|
228
|
+
# Its OWN fragment + OWN handler: clicking ✕ removes the item,
|
|
229
|
+
# clicking the row body still pulls it for editing. Two adjacent
|
|
230
|
+
# targets, zero ambiguity.
|
|
231
|
+
frags.append(("class:qp.drop", " ✕", _drop_handler(drop, i, forward)))
|
|
185
232
|
return frags
|