webbee 0.3.17__tar.gz → 0.3.19__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.17 → webbee-0.3.19}/CHANGELOG.md +33 -0
- {webbee-0.3.17 → webbee-0.3.19}/PKG-INFO +1 -1
- {webbee-0.3.17 → webbee-0.3.19}/pyproject.toml +1 -1
- webbee-0.3.19/src/webbee/__init__.py +1 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/boot.py +31 -3
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/commands.py +16 -0
- webbee-0.3.19/src/webbee/home.py +290 -0
- webbee-0.3.19/src/webbee/output_pane.py +351 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/queue_panel.py +41 -18
- webbee-0.3.19/src/webbee/reflow.py +53 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/render.py +37 -24
- webbee-0.3.19/src/webbee/repl.py +1045 -0
- webbee-0.3.19/src/webbee/selection.py +160 -0
- webbee-0.3.19/src/webbee/sizing.py +40 -0
- webbee-0.3.19/src/webbee/slots.py +146 -0
- webbee-0.3.19/src/webbee/tabs.py +125 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/todo_panel.py +23 -14
- webbee-0.3.19/src/webbee/tui.py +1067 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_commands.py +34 -0
- webbee-0.3.19/tests/test_home.py +430 -0
- webbee-0.3.19/tests/test_reflow.py +85 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_render.py +28 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_repl.py +961 -1
- webbee-0.3.19/tests/test_sizing.py +30 -0
- webbee-0.3.19/tests/test_slots.py +338 -0
- webbee-0.3.19/tests/test_tabs.py +280 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_todo_panel.py +52 -0
- webbee-0.3.19/tests/test_tui.py +4600 -0
- webbee-0.3.17/src/webbee/__init__.py +0 -1
- webbee-0.3.17/src/webbee/output_pane.py +0 -238
- webbee-0.3.17/src/webbee/repl.py +0 -523
- webbee-0.3.17/src/webbee/tui.py +0 -657
- webbee-0.3.17/tests/test_tui.py +0 -1759
- {webbee-0.3.17 → webbee-0.3.19}/.github/workflows/publish.yml +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/.gitignore +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/LICENSE +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/README.md +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/install.sh +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/account.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/banner_art.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/checkpoints.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/cli.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/clipboard.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/coding_context.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/config.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/consent.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/details.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/events.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/frames.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/http.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/intel/chunker.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/intel/embed.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/intel/graph.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/intel/indexer.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/intel/models.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/intel/query.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/intel/service.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/intel/store.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/intel/watch.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/remote.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/repo.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/session.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/sessions.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/steer.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/stream.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/thread.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/tokens.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/tools.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/src/webbee/update.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/__init__.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_account.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_checkpoints.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_cli.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_clipboard.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_config.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_details.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_events.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_freeze_fix.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_intel_chunker.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_intel_embed.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_intel_graph.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_intel_indexer.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_intel_query.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_intel_service.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_intel_store.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_intel_vectors.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_intel_watch.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_marathon.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_packaging.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_repo.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_session.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_sessions.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_steer.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_stream.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_thread.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_tokens.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_tools.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_tui_hardening.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_update.py +0 -0
- {webbee-0.3.17 → webbee-0.3.19}/tests/test_version.py +0 -0
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.19
|
|
4
|
+
|
|
5
|
+
The browser wave: Webbee Code becomes a browser inside your terminal.
|
|
6
|
+
|
|
7
|
+
- A tab bar lives at the top: ◆ Home first, your sessions after it, each with
|
|
8
|
+
a live status glyph (▶ working · ⚠ waiting for your approval · ○ idle).
|
|
9
|
+
Click a tab or press Alt+0..9 to switch; Ctrl+T jumps to Home; Ctrl+W or
|
|
10
|
+
/close closes a tab (the run keeps living server-side); the ✕ on a tab
|
|
11
|
+
closes exactly that tab.
|
|
12
|
+
- Home is a new-tab page: your identity and plan, every open tab, the current
|
|
13
|
+
repo's intelligence and checkpoints, remote-control state and updates —
|
|
14
|
+
filled live, resize-aware. Start typing on Home and a new session tab opens
|
|
15
|
+
with your message. A fresh install lands on Home; an ongoing conversation
|
|
16
|
+
lands back in its session tab.
|
|
17
|
+
- Every tab is its own world: transcript, queue, todos, approvals, mode and
|
|
18
|
+
input history never leak between tabs. Background tabs keep streaming; a
|
|
19
|
+
tab waiting for your approval shows a ⚠ badge from anywhere; a running
|
|
20
|
+
background turn is protected from a stray Ctrl+D.
|
|
21
|
+
|
|
22
|
+
## 0.3.18
|
|
23
|
+
|
|
24
|
+
The adaptive wave: the terminal finally behaves like a real app.
|
|
25
|
+
|
|
26
|
+
- Stretch or shrink the window and the WHOLE transcript re-wraps live to the
|
|
27
|
+
new size — history included, splash re-centered, nothing clipped, no dead
|
|
28
|
+
margins. Panels, the input box and text truncations now scale as
|
|
29
|
+
proportions of your screen, not fixed character counts.
|
|
30
|
+
- Select text with the mouse and drag past the edge — the transcript
|
|
31
|
+
auto-scrolls under your selection, keeps selecting while you hold at the
|
|
32
|
+
edge, and finishes the copy even if you release outside the pane. Scrolling
|
|
33
|
+
mid-selection no longer corrupts what you copy, and a lost release can
|
|
34
|
+
never hijack your next click or your clipboard.
|
|
35
|
+
|
|
3
36
|
## 0.3.17
|
|
4
37
|
|
|
5
38
|
Bulletproof core (W1): a marathon can no longer be killed by transport, the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.19"
|
|
@@ -60,10 +60,17 @@ def _default_intel_factory(cfg, workspace: str):
|
|
|
60
60
|
return IntelService(root, compute_repo_key(root), cache_dir=cfg.cache_dir)
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
async def replay_thread(cfg, token_provider, sink) ->
|
|
63
|
+
async def replay_thread(cfg, token_provider, sink) -> int:
|
|
64
64
|
"""Boot replay of the durable per-user thread (Task 9): best-effort,
|
|
65
65
|
entirely swallowed on any failure -- history is a nice-to-have,
|
|
66
|
-
never a boot blocker (network down, no such session yet, etc.).
|
|
66
|
+
never a boot blocker (network down, no such session yet, etc.).
|
|
67
|
+
|
|
68
|
+
Returns the count of replayed DISPLAY messages (FIX7e, W4a final review
|
|
69
|
+
— land-on-Home): 0 on a fresh/empty thread, or on ANY failure/skip --
|
|
70
|
+
the never-raise contract is unchanged, this is purely an additional
|
|
71
|
+
signal. repl's dock boot uses it to decide whether to land on the
|
|
72
|
+
session tab (there's something to show) or on Home (nothing yet --
|
|
73
|
+
the new-tab dashboard is the more useful first screen)."""
|
|
67
74
|
try:
|
|
68
75
|
from imperal_mcp.client import ImperalClient
|
|
69
76
|
from webbee.thread import (conversational_text, fetch_recent_thread,
|
|
@@ -80,8 +87,9 @@ async def replay_thread(cfg, token_provider, sink) -> None:
|
|
|
80
87
|
_shown += 1
|
|
81
88
|
if _shown:
|
|
82
89
|
sink.note("— live —")
|
|
90
|
+
return _shown
|
|
83
91
|
except Exception:
|
|
84
|
-
|
|
92
|
+
return 0 # replay is best-effort; never block boot
|
|
85
93
|
|
|
86
94
|
|
|
87
95
|
async def start_intel(cfg, workspace: str, intel_factory):
|
|
@@ -106,3 +114,23 @@ async def start_shadow(cfg, workspace: str, shadow_factory):
|
|
|
106
114
|
return await asyncio.to_thread(shadow_factory or _default_shadow_factory, cfg, workspace)
|
|
107
115
|
except Exception:
|
|
108
116
|
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def boot_workspace(cfg, workspace: str, intel_factory, shadow_factory) -> dict:
|
|
120
|
+
"""Per-WORKSPACE boot phase (W4a boot split, map §6): everything same-
|
|
121
|
+
repo session slots SHARE -- intel + its watcher task, the reversibility
|
|
122
|
+
shadow, and the cached git branch (subprocess.run, off the event loop --
|
|
123
|
+
only /status reads it, recomputing per input line froze the dock).
|
|
124
|
+
Called once per distinct repo root; webbee.slots.WorkspaceResources
|
|
125
|
+
caches the returned bundle so a second slot opened on the SAME root
|
|
126
|
+
reuses it instead of re-booting (re-indexing, a second watcher, a
|
|
127
|
+
second git subprocess)."""
|
|
128
|
+
if cfg.intel_enabled:
|
|
129
|
+
# Guarded off-loop build + watcher; any failure degrades to
|
|
130
|
+
# intel=None, never crashes the boot (start_intel above).
|
|
131
|
+
intel, watcher_task = await start_intel(cfg, workspace, intel_factory)
|
|
132
|
+
else:
|
|
133
|
+
intel, watcher_task = None, None
|
|
134
|
+
shadow = await start_shadow(cfg, workspace, shadow_factory)
|
|
135
|
+
git_branch = await asyncio.to_thread(_git_branch, workspace)
|
|
136
|
+
return {"intel": intel, "watcher_task": watcher_task, "shadow": shadow, "git_branch": git_branch}
|
|
@@ -10,6 +10,10 @@ _HELP = """Commands:
|
|
|
10
10
|
/mode [default|plan|autopilot] consent mode (no arg — show current)
|
|
11
11
|
/cost (=/usage) tokens + credits this session
|
|
12
12
|
/status cwd · git · surface · tokens · version
|
|
13
|
+
/new [path] open a new tab — a session in path (default: cwd)
|
|
14
|
+
/tab N switch to tab N (see /tabs for the numbers)
|
|
15
|
+
/close close the active tab (the run keeps going server-side)
|
|
16
|
+
/tabs list open tabs
|
|
13
17
|
/queue [clear] messages queued while Webbee works (clear drops them all)
|
|
14
18
|
/steps [N] last turn's steps; N expands one (also: Up/Down + Enter)
|
|
15
19
|
/checkpoints the reversibility time machine — list workspace checkpoints
|
|
@@ -80,6 +84,18 @@ def dispatch(line: str, ctx: CommandContext) -> SlashResult:
|
|
|
80
84
|
return SlashResult(handled=True, action="sessions")
|
|
81
85
|
if cmd == "/logout-others":
|
|
82
86
|
return SlashResult(handled=True, action="logout_others")
|
|
87
|
+
if cmd == "/new":
|
|
88
|
+
return SlashResult(handled=True, action="new_tab", arg=(" ".join(args) if args else ""))
|
|
89
|
+
if cmd == "/tab":
|
|
90
|
+
# N's validity depends on how many tabs are OPEN right now — CommandContext
|
|
91
|
+
# doesn't carry the slot list (same split as /sessions revoke <#>, which
|
|
92
|
+
# forwards the raw index and lets the repl validate against its own
|
|
93
|
+
# live state); the repl reports "no such tab" for a bad/missing N.
|
|
94
|
+
return SlashResult(handled=True, action="tab_switch", arg=(args[0] if args else ""))
|
|
95
|
+
if cmd == "/close":
|
|
96
|
+
return SlashResult(handled=True, action="tab_close")
|
|
97
|
+
if cmd == "/tabs":
|
|
98
|
+
return SlashResult(handled=True, action="tabs_list")
|
|
83
99
|
if cmd == "/queue":
|
|
84
100
|
# ctx.queued is the live deque's snapshot (threaded by the repl the
|
|
85
101
|
# same way /status reads session state); the repl clears the actual
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""The Home tab — the dock's new-tab page (W4a Task 6). Slot 0 is always
|
|
2
|
+
Home (`SessionSlot(kind="home", sink=None, agent=None)`, wiring map §1): its
|
|
3
|
+
own pane holds a live, best-effort DASHBOARD instead of a session transcript
|
|
4
|
+
— identity, open tabs, repo intelligence, system state — filled
|
|
5
|
+
asynchronously by `fill_home` so switching to Home (or booting) never blocks
|
|
6
|
+
on a network call. Every print goes through `slot.pane.console` (the SAME
|
|
7
|
+
RecordingConsole every session slot uses — W2 reflow re-centers on resize
|
|
8
|
+
for free, no Home-specific sizing code needed). Zero new server logic: every
|
|
9
|
+
data source below is an EXISTING reader (account.py / intel / checkpoints /
|
|
10
|
+
remote.py / update.py) — this module only arranges what they already return.
|
|
11
|
+
|
|
12
|
+
Rendering is append-only (Rich prints into a StringIO, never edits a
|
|
13
|
+
previous line — see output_pane.py), so "patch tiles as data lands" means
|
|
14
|
+
clear() + a full repaint each time a new piece of data arrives, not
|
|
15
|
+
in-place editing of one region: `fill_home` clears, paints the instant
|
|
16
|
+
`render_skeleton` shell, then re-clears and re-paints the WHOLE dashboard
|
|
17
|
+
(skeleton + whatever tiles have real data so far, "…" for the rest) after
|
|
18
|
+
each async fetch completes — bounded (four repaints total: the instant
|
|
19
|
+
skeleton, then one per async tile) and cheap enough because the pane's own
|
|
20
|
+
records ring is what makes a full repaint affordable at all."""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import os
|
|
25
|
+
import time
|
|
26
|
+
|
|
27
|
+
from rich.align import Align
|
|
28
|
+
from rich.text import Text
|
|
29
|
+
|
|
30
|
+
from webbee.banner_art import WEBBEE_CODE
|
|
31
|
+
from webbee.render import _ACCENT, _BEE
|
|
32
|
+
|
|
33
|
+
HOME_HINT = "type a task — a new tab opens · Ctrl+T here anytime · Alt+N switch"
|
|
34
|
+
_SECTIONS = ("Identity", "Tabs", "Repo", "System")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _mask_email(email: str) -> str:
|
|
38
|
+
"""PII-safe display of an email: only the FIRST character of the local
|
|
39
|
+
part and of the domain survive (`valentin@webhostmost.com` ->
|
|
40
|
+
`v•••@w•••`) — enough to recognize your OWN account at a glance, nothing
|
|
41
|
+
an onlooker could read off a shared screen. Empty input -> empty output;
|
|
42
|
+
a malformed value with no `@` gets the same first-char+mask treatment
|
|
43
|
+
applied to the whole string, so it still can't leak past one character."""
|
|
44
|
+
email = (email or "").strip()
|
|
45
|
+
if not email:
|
|
46
|
+
return ""
|
|
47
|
+
if "@" not in email:
|
|
48
|
+
return f"{email[0]}•••"
|
|
49
|
+
local, _, domain = email.partition("@")
|
|
50
|
+
local_mask = f"{local[0]}•••" if local else "•••"
|
|
51
|
+
domain_mask = f"{domain[0]}•••" if domain else "•••"
|
|
52
|
+
return f"{local_mask}@{domain_mask}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _render_header(console) -> None:
|
|
56
|
+
console.print()
|
|
57
|
+
console.print(Align.center(Text(WEBBEE_CODE, style=f"bold {_BEE}")))
|
|
58
|
+
console.print(Align.center(Text("◆ Home", style=f"bold {_ACCENT}")))
|
|
59
|
+
console.print()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _render_hint(console) -> None:
|
|
63
|
+
console.print()
|
|
64
|
+
console.print(Align.center(Text(HOME_HINT, style="dim")))
|
|
65
|
+
console.print()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def render_skeleton(console, width: int) -> None:
|
|
69
|
+
"""The instant, data-free shell: the logo, `◆ Home`, the four section
|
|
70
|
+
headers (each a dim placeholder line) and the hint. ZERO awaits — called
|
|
71
|
+
synchronously the moment `fill_home` starts, so switching to Home (or
|
|
72
|
+
booting) never shows a blank pane while the async tiles below are still
|
|
73
|
+
in flight. `width` mirrors the other renderers' signatures for parity;
|
|
74
|
+
`Align.center` itself measures the console's LIVE width at print time
|
|
75
|
+
(W2 replay-safe, same vocabulary as `render.RichSink.welcome`), so this
|
|
76
|
+
function has no direct use for it."""
|
|
77
|
+
_render_header(console)
|
|
78
|
+
for name in _SECTIONS:
|
|
79
|
+
console.print(Text(f" {name}", style=f"bold {_BEE}"))
|
|
80
|
+
console.print(Text(" …", style="dim"))
|
|
81
|
+
console.print()
|
|
82
|
+
_render_hint(console)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def render_identity(console, account) -> None:
|
|
86
|
+
"""Nickname prominent, plan next, email masked last — the raw address
|
|
87
|
+
(PII) never prints. `account is None` (not signed in, or the fetch is
|
|
88
|
+
still pending / failed) renders the same neutral placeholder either
|
|
89
|
+
way — Home can't and shouldn't distinguish "loading" from "offline" for
|
|
90
|
+
the person looking at it."""
|
|
91
|
+
console.print(Text(" Identity", style=f"bold {_BEE}"))
|
|
92
|
+
if account is None or not getattr(account, "signed_in", False):
|
|
93
|
+
console.print(Text(" …", style="dim"))
|
|
94
|
+
console.print()
|
|
95
|
+
return
|
|
96
|
+
nickname = getattr(account, "nickname", "") or ""
|
|
97
|
+
headline = f"@{nickname}" if nickname else "(no nickname set)"
|
|
98
|
+
console.print(Text(f" {headline}", style="bold white"))
|
|
99
|
+
plan = getattr(account, "plan", "") or ""
|
|
100
|
+
if plan:
|
|
101
|
+
status = getattr(account, "plan_status", "") or ""
|
|
102
|
+
tag = f" ({status})" if status and status != "active" else ""
|
|
103
|
+
console.print(Text(f" {plan} plan{tag}", style="dim"))
|
|
104
|
+
email = getattr(account, "email", "") or ""
|
|
105
|
+
if email:
|
|
106
|
+
console.print(Text(f" {_mask_email(email)}", style="dim"))
|
|
107
|
+
console.print()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def render_slots_tile(console, slots) -> None:
|
|
111
|
+
"""Every OPEN session tab (Home never lists itself), live status glyphs
|
|
112
|
+
(`slot.status_glyph()`) and the always-true hint for how to get one
|
|
113
|
+
started. `slots` is a `SlotManager` — read live, never a snapshot, so
|
|
114
|
+
this always reflects the tabs that exist right now."""
|
|
115
|
+
console.print(Text(" Tabs", style=f"bold {_BEE}"))
|
|
116
|
+
sessions = [(i, s) for i, s in enumerate(slots.slots) if s.kind == "session"]
|
|
117
|
+
if not sessions:
|
|
118
|
+
console.print(Text(" no tabs open yet", style="dim"))
|
|
119
|
+
else:
|
|
120
|
+
active_idx = slots.active_idx
|
|
121
|
+
for idx, s in sessions:
|
|
122
|
+
marker = "●" if idx == active_idx else "○"
|
|
123
|
+
console.print(Text(f" {marker} {idx} {s.label} {s.status_glyph()}", style="white"))
|
|
124
|
+
console.print(Text(" + Ctrl+T / type to start", style="dim"))
|
|
125
|
+
console.print()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def render_repo_tile(console, profile, branch: str, checkpoints: str | None) -> None:
|
|
129
|
+
"""Repo intelligence for whichever workspace `fill_home` picked (the
|
|
130
|
+
active session tab, or the most recently opened one). `profile` falsy
|
|
131
|
+
covers "no repo yet", "intel disabled" and "the fetch failed" alike —
|
|
132
|
+
all three render the same neutral placeholder; there is nothing actionable
|
|
133
|
+
to tell the difference between them."""
|
|
134
|
+
console.print(Text(" Repo", style=f"bold {_BEE}"))
|
|
135
|
+
if not profile:
|
|
136
|
+
console.print(Text(" …", style="dim"))
|
|
137
|
+
console.print()
|
|
138
|
+
return
|
|
139
|
+
langs = ", ".join(sorted((profile.get("languages") or {}).keys())) or "—"
|
|
140
|
+
files = profile.get("file_count", 0)
|
|
141
|
+
console.print(Text(f" branch {branch or '-'} · {files} files · {langs}", style="white"))
|
|
142
|
+
if checkpoints:
|
|
143
|
+
head = next((ln for ln in checkpoints.splitlines() if ln.strip()), "")
|
|
144
|
+
if head:
|
|
145
|
+
console.print(Text(f" {head}", style="dim"))
|
|
146
|
+
console.print()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def render_system_tile(console, *, remote_desc: str | None, update_notice: str | None) -> None:
|
|
150
|
+
"""Remote-control status (only meaningful once a coding turn has run at
|
|
151
|
+
least once — `fill_home` passes `None` otherwise, a different state from
|
|
152
|
+
"the fetch failed" but rendered the same, since neither is actionable
|
|
153
|
+
from Home) plus a one-line update notice, when there is one."""
|
|
154
|
+
console.print(Text(" System", style=f"bold {_BEE}"))
|
|
155
|
+
line = remote_desc if remote_desc else "remote control: no live session yet"
|
|
156
|
+
console.print(Text(f" {line}", style="dim"))
|
|
157
|
+
if update_notice:
|
|
158
|
+
console.print(Text(f" {update_notice}", style="dim"))
|
|
159
|
+
console.print()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def is_stale(slot, now: float, ttl: float = 300.0) -> bool:
|
|
163
|
+
"""PURE. The switch-to-Home refill gate: a slot that has never been
|
|
164
|
+
filled (`_last_fill` at its `0.0` default) is ALWAYS stale, regardless
|
|
165
|
+
of how small `now` itself is (e.g. moments after process start, when
|
|
166
|
+
`time.monotonic()` hasn't yet ticked past `ttl`) — "never filled" and
|
|
167
|
+
"filled too long ago" both mean the same thing here: go fill it."""
|
|
168
|
+
last = getattr(slot, "_last_fill", 0.0)
|
|
169
|
+
if last <= 0.0:
|
|
170
|
+
return True
|
|
171
|
+
return (now - last) > ttl
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _pick_session_slot(slots):
|
|
175
|
+
"""Which session tab Home's repo/system tiles describe: the ACTIVE slot
|
|
176
|
+
when it's itself a real session (the tab you're actually looking at is
|
|
177
|
+
the most relevant one — true right after boot, when Home's own fill runs
|
|
178
|
+
while the first session tab is active); otherwise the most recently
|
|
179
|
+
OPENED session tab; `None` when no session tab exists at all (every tab
|
|
180
|
+
closed — the kernel run itself survives regardless, browser-tab model)."""
|
|
181
|
+
active = slots.active()
|
|
182
|
+
if active.kind == "session":
|
|
183
|
+
return active
|
|
184
|
+
for s in reversed(slots.slots):
|
|
185
|
+
if s.kind == "session":
|
|
186
|
+
return s
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def fill_home(slot, *, cfg, token_provider, slots, account_fetcher,
|
|
191
|
+
sessions_client, resources, version: str) -> None:
|
|
192
|
+
"""Paint Home's dashboard: the instant skeleton, then each tile
|
|
193
|
+
best-effort, in order (identity, tabs, repo, system) — a full repaint
|
|
194
|
+
after every step so real data replaces the "…" placeholder as it lands
|
|
195
|
+
(append-only console — see the module docstring, no live-editing one
|
|
196
|
+
region). Self-guarded against overlap (`slot._filling`): a stale
|
|
197
|
+
switch-to-Home while a boot fill is still running is a safe no-op, never
|
|
198
|
+
a second concurrent fetch. `slot._last_fill` (monotonic) is stamped on
|
|
199
|
+
the way out regardless of how it went, so a failed run still counts as
|
|
200
|
+
"tried just now" rather than retrying on every single switch.
|
|
201
|
+
`sessions_client` is accepted for interface/signature parity with the
|
|
202
|
+
wiring map's reachable-data-sources list (§5) but unused here — the
|
|
203
|
+
multi-surface "other devices" list is a different concept from this
|
|
204
|
+
tile's OWN dock tabs; a wallet/activity/connections tile is explicitly
|
|
205
|
+
W5 (map §5: no client wrappers exist for those yet)."""
|
|
206
|
+
if slot._filling:
|
|
207
|
+
return
|
|
208
|
+
slot._filling = True
|
|
209
|
+
console = slot.pane.console
|
|
210
|
+
width = console.width
|
|
211
|
+
|
|
212
|
+
account = None
|
|
213
|
+
profile, branch, checkpoints = None, "-", None
|
|
214
|
+
remote_desc, update_notice = None, None
|
|
215
|
+
|
|
216
|
+
def _repaint() -> None:
|
|
217
|
+
try:
|
|
218
|
+
console.clear()
|
|
219
|
+
render_skeleton(console, width)
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
for render_fn, args, kwargs in (
|
|
223
|
+
(render_identity, (account,), {}),
|
|
224
|
+
(render_slots_tile, (slots,), {}),
|
|
225
|
+
(render_repo_tile, (profile, branch, checkpoints), {}),
|
|
226
|
+
(render_system_tile, (), {"remote_desc": remote_desc, "update_notice": update_notice}),
|
|
227
|
+
):
|
|
228
|
+
try:
|
|
229
|
+
render_fn(console, *args, **kwargs)
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
try:
|
|
233
|
+
slot.pane.notify()
|
|
234
|
+
except Exception:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
_repaint() # instant shell — no data yet
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
account = await account_fetcher(cfg, token_provider)
|
|
242
|
+
except Exception:
|
|
243
|
+
account = None
|
|
244
|
+
_repaint()
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
picked = _pick_session_slot(slots)
|
|
248
|
+
ws = picked.workspace if picked is not None else slot.workspace
|
|
249
|
+
bundle = resources.get(ws) or {}
|
|
250
|
+
branch = bundle.get("git_branch", "-")
|
|
251
|
+
intel = bundle.get("intel")
|
|
252
|
+
shadow = bundle.get("shadow")
|
|
253
|
+
if intel is not None:
|
|
254
|
+
try:
|
|
255
|
+
profile = intel.repo_profile()
|
|
256
|
+
except Exception:
|
|
257
|
+
profile = None
|
|
258
|
+
if shadow is not None:
|
|
259
|
+
try:
|
|
260
|
+
checkpoints = await asyncio.to_thread(shadow.describe)
|
|
261
|
+
except Exception:
|
|
262
|
+
checkpoints = None
|
|
263
|
+
except Exception:
|
|
264
|
+
picked = None
|
|
265
|
+
_repaint()
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
sid = getattr(getattr(picked, "agent", None), "session_id", "") if picked is not None else ""
|
|
269
|
+
if sid:
|
|
270
|
+
from webbee import remote as _remote
|
|
271
|
+
try:
|
|
272
|
+
state = await _remote.get_remote(cfg, token_provider, sid)
|
|
273
|
+
remote_desc = _remote.describe(state)
|
|
274
|
+
except Exception:
|
|
275
|
+
remote_desc = None
|
|
276
|
+
except Exception:
|
|
277
|
+
remote_desc = None
|
|
278
|
+
try:
|
|
279
|
+
from pathlib import Path
|
|
280
|
+
|
|
281
|
+
from webbee.update import check_for_update, default_fetch
|
|
282
|
+
cache = Path(os.path.expanduser("~/.cache/webbee/update.json"))
|
|
283
|
+
update_notice = await asyncio.to_thread(
|
|
284
|
+
check_for_update, version, cache_path=cache, now=time.time(), fetch=default_fetch)
|
|
285
|
+
except Exception:
|
|
286
|
+
update_notice = None
|
|
287
|
+
_repaint()
|
|
288
|
+
finally:
|
|
289
|
+
slot._last_fill = time.monotonic()
|
|
290
|
+
slot._filling = False
|