webbee 0.3.11__tar.gz → 0.3.13__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.11 → webbee-0.3.13}/CHANGELOG.md +22 -0
- {webbee-0.3.11 → webbee-0.3.13}/PKG-INFO +1 -1
- {webbee-0.3.11 → webbee-0.3.13}/pyproject.toml +1 -1
- webbee-0.3.13/src/webbee/__init__.py +1 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/commands.py +19 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/frames.py +23 -0
- webbee-0.3.13/src/webbee/queue_panel.py +101 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/render.py +77 -22
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/repl.py +13 -1
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/session.py +15 -8
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/tui.py +132 -38
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_commands.py +34 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_marathon.py +64 -1
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_render.py +128 -6
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_repl.py +11 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_session.py +7 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_tui.py +470 -0
- webbee-0.3.11/src/webbee/__init__.py +0 -1
- {webbee-0.3.11 → webbee-0.3.13}/.github/workflows/publish.yml +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/.gitignore +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/LICENSE +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/README.md +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/install.sh +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/account.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/banner_art.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/boot.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/checkpoints.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/cli.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/clipboard.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/coding_context.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/config.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/consent.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/details.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/events.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/intel/chunker.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/intel/embed.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/intel/graph.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/intel/indexer.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/intel/models.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/intel/query.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/intel/service.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/intel/store.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/intel/watch.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/output_pane.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/remote.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/repo.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/sessions.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/steer.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/stream.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/thread.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/tokens.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/tools.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/src/webbee/update.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/__init__.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_account.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_checkpoints.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_cli.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_clipboard.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_config.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_details.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_events.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_freeze_fix.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_intel_chunker.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_intel_embed.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_intel_graph.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_intel_indexer.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_intel_query.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_intel_service.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_intel_store.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_intel_vectors.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_intel_watch.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_packaging.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_repo.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_sessions.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_steer.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_stream.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_thread.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_tokens.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_tools.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_tui_hardening.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_update.py +0 -0
- {webbee-0.3.11 → webbee-0.3.13}/tests/test_version.py +0 -0
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.13
|
|
4
|
+
|
|
5
|
+
- The message queue is now a live panel above the input — you see exactly
|
|
6
|
+
what's waiting, press ↑ to pull the last one back for editing (it leaves
|
|
7
|
+
the queue and returns when you resend), or click one to edit it. The
|
|
8
|
+
transcript stays clean.
|
|
9
|
+
- Cleaner welcome screen — just the essentials (who you are, your plan, how
|
|
10
|
+
to start) plus a clear word on privacy: your work is never sold and never
|
|
11
|
+
used to train models, and PII is masked before it reaches the model.
|
|
12
|
+
|
|
13
|
+
## 0.3.12
|
|
14
|
+
|
|
15
|
+
- Queued messages are now visible — when you type while Webbee is working,
|
|
16
|
+
your message shows as `⋯ queued: …` right away, the toolbar shows the count,
|
|
17
|
+
and it runs (clearly) after the current turn. New `/queue` and `/queue clear`
|
|
18
|
+
to see and manage what's waiting — they work even mid-turn.
|
|
19
|
+
- Stopping a turn (Esc/Ctrl-C) no longer auto-runs the queue: it stays put,
|
|
20
|
+
visible, until you clear it or a next turn finishes naturally.
|
|
21
|
+
- Task lists now render in full — every to-do with its status (✓ done ·
|
|
22
|
+
▶ doing · ○ next), redrawn on each update, so you always see the whole plan
|
|
23
|
+
and where Webbee is in it.
|
|
24
|
+
|
|
3
25
|
## 0.3.11
|
|
4
26
|
|
|
5
27
|
- Type-ahead queue: send follow-ups while Webbee is working — they queue and
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.13"
|
|
@@ -10,6 +10,7 @@ _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
|
+
/queue [clear] messages queued while Webbee works (clear drops them all)
|
|
13
14
|
/steps [N] last turn's steps; N expands one (also: Up/Down + Enter)
|
|
14
15
|
/checkpoints the reversibility time machine — list workspace checkpoints
|
|
15
16
|
/rollback <ref> restore the workspace to a checkpoint (id, cp-N or N)
|
|
@@ -30,6 +31,7 @@ class CommandContext:
|
|
|
30
31
|
session_tokens: int
|
|
31
32
|
session_credits: int
|
|
32
33
|
git_branch: str
|
|
34
|
+
queued: tuple = () # snapshot of the dock's type-ahead queue, oldest first
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
@dataclass(frozen=True)
|
|
@@ -78,6 +80,23 @@ def dispatch(line: str, ctx: CommandContext) -> SlashResult:
|
|
|
78
80
|
return SlashResult(handled=True, action="sessions")
|
|
79
81
|
if cmd == "/logout-others":
|
|
80
82
|
return SlashResult(handled=True, action="logout_others")
|
|
83
|
+
if cmd == "/queue":
|
|
84
|
+
# ctx.queued is the live deque's snapshot (threaded by the repl the
|
|
85
|
+
# same way /status reads session state); the repl clears the actual
|
|
86
|
+
# deque on action="queue_clear" — the message here already carries
|
|
87
|
+
# the honest drop count.
|
|
88
|
+
if args and args[0].lower() == "clear":
|
|
89
|
+
n = len(ctx.queued)
|
|
90
|
+
return SlashResult(handled=True, action="queue_clear",
|
|
91
|
+
message=(f"Queue cleared ({n} dropped)." if n
|
|
92
|
+
else "Queue is already empty."))
|
|
93
|
+
if not ctx.queued:
|
|
94
|
+
return SlashResult(handled=True, action="queue",
|
|
95
|
+
message="Queue is empty — press Enter while Webbee is working to queue a message.")
|
|
96
|
+
lines = "\n".join(f" {i}. {t}" for i, t in enumerate(ctx.queued, 1))
|
|
97
|
+
return SlashResult(handled=True, action="queue",
|
|
98
|
+
message=(f"Queued ({len(ctx.queued)}) — runs in order, "
|
|
99
|
+
f"one per finished turn:\n{lines}"))
|
|
81
100
|
if cmd == "/steps":
|
|
82
101
|
if args:
|
|
83
102
|
return SlashResult(handled=True, action="step_detail", arg=args[0])
|
|
@@ -250,3 +250,26 @@ def marathon_note(frame: dict) -> str:
|
|
|
250
250
|
head = f"📋 Todos {done}/{total}"
|
|
251
251
|
return f"{head} — now: {current}" if current else head
|
|
252
252
|
return str(frame.get("type", ""))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def render_todo_frame(frame: dict, sink) -> None:
|
|
256
|
+
"""Full-checklist render for a `todo` FACT (I-WEBBEE-CODE-TODO-SCRATCHPAD).
|
|
257
|
+
The kernel republishes the WHOLE list on every todo_write, so route it to
|
|
258
|
+
sink.todos(items, total, done) — the sink redraws the complete current
|
|
259
|
+
checklist inline (Claude-Code-style: the latest full list always sits in
|
|
260
|
+
the scrollback). Guarded like the other optional sink hooks: a minimal
|
|
261
|
+
sink without `todos` degrades to the old one-line marathon_note, and a
|
|
262
|
+
render error must never break the frame loop in session.run()."""
|
|
263
|
+
todos = frame.get("todos") if isinstance(frame.get("todos"), list) else []
|
|
264
|
+
total = frame.get("total", len(todos))
|
|
265
|
+
done = frame.get("completed", 0)
|
|
266
|
+
render = getattr(sink, "todos", None)
|
|
267
|
+
try:
|
|
268
|
+
if render is not None:
|
|
269
|
+
render(todos, total, done)
|
|
270
|
+
else:
|
|
271
|
+
note = getattr(sink, "note", None)
|
|
272
|
+
if note is not None:
|
|
273
|
+
note(marathon_note(frame))
|
|
274
|
+
except Exception:
|
|
275
|
+
pass
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""The LIVE pending-queue panel pinned between the output pane and the input
|
|
2
|
+
box (0.3.13 — replaces the static `⋯ queued:` scrollback echoes, which
|
|
3
|
+
scrolled away, duplicated and never updated). The builders here are PURE
|
|
4
|
+
(unit-tested without an Application); tui.run_session mounts them in a
|
|
5
|
+
ConditionalContainer that occupies ZERO rows while the queue is empty, so
|
|
6
|
+
the empty-queue dock is pixel-identical to before. Item rows carry a
|
|
7
|
+
per-fragment mouse handler (prompt_toolkit 3.0.52 3-tuple fragments,
|
|
8
|
+
verified in venv) firing on MOUSE_UP only — a PASSIVE consumer of the
|
|
9
|
+
clicks that already flow under the ?1000/?1002 button-event tracking; it
|
|
10
|
+
enables nothing at the terminal level, so the 0.3.3 mouse-flood fix
|
|
11
|
+
(?1003 any-event stays off, tui.configure_mouse_modes) is untouched.
|
|
12
|
+
Split out of tui.py to keep both files under the file-size ceiling."""
|
|
13
|
+
|
|
14
|
+
QP_MAX_ITEMS = 5 # newest items shown; deeper queues add one `… +K more` row
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def one_line(text: str, width: int) -> str:
|
|
18
|
+
"""PURE. Collapse whitespace/newlines so a queued item costs EXACTLY one
|
|
19
|
+
panel row, truncating with `…` when it would overflow `width` columns
|
|
20
|
+
(width<=0 = no truncation — headless/unknown terminal)."""
|
|
21
|
+
t = " ".join((text or "").split())
|
|
22
|
+
if width > 0 and len(t) > width:
|
|
23
|
+
t = t[:max(1, width - 1)] + "…"
|
|
24
|
+
return t
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def queue_height(pending) -> int:
|
|
28
|
+
"""PURE. Rows the panel needs: 1 header + one per SHOWN item + one
|
|
29
|
+
`… +K more` row when the queue is deeper than QP_MAX_ITEMS. 0 when empty
|
|
30
|
+
(the ConditionalContainer hides the panel then anyway). The cap keeps the
|
|
31
|
+
output pane dominant on small terminals; the toolbar's `⋯N queued`
|
|
32
|
+
segment stays the truth-teller for the full depth."""
|
|
33
|
+
n = len(pending)
|
|
34
|
+
if not n:
|
|
35
|
+
return 0
|
|
36
|
+
return 1 + min(n, QP_MAX_ITEMS) + (1 if n > QP_MAX_ITEMS else 0)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def pull_item(pending, buf, index: int) -> bool:
|
|
40
|
+
"""The ONE pull-to-edit implementation (serves BOTH the ↑ key — newest,
|
|
41
|
+
index len(pending)-1 — and a panel-row click — that row's index): move
|
|
42
|
+
pending[index] OUT of the queue and into the input buffer for editing,
|
|
43
|
+
cursor at the end. Guards, identical on both paths: a buffer with ANY
|
|
44
|
+
text is never clobbered, and a stale index (the queue drained between
|
|
45
|
+
render and click) is ignored. Returns True when a pull happened (the
|
|
46
|
+
caller invalidates)."""
|
|
47
|
+
if buf.text or not (0 <= index < len(pending)):
|
|
48
|
+
return False
|
|
49
|
+
item = pending[index]
|
|
50
|
+
del pending[index]
|
|
51
|
+
buf.text = item
|
|
52
|
+
buf.cursor_position = len(item)
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _item_handler(pull, index: int):
|
|
57
|
+
"""One row's mouse handler: MOUSE_UP (a click, not a drag/press) pulls
|
|
58
|
+
THAT queued item into the input via `pull(index)`; every other event
|
|
59
|
+
falls through (NotImplemented) so wheel scroll etc. keep today's
|
|
60
|
+
behavior. Mirrors OutputPane._SelectControl's event discipline."""
|
|
61
|
+
def _h(mouse_event):
|
|
62
|
+
from prompt_toolkit.mouse_events import MouseEventType
|
|
63
|
+
if mouse_event.event_type == MouseEventType.MOUSE_UP:
|
|
64
|
+
pull(index)
|
|
65
|
+
return None
|
|
66
|
+
return NotImplemented
|
|
67
|
+
return _h
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def queue_fragments(pending, pull=None, width: int = 0):
|
|
71
|
+
"""PURE builder: the panel as prompt_toolkit formatted text, re-invoked
|
|
72
|
+
every redraw (same live mechanics as the toolbar) so every queue
|
|
73
|
+
add/edit/drain shows at once. Layout, top→bottom = drain order (FIFO —
|
|
74
|
+
the TOP row runs next; the BOTTOM row is the newest, the one ↑ pulls,
|
|
75
|
+
sitting right above the input):
|
|
76
|
+
|
|
77
|
+
⋯ queued (N) · ↑ edit last · click to edit
|
|
78
|
+
… +K more ← only when N > QP_MAX_ITEMS (the OLDEST hide)
|
|
79
|
+
older item ← muted (qp.item)
|
|
80
|
+
newest item ← accent (qp.last)
|
|
81
|
+
|
|
82
|
+
When `pull` is given each item row is a 3-tuple fragment carrying a
|
|
83
|
+
mouse handler that pulls exactly that item (see _item_handler). Empty
|
|
84
|
+
queue → [] (the panel is hidden)."""
|
|
85
|
+
items = list(pending)
|
|
86
|
+
n = len(items)
|
|
87
|
+
if not n:
|
|
88
|
+
return []
|
|
89
|
+
frags = [("class:qp.header", f" ⋯ queued ({n})"),
|
|
90
|
+
("class:qp.item", " · ↑ edit last · click to edit")]
|
|
91
|
+
start = max(0, n - QP_MAX_ITEMS)
|
|
92
|
+
if start:
|
|
93
|
+
frags.append(("class:qp.item", f"\n … +{start} more"))
|
|
94
|
+
for i in range(start, n):
|
|
95
|
+
style = "class:qp.last" if i == n - 1 else "class:qp.item"
|
|
96
|
+
row = "\n " + one_line(items[i], width - 4 if width > 0 else 0)
|
|
97
|
+
if pull is None:
|
|
98
|
+
frags.append((style, row))
|
|
99
|
+
else:
|
|
100
|
+
frags.append((style, row, _item_handler(pull, i)))
|
|
101
|
+
return frags
|
|
@@ -44,6 +44,18 @@ _ICON = {"read_file": "📖", "grep": "🔎", "glob": "🗂️", "write_file": "
|
|
|
44
44
|
_BEE = "yellow" # bee-yellow brand accent — logo / 🐝 / notes / busy dot ONLY
|
|
45
45
|
_ACCENT = "cyan" # interactive chrome ONLY — live caret / mode / panel url
|
|
46
46
|
|
|
47
|
+
# ---- welcome copy (single source of truth for the splash + its tests) ------
|
|
48
|
+
# The privacy promise is claims-disciplined: every clause is TRUE and enforced
|
|
49
|
+
# today — user data is never sold and never used to train any model (ours or
|
|
50
|
+
# third-party; documented in the DPA), and PII is masked before tool output
|
|
51
|
+
# re-enters the model (platform-enforced and covered by the platform's own
|
|
52
|
+
# test suite). Deliberately NOT said: "we collect nothing" (sessions ARE
|
|
53
|
+
# stored so Webbee can resume your work) and "guaranteed" (we say what is
|
|
54
|
+
# enforced, not what is promised). Change the wording only with a source.
|
|
55
|
+
WELCOME_PRIVACY = "🔒 Your work stays yours — never sold, never training data."
|
|
56
|
+
WELCOME_PRIVACY_DETAIL = "PII is masked before it reaches the model."
|
|
57
|
+
WELCOME_HINT = "Type a task — Webbee runs it to completion · /help · Ctrl-D to exit"
|
|
58
|
+
|
|
47
59
|
|
|
48
60
|
def _fmt_tokens(n: int) -> str:
|
|
49
61
|
"""Compact count for the live toolbar: 900 -> '900', 2_100 -> '2.1k',
|
|
@@ -132,10 +144,13 @@ class RichSink:
|
|
|
132
144
|
|
|
133
145
|
# ---- welcome ------------------------------------------------------------
|
|
134
146
|
def welcome(self, account, cwd: str, surface: str) -> None:
|
|
135
|
-
"""One-time launch splash
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
147
|
+
"""One-time launch splash, trimmed to what a human actually needs:
|
|
148
|
+
the WEBBEE CODE logo (the brand), ONE identity line (who am I / plan —
|
|
149
|
+
plan status shows only when it needs attention), the privacy promise
|
|
150
|
+
(true, enforced claims only — see WELCOME_PRIVACY above), and one hint
|
|
151
|
+
line to get going. Runs BEFORE the dock starts. Clears the screen ONLY
|
|
152
|
+
in the non-pane path (the full-screen dock owns its own alternate
|
|
153
|
+
screen — clearing there would corrupt it)."""
|
|
139
154
|
if self._on_output is None:
|
|
140
155
|
self.console.clear()
|
|
141
156
|
w = self.console.width
|
|
@@ -152,29 +167,24 @@ class RichSink:
|
|
|
152
167
|
self.console.print(Text("ICNLI AI Cloud OS · Agent".center(w), style=f"bold {_ACCENT}"))
|
|
153
168
|
self.console.print(Text("· i m p e r a l . i o ·".center(w), style="dim"))
|
|
154
169
|
self.console.print()
|
|
155
|
-
rows = []
|
|
156
170
|
if account.signed_in:
|
|
157
|
-
who = account.email
|
|
158
|
-
|
|
171
|
+
who = account.email or ""
|
|
172
|
+
if account.nickname:
|
|
173
|
+
who += f" · @{account.nickname}"
|
|
159
174
|
if account.plan:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
rows.append(("Member since", account.member_since))
|
|
175
|
+
who += f" · {account.plan} plan"
|
|
176
|
+
if account.plan_status and account.plan_status != "active":
|
|
177
|
+
who += f" ({account.plan_status})"
|
|
178
|
+
label = "Signed in as "
|
|
179
|
+
pad = " " * max(0, (w - len(label) - len(who)) // 2)
|
|
180
|
+
self.console.print(Text.assemble((pad + label, "dim"), (who, "white")))
|
|
167
181
|
else:
|
|
168
|
-
|
|
169
|
-
bw = max((len(label.ljust(14) + value) for label, value in rows), default=0)
|
|
170
|
-
pad = " " * max(0, (w - bw) // 2)
|
|
171
|
-
for label, value in rows:
|
|
172
|
-
self.console.print(Text.assemble((pad + label.ljust(14), "dim"), (value, "white")))
|
|
182
|
+
self.console.print(Text("not signed in — /login".center(w), style="dim"))
|
|
173
183
|
self.console.print()
|
|
174
|
-
|
|
175
|
-
self.console.print(Text(
|
|
184
|
+
self.console.print(Text(WELCOME_PRIVACY.center(w), style="white"))
|
|
185
|
+
self.console.print(Text(WELCOME_PRIVACY_DETAIL.center(w), style="dim"))
|
|
176
186
|
self.console.print()
|
|
177
|
-
self.console.print(Text(
|
|
187
|
+
self.console.print(Text(WELCOME_HINT.center(w), style="dim"))
|
|
178
188
|
self.console.print()
|
|
179
189
|
self._nudge()
|
|
180
190
|
|
|
@@ -215,6 +225,40 @@ class RichSink:
|
|
|
215
225
|
self.console.print(_pad(Text(_clean(message), style=_BEE)))
|
|
216
226
|
self._nudge()
|
|
217
227
|
|
|
228
|
+
def todos(self, items: list, total: int, done: int) -> None:
|
|
229
|
+
"""Claude-Code-style checklist for the coding TODO scratchpad. The
|
|
230
|
+
kernel republishes the FULL list on every todo_write, so each update
|
|
231
|
+
re-renders the whole current plan inline — the latest complete list
|
|
232
|
+
always sits in the scrollback (deliberately NOT a pinned sticky panel:
|
|
233
|
+
inline re-render can't fight the dock layout). Order preserved, one
|
|
234
|
+
line per item: ✓ completed (dim, struck) · ▶ in progress (bold, so
|
|
235
|
+
"what's happening now" pops) · ○ pending (muted). Defensive: malformed
|
|
236
|
+
items are skipped, unknown statuses render as pending, bad counts
|
|
237
|
+
degrade to a bare header — never raises."""
|
|
238
|
+
try:
|
|
239
|
+
head = f"📋 Todos ({int(done)}/{int(total)})"
|
|
240
|
+
except (TypeError, ValueError):
|
|
241
|
+
head = "📋 Todos"
|
|
242
|
+
body = Text() # empty base style — segments below
|
|
243
|
+
body.append(head, style=f"bold {_BEE}") # carry their OWN, no bleed-through
|
|
244
|
+
for item in (items if isinstance(items, (list, tuple)) else ()):
|
|
245
|
+
if not isinstance(item, dict):
|
|
246
|
+
continue # malformed entry — skip, never raise
|
|
247
|
+
content = _clean(item.get("content", "")).strip()
|
|
248
|
+
if not content:
|
|
249
|
+
continue
|
|
250
|
+
status = str(item.get("status", "") or "")
|
|
251
|
+
if status == "completed":
|
|
252
|
+
glyph, g_style, t_style = "✓", "green", "dim strike"
|
|
253
|
+
elif status == "in_progress":
|
|
254
|
+
glyph, g_style, t_style = "▶", f"bold {_BEE}", "bold"
|
|
255
|
+
else: # pending / unknown -> not started yet
|
|
256
|
+
glyph, g_style, t_style = "○", "grey66", "grey66"
|
|
257
|
+
body.append(f"\n {glyph} ", style=g_style)
|
|
258
|
+
body.append(content, style=t_style)
|
|
259
|
+
self.console.print(_pad(body))
|
|
260
|
+
self._nudge()
|
|
261
|
+
|
|
218
262
|
def user_echo(self, text: str) -> None:
|
|
219
263
|
"""Commit the just-sent user message as its own clearly-readable line
|
|
220
264
|
with a background bar (NOT boxed like the live input) so it stands out
|
|
@@ -222,6 +266,17 @@ class RichSink:
|
|
|
222
266
|
self.console.print(_pad(Text(" ❯ " + _clean(text) + " ", style="bold white on grey30")))
|
|
223
267
|
self._nudge()
|
|
224
268
|
|
|
269
|
+
def queued_run(self, remaining: int) -> None:
|
|
270
|
+
"""The tiny lifecycle marker printed right before a drained queued
|
|
271
|
+
line starts: `▶ running queued message` (+ how many still wait) — a
|
|
272
|
+
drain is never a silent start; the drained text's normal ❯ user_echo
|
|
273
|
+
follows immediately."""
|
|
274
|
+
tail = f" · {remaining} still queued" if remaining else ""
|
|
275
|
+
self.console.print(_pad(Text.assemble(
|
|
276
|
+
("▶ ", f"bold {_BEE}"),
|
|
277
|
+
(f"running queued message{tail}", "italic grey66"))))
|
|
278
|
+
self._nudge()
|
|
279
|
+
|
|
225
280
|
def foreign_turn(self, surface: str, role: str, text: str) -> None:
|
|
226
281
|
"""One tagged line for a turn that lives on ANOTHER surface (a
|
|
227
282
|
Telegram/panel-steered turn on the shared stream, or a replayed prior
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import contextlib
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
|
+
from collections import deque
|
|
5
6
|
|
|
6
7
|
from webbee import __version__
|
|
7
8
|
from webbee.account import login_device_flow
|
|
@@ -80,6 +81,10 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
|
|
|
80
81
|
# inject sink/read_line and take the plain fallback loop.
|
|
81
82
|
use_dock = sink is None and read_line is input and sys.stdin.isatty()
|
|
82
83
|
state = {"mode": mode, "logged_in": False}
|
|
84
|
+
# Type-ahead queue: OWNED here and shared with tui.run_session, so /queue
|
|
85
|
+
# and /queue clear (dispatched through CommandContext.queued, same
|
|
86
|
+
# mechanism /status uses for session state) see the live deque.
|
|
87
|
+
pending_queue: deque = deque()
|
|
83
88
|
_sink = None # assigned by _boot
|
|
84
89
|
agent = None # assigned by _boot
|
|
85
90
|
intel = None # assigned by _boot -- IntelService, or None (off/base-install/boot failure)
|
|
@@ -96,7 +101,8 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
|
|
|
96
101
|
surface="terminal", logged_in=state["logged_in"],
|
|
97
102
|
session_tokens=getattr(_sink, "session_tokens", 0),
|
|
98
103
|
session_credits=getattr(_sink, "session_credits", 0),
|
|
99
|
-
git_branch=state.get("git_branch", "-")
|
|
104
|
+
git_branch=state.get("git_branch", "-"),
|
|
105
|
+
queued=tuple(pending_queue))
|
|
100
106
|
|
|
101
107
|
async def _handle(line: str) -> str:
|
|
102
108
|
"""Process one input line. Returns 'exit' or 'continue'."""
|
|
@@ -198,6 +204,11 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
|
|
|
198
204
|
except Exception as e:
|
|
199
205
|
_sink.note(f"Remote control unavailable: {type(e).__name__}")
|
|
200
206
|
return "continue"
|
|
207
|
+
if res.action == "queue_clear":
|
|
208
|
+
# dispatch built the message (with the drop count) from the
|
|
209
|
+
# ctx snapshot; here we drop the live deque — the toolbar
|
|
210
|
+
# count follows on the sink's redraw below.
|
|
211
|
+
pending_queue.clear()
|
|
201
212
|
if res.action == "clear":
|
|
202
213
|
_sink.clear()
|
|
203
214
|
_sink.note(res.message)
|
|
@@ -314,6 +325,7 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
|
|
|
314
325
|
"expand": lambda i: _handle(f"/steps {i + 1}"),
|
|
315
326
|
},
|
|
316
327
|
stop_turn=lambda: agent.stop(),
|
|
328
|
+
pending=pending_queue, queued_run=_sink.queued_run,
|
|
317
329
|
)
|
|
318
330
|
finally:
|
|
319
331
|
_cancel_background()
|
|
@@ -15,6 +15,7 @@ from webbee.frames import (
|
|
|
15
15
|
handle_tool_request,
|
|
16
16
|
marathon_note,
|
|
17
17
|
render_foreign_frame,
|
|
18
|
+
render_todo_frame,
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
|
|
@@ -226,14 +227,20 @@ class AgentSession:
|
|
|
226
227
|
elif ftype == "marathon_complete": # U4 — the whole GOAL is done: terminal
|
|
227
228
|
return frame.get("text", "")
|
|
228
229
|
|
|
229
|
-
elif ftype in _MARATHON_FACT_TYPES: # U4 — marathon plan/milestone/pause
|
|
230
|
-
# Facts-only
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
230
|
+
elif ftype in _MARATHON_FACT_TYPES: # U4 — marathon plan/milestone/pause/todo
|
|
231
|
+
# Facts-only. A `todo` fact carries the FULL list -> the
|
|
232
|
+
# dedicated full-checklist render (falls back to the old
|
|
233
|
+
# one-line note on a minimal sink). The other facts render
|
|
234
|
+
# ONE human-readable line. Guarded: a minimal sink (no
|
|
235
|
+
# `note`) simply drops the fact rather than crashing the
|
|
236
|
+
# turn (the stream reader already tolerates unknown frame
|
|
237
|
+
# types by ignoring them).
|
|
238
|
+
if ftype == "todo":
|
|
239
|
+
render_todo_frame(frame, sink)
|
|
240
|
+
else:
|
|
241
|
+
_note = getattr(sink, "note", None)
|
|
242
|
+
if _note is not None:
|
|
243
|
+
_note(marathon_note(frame))
|
|
237
244
|
if ftype == "marathon_paused":
|
|
238
245
|
# Parked (out-of-credits / consent / runaway) -> end the
|
|
239
246
|
# turn so the dock leaves "working"; the note shows why, and
|