webbee 0.3.14__tar.gz → 0.3.16__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.14 → webbee-0.3.16}/CHANGELOG.md +14 -0
- {webbee-0.3.14 → webbee-0.3.16}/PKG-INFO +1 -1
- {webbee-0.3.14 → webbee-0.3.16}/pyproject.toml +1 -1
- webbee-0.3.16/src/webbee/__init__.py +1 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/render.py +93 -18
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/repl.py +37 -2
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/session.py +11 -10
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/thread.py +23 -1
- webbee-0.3.16/src/webbee/todo_panel.py +91 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/tui.py +114 -22
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_packaging.py +2 -1
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_render.py +131 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_repl.py +82 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_session.py +17 -10
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_thread.py +71 -0
- webbee-0.3.16/tests/test_todo_panel.py +87 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_tui.py +222 -0
- webbee-0.3.14/src/webbee/__init__.py +0 -1
- {webbee-0.3.14 → webbee-0.3.16}/.github/workflows/publish.yml +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/.gitignore +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/LICENSE +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/README.md +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/install.sh +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/account.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/banner_art.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/boot.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/checkpoints.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/cli.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/clipboard.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/coding_context.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/commands.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/config.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/consent.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/details.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/events.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/frames.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/intel/chunker.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/intel/embed.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/intel/graph.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/intel/indexer.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/intel/models.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/intel/query.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/intel/service.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/intel/store.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/intel/watch.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/output_pane.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/queue_panel.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/remote.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/repo.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/sessions.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/steer.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/stream.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/tokens.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/tools.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/src/webbee/update.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/__init__.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_account.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_checkpoints.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_cli.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_clipboard.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_commands.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_config.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_details.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_events.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_freeze_fix.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_intel_chunker.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_intel_embed.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_intel_graph.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_intel_indexer.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_intel_query.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_intel_service.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_intel_store.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_intel_vectors.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_intel_watch.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_marathon.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_repo.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_sessions.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_steer.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_stream.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_tokens.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_tools.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_tui_hardening.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_update.py +0 -0
- {webbee-0.3.14 → webbee-0.3.16}/tests/test_version.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.16
|
|
4
|
+
|
|
5
|
+
- A message typed while Webbee is working now shows in the queue panel
|
|
6
|
+
exactly once: the queue reconciles every entry by its id, so a retried
|
|
7
|
+
delivery or a send that only looked failed no longer produces a duplicate
|
|
8
|
+
row.
|
|
9
|
+
|
|
10
|
+
## 0.3.15
|
|
11
|
+
|
|
12
|
+
- Type a follow-up while Webbee is working and it now flies into the current
|
|
13
|
+
run within seconds (not after it finishes), shown in the queue tagged by
|
|
14
|
+
where it came from. The task list is now a pinned panel that stays visible
|
|
15
|
+
and updates live.
|
|
16
|
+
|
|
3
17
|
## 0.3.14
|
|
4
18
|
|
|
5
19
|
- See queued messages from Telegram/panel right in the terminal queue
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.16"
|
|
@@ -139,6 +139,21 @@ class RichSink:
|
|
|
139
139
|
# queue panel reads THIS list object every redraw — mutate in place
|
|
140
140
|
# only (append/del/clear), never rebind.
|
|
141
141
|
self.remote_pending: list = []
|
|
142
|
+
# The repl's OWN type-ahead deque (the SAME object tui mutates),
|
|
143
|
+
# shared by reference at boot. remote_queued reconciles against it:
|
|
144
|
+
# a kernel task_queued echo carrying a local QueuedLine's steer_iid
|
|
145
|
+
# is positive proof the inject landed — the local twin is removed so
|
|
146
|
+
# one message is one row with ONE owner (the kernel). None until the
|
|
147
|
+
# repl wires it (minimal sinks / unit tests without a queue).
|
|
148
|
+
self.local_pending = None
|
|
149
|
+
# The current coding checklist (sticky todo panel, 0.3.15) — the twin
|
|
150
|
+
# of remote_pending: todos() replaces its contents in place on every
|
|
151
|
+
# todo frame; the dock's todo panel reads THIS list object every
|
|
152
|
+
# redraw. Sticky: it persists across turn ends (the panel stays up)
|
|
153
|
+
# and clears only on /clear.
|
|
154
|
+
self.current_todos: list = []
|
|
155
|
+
self._todo_counts = (0, 0) # kernel-reported (done, total)
|
|
156
|
+
self._todos_dirty = False # a turn touched the list -> end_turn records it
|
|
142
157
|
|
|
143
158
|
def _nudge(self) -> None:
|
|
144
159
|
"""After output/state changes: let the pane follow the tail + redraw."""
|
|
@@ -213,6 +228,14 @@ class RichSink:
|
|
|
213
228
|
# would linger as phantoms. Clear them (in place — the panel holds
|
|
214
229
|
# this list object).
|
|
215
230
|
self.remote_pending.clear()
|
|
231
|
+
# Sticky-todo scrollback record (0.3.15): in the dock the live panel
|
|
232
|
+
# replaced the inline re-renders, so print the FINAL checklist state
|
|
233
|
+
# ONCE per turn that touched it — the transcript keeps the history
|
|
234
|
+
# while the panel itself persists (sticky) into idle.
|
|
235
|
+
if self._on_output is not None and self._todos_dirty and self.current_todos:
|
|
236
|
+
done, total = self._todo_counts
|
|
237
|
+
self._todos_inline(self.current_todos, total, done)
|
|
238
|
+
self._todos_dirty = False
|
|
216
239
|
final_text = _clean(final_text)
|
|
217
240
|
if final_text:
|
|
218
241
|
self.console.print() # separation before the focus block
|
|
@@ -237,15 +260,41 @@ class RichSink:
|
|
|
237
260
|
self._nudge()
|
|
238
261
|
|
|
239
262
|
def todos(self, items: list, total: int, done: int) -> None:
|
|
240
|
-
"""
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
263
|
+
"""Checklist state for the coding TODO scratchpad. The kernel
|
|
264
|
+
republishes the FULL list on every todo_write; each update replaces
|
|
265
|
+
`current_todos` IN PLACE (the dock's sticky todo panel holds this
|
|
266
|
+
list object and re-reads it every redraw — 0.3.15, no more inline
|
|
267
|
+
scroll-away in the dock; end_turn prints the final checklist ONCE as
|
|
268
|
+
the scrollback record). Headless / non-dock sinks (no on_output pane)
|
|
269
|
+
keep today's full inline render. Defensive both ways: malformed items
|
|
270
|
+
are skipped, bad counts degrade, never raises."""
|
|
271
|
+
rows = []
|
|
272
|
+
for item in (items if isinstance(items, (list, tuple)) else ()):
|
|
273
|
+
if not isinstance(item, dict):
|
|
274
|
+
continue # malformed entry — skip, never raise
|
|
275
|
+
content = _clean(item.get("content", "")).strip()
|
|
276
|
+
if not content:
|
|
277
|
+
continue
|
|
278
|
+
rows.append({"content": content,
|
|
279
|
+
"status": str(item.get("status", "") or "")})
|
|
280
|
+
self.current_todos[:] = rows # in place — the panel holds this list
|
|
281
|
+
try:
|
|
282
|
+
self._todo_counts = (int(done), int(total))
|
|
283
|
+
except (TypeError, ValueError):
|
|
284
|
+
self._todo_counts = (sum(1 for r in rows if r["status"] == "completed"),
|
|
285
|
+
len(rows))
|
|
286
|
+
self._todos_dirty = True
|
|
287
|
+
if self._on_output is not None: # dock: the sticky panel renders it live
|
|
288
|
+
self._nudge()
|
|
289
|
+
return
|
|
290
|
+
self._todos_inline(items, total, done)
|
|
291
|
+
|
|
292
|
+
def _todos_inline(self, items, total, done) -> None:
|
|
293
|
+
"""The Claude-Code-style INLINE checklist render (the pre-0.3.15
|
|
294
|
+
behavior, verbatim): one line per item — ✓ completed (dim, struck) ·
|
|
295
|
+
▶ in progress (bold, so "what's happening now" pops) · ○ pending
|
|
296
|
+
(muted), order preserved. Used by the headless/non-dock path on every
|
|
297
|
+
todo frame and by end_turn's one-shot scrollback record in the dock."""
|
|
249
298
|
try:
|
|
250
299
|
head = f"📋 Todos ({int(done)}/{int(total)})"
|
|
251
300
|
except (TypeError, ValueError):
|
|
@@ -283,25 +332,46 @@ class RichSink:
|
|
|
283
332
|
queue panel the instant it queues — tagged `[origin]`, "as if typed".
|
|
284
333
|
DISPLAY-ONLY: it renders above the local rows but is never pullable
|
|
285
334
|
(↑/click) — the kernel owns it; only a `task_dequeued` (or turn end)
|
|
286
|
-
removes it.
|
|
335
|
+
removes it. This is the ONE reconciliation chokepoint (0.3.16): the
|
|
336
|
+
steer_iid minted at enqueue time dedups the display too — a frame
|
|
337
|
+
delivered twice (publish retry / SSE resume) never doubles a row, and
|
|
338
|
+
the echo of a terminal-injected line whose POST merely LOOKED failed
|
|
339
|
+
removes its local fallback twin (the echo is positive proof it
|
|
340
|
+
landed; kernel-owned = one row). Empty iid (legacy kernels) always
|
|
341
|
+
appends — text is never a dedup key."""
|
|
342
|
+
iid = str(iid or "")
|
|
343
|
+
if iid:
|
|
344
|
+
if any(r.get("iid") == iid for r in self.remote_pending):
|
|
345
|
+
self._nudge()
|
|
346
|
+
return
|
|
347
|
+
lp = self.local_pending
|
|
348
|
+
if lp is not None:
|
|
349
|
+
twin = next((q for q in lp if getattr(q, "iid", "") == iid), None)
|
|
350
|
+
if twin is not None:
|
|
351
|
+
try:
|
|
352
|
+
lp.remove(twin)
|
|
353
|
+
except ValueError:
|
|
354
|
+
pass
|
|
287
355
|
self.remote_pending.append({"origin": _clean(str(origin or "")),
|
|
288
356
|
"text": _clean(str(text or "")),
|
|
289
|
-
"iid":
|
|
357
|
+
"iid": iid})
|
|
290
358
|
self._nudge()
|
|
291
359
|
|
|
292
360
|
def remote_dequeued(self, origin: str, iid: str) -> None:
|
|
293
361
|
"""The kernel drained (or dedup-dropped) one queued item
|
|
294
|
-
(`task_dequeued` frame): remove its panel row
|
|
295
|
-
|
|
296
|
-
FIFO, so
|
|
297
|
-
|
|
298
|
-
|
|
362
|
+
(`task_dequeued` frame): remove its panel row by `iid`. The
|
|
363
|
+
oldest-same-origin fallback is LEGACY-ONLY (empty iid — the kernel
|
|
364
|
+
queue is FIFO, so the oldest row of that origin is the one that just
|
|
365
|
+
started): a NON-empty unmatched iid is a no-op, because the kernel
|
|
366
|
+
emits one dequeue per pop INCLUDING dedup-dropped twins, and a twin's
|
|
367
|
+
second dequeue must never eat a different same-origin row. Nothing
|
|
368
|
+
matched → no-op (the row was already cleared or never announced)."""
|
|
299
369
|
iid = str(iid or "")
|
|
300
370
|
origin = _clean(str(origin or ""))
|
|
301
371
|
rows = self.remote_pending
|
|
302
372
|
idx = next((i for i, r in enumerate(rows) if iid and r.get("iid") == iid),
|
|
303
373
|
None)
|
|
304
|
-
if idx is None:
|
|
374
|
+
if idx is None and not iid:
|
|
305
375
|
idx = next((i for i, r in enumerate(rows) if r.get("origin") == origin),
|
|
306
376
|
None)
|
|
307
377
|
if idx is None:
|
|
@@ -346,7 +416,10 @@ class RichSink:
|
|
|
346
416
|
self._nudge()
|
|
347
417
|
|
|
348
418
|
def clear(self) -> None:
|
|
349
|
-
"""/clear: wipe the pane/screen + reset the session counters.
|
|
419
|
+
"""/clear: wipe the pane/screen + reset the session counters. The
|
|
420
|
+
sticky todo panel resets with it (in place — the panel holds the
|
|
421
|
+
list object); a mid-run abort deliberately does NOT (the last known
|
|
422
|
+
plan state is still true and the panel is always-on)."""
|
|
350
423
|
self.console.clear()
|
|
351
424
|
self.tokens = 0
|
|
352
425
|
self.credits = 0
|
|
@@ -354,6 +427,8 @@ class RichSink:
|
|
|
354
427
|
self.session_credits = 0
|
|
355
428
|
self._tools = 0
|
|
356
429
|
self._current = ""
|
|
430
|
+
self.current_todos.clear()
|
|
431
|
+
self._todos_dirty = False
|
|
357
432
|
self._nudge()
|
|
358
433
|
|
|
359
434
|
def abort(self) -> None:
|
|
@@ -54,6 +54,30 @@ async def run_marathon(cfg, mode: str, goal: str, *, sink=None, auth=None,
|
|
|
54
54
|
return text
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
async def _inject_via_gateway(cfg, token_provider, agent, sink,
|
|
58
|
+
text: str, steer_iid: str) -> bool:
|
|
59
|
+
"""The gateway leg of the dock's Enter-while-busy fly-in (mid-turn inject,
|
|
60
|
+
0.3.15): POST the line straight into the agent's LIVE running session so
|
|
61
|
+
the marathon absorbs it at the next brain step (seconds), instead of
|
|
62
|
+
holding it client-side until the turn ends. On ok the line is kernel-owned
|
|
63
|
+
— the ❯ echo records it as sent and the kernel's task_queued[terminal]
|
|
64
|
+
echo drives the panel row. False on ANY failure (no live session yet,
|
|
65
|
+
network, auth, gateway refusal) — the dock then falls back to today's
|
|
66
|
+
local type-ahead queue (tui._inject_or_queue), carrying the same iid so
|
|
67
|
+
the kernel ring dedups a twin. Module-level so tests drive it directly."""
|
|
68
|
+
sid = getattr(agent, "session_id", "")
|
|
69
|
+
if not sid:
|
|
70
|
+
return False
|
|
71
|
+
try:
|
|
72
|
+
from webbee.thread import inject_to_session
|
|
73
|
+
ok = await inject_to_session(cfg, token_provider, sid, text, steer_iid)
|
|
74
|
+
except Exception:
|
|
75
|
+
return False
|
|
76
|
+
if ok:
|
|
77
|
+
sink.user_echo(text) # the transcript records the message as sent
|
|
78
|
+
return ok
|
|
79
|
+
|
|
80
|
+
|
|
57
81
|
async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None, read_line=input,
|
|
58
82
|
agent_factory=None, auth=None, account_fetcher=None,
|
|
59
83
|
sessions_client=None, intel_factory=None, shadow_factory=None) -> None:
|
|
@@ -220,9 +244,12 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
|
|
|
220
244
|
_sink.note(res.message)
|
|
221
245
|
return "continue"
|
|
222
246
|
|
|
223
|
-
# A task for the agent.
|
|
247
|
+
# A task for the agent. A drained type-ahead line minted for a
|
|
248
|
+
# failed mid-turn inject still carries its steer_iid (tui.QueuedLine)
|
|
249
|
+
# -- thread it so the kernel's dedup ring drops the twin if the
|
|
250
|
+
# inject actually landed server-side (a plain typed line has none).
|
|
224
251
|
_sink.user_echo(line)
|
|
225
|
-
await _run_turn(line)
|
|
252
|
+
await _run_turn(line, steer_iid=getattr(line, "iid", ""))
|
|
226
253
|
return "continue"
|
|
227
254
|
|
|
228
255
|
async def _run_turn(line: str, surface: str = "", steer_iid: str = "") -> None:
|
|
@@ -317,6 +344,11 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
|
|
|
317
344
|
async def _boot(s) -> None:
|
|
318
345
|
nonlocal _sink, agent, intel, watcher_task, shadow, steer_task
|
|
319
346
|
_sink = s
|
|
347
|
+
# Queue-panel single-source dedup (0.3.16): hand the sink the SAME
|
|
348
|
+
# type-ahead deque tui mutates, so a kernel task_queued echo can
|
|
349
|
+
# promote a landed local twin (matched by steer_iid) into the one
|
|
350
|
+
# kernel-owned row. Reference share — never a copy.
|
|
351
|
+
s.local_pending = pending_queue
|
|
320
352
|
# Cache git branch OFF the event loop (subprocess.run blocks it). Only
|
|
321
353
|
# /status reads it; recomputing it per input line froze the dock.
|
|
322
354
|
state["git_branch"] = await asyncio.to_thread(_git_branch, workspace)
|
|
@@ -382,6 +414,9 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
|
|
|
382
414
|
stop_turn=lambda: agent.stop(),
|
|
383
415
|
pending=pending_queue, queued_run=_sink.queued_run,
|
|
384
416
|
remote_pending=getattr(_sink, "remote_pending", None),
|
|
417
|
+
todos=getattr(_sink, "current_todos", None),
|
|
418
|
+
inject=lambda text, iid: _inject_via_gateway(
|
|
419
|
+
cfg, token_provider, agent, _sink, text, iid),
|
|
385
420
|
)
|
|
386
421
|
finally:
|
|
387
422
|
_cancel_background()
|
|
@@ -226,19 +226,20 @@ class AgentSession:
|
|
|
226
226
|
|
|
227
227
|
elif ftype in ("task_queued", "task_dequeued"):
|
|
228
228
|
# Full-queue-layer K1: a follow-up queued into the RUNNING
|
|
229
|
-
# kernel session
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
#
|
|
229
|
+
# kernel session shows in the live queue panel the instant
|
|
230
|
+
# it queues (tagged by origin) and leaves the panel when
|
|
231
|
+
# the kernel drains it. These frames carry NO task_id
|
|
232
|
+
# (they belong to the session, not a turn), so the C7
|
|
233
|
+
# filter never eats them; getattr-guarded like todos/
|
|
234
|
+
# queued_run — a minimal sink drops them, a render error
|
|
235
|
+
# never breaks the loop. Terminal-origin rows render TOO
|
|
236
|
+
# (mid-turn inject, 0.3.15): an injected line never sits
|
|
237
|
+
# in the LOCAL panel — the kernel's echo is its only row,
|
|
238
|
+
# and task_dequeued clears it when the turn absorbs it.
|
|
238
239
|
_origin = str(frame.get("origin", "") or "")
|
|
239
240
|
_hook = getattr(sink, "remote_queued" if ftype == "task_queued"
|
|
240
241
|
else "remote_dequeued", None)
|
|
241
|
-
if _hook is not None
|
|
242
|
+
if _hook is not None:
|
|
242
243
|
_iid = str(frame.get("steer_iid", "") or "")
|
|
243
244
|
try:
|
|
244
245
|
if ftype == "task_queued":
|
|
@@ -8,7 +8,8 @@ itself -- `_boot` wraps the whole replay in one try/except so a network
|
|
|
8
8
|
failure never blocks/delays boot beyond the timeout, it just skips the
|
|
9
9
|
replay (same division of labor as remote.py + the /notify call site).
|
|
10
10
|
Also home to the /thread endpoint's pending-steer sibling read (liveness v2
|
|
11
|
-
§B) -- the drain webbee.steer polls while the REPL is idle
|
|
11
|
+
§B) -- the drain webbee.steer polls while the REPL is idle -- and the
|
|
12
|
+
mid-turn inject POST (0.3.15) the dock fires on Enter-while-busy."""
|
|
12
13
|
from __future__ import annotations
|
|
13
14
|
|
|
14
15
|
_DISPLAY_LIMIT = 400
|
|
@@ -67,6 +68,27 @@ async def fetch_pending_steer(cfg, token_provider, session_id: str) -> dict:
|
|
|
67
68
|
return r.json() or {}
|
|
68
69
|
|
|
69
70
|
|
|
71
|
+
async def inject_to_session(cfg, token_provider, session_id: str, text: str,
|
|
72
|
+
steer_iid: str) -> bool:
|
|
73
|
+
"""Mid-turn inject (0.3.15): POST an Enter-while-busy line straight into
|
|
74
|
+
the user's OWN running session — `/v1/agent/sessions/{id}/inject`, body
|
|
75
|
+
`{text, steer_iid}`. The gateway signals a task_id-LESS new_task, so the
|
|
76
|
+
kernel's mid-turn fly-in absorbs it at the next brain step under the
|
|
77
|
+
running turn's own task_id (frames stay visible in this terminal), and
|
|
78
|
+
the given steer_iid rides the kernel's dedup ring. Returns True only when
|
|
79
|
+
the gateway accepted it ({ok: true}). Non-swallowing like its siblings
|
|
80
|
+
above — the repl wiring wraps it and falls back to the local type-ahead
|
|
81
|
+
queue on any failure."""
|
|
82
|
+
import httpx
|
|
83
|
+
token = await token_provider()
|
|
84
|
+
async with httpx.AsyncClient(base_url=cfg.api_url, timeout=10) as c:
|
|
85
|
+
r = await c.post(f"/v1/agent/sessions/{session_id}/inject",
|
|
86
|
+
json={"text": text, "steer_iid": steer_iid},
|
|
87
|
+
headers={"Authorization": f"Bearer {token}"})
|
|
88
|
+
r.raise_for_status()
|
|
89
|
+
return bool((r.json() or {}).get("ok"))
|
|
90
|
+
|
|
91
|
+
|
|
70
92
|
def truncate_for_display(text, limit: int = _DISPLAY_LIMIT) -> str:
|
|
71
93
|
"""Cap one replayed message's text so a single huge assistant reply can't
|
|
72
94
|
flood the boot screen -- foreign_turn renders one line per message."""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""The STICKY todo panel pinned above the queue panel (0.3.15 — replaces the
|
|
2
|
+
inline scroll-away checklist in the dock: the plan now stays visible and
|
|
3
|
+
updates in place on every `todo` frame, mirroring the proven queue-panel
|
|
4
|
+
pattern). The builders here are PURE (unit-tested without an Application);
|
|
5
|
+
tui.run_session mounts them in a ConditionalContainer that occupies ZERO rows
|
|
6
|
+
while the list is empty, so the todo-less dock is pixel-identical to before.
|
|
7
|
+
The panel reads the sink-owned `RichSink.current_todos` list object every
|
|
8
|
+
redraw (in-place mutation, the twin of `remote_pending`); the scrollback
|
|
9
|
+
record is the ONE inline checklist RichSink.end_turn prints per turn. Split
|
|
10
|
+
into its own file like queue_panel.py to stay far under the size ceiling."""
|
|
11
|
+
|
|
12
|
+
from webbee.queue_panel import one_line
|
|
13
|
+
|
|
14
|
+
TP_MAX_ITEMS = 6 # item rows shown; completed collapse first, then `… +K more`
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _rows(todos) -> list:
|
|
18
|
+
"""PURE. Normalize to (status, content) pairs: dict items with a
|
|
19
|
+
non-empty content only — malformed entries are skipped, never raised on
|
|
20
|
+
(same defensive contract as the inline checklist render)."""
|
|
21
|
+
out = []
|
|
22
|
+
for t in (todos if isinstance(todos, (list, tuple)) else ()):
|
|
23
|
+
if not isinstance(t, dict):
|
|
24
|
+
continue
|
|
25
|
+
content = str(t.get("content", "") or "").strip()
|
|
26
|
+
if not content:
|
|
27
|
+
continue
|
|
28
|
+
out.append((str(t.get("status", "") or ""), content))
|
|
29
|
+
return out
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def todo_fragments(todos, width: int = 0):
|
|
33
|
+
"""PURE builder: the panel as prompt_toolkit formatted text, re-invoked
|
|
34
|
+
every redraw (same live mechanics as the queue panel) so every todo_write
|
|
35
|
+
republish shows at once. Layout, top→bottom = plan order:
|
|
36
|
+
|
|
37
|
+
📋 Todos (done/total)
|
|
38
|
+
… ✓K done ← completed collapse FIRST when over the cap
|
|
39
|
+
✓ finished item ← green glyph, dim struck text
|
|
40
|
+
▶ current item ← bold bee-yellow — NEVER hidden by the cap
|
|
41
|
+
○ pending item ← muted
|
|
42
|
+
… +K more ← pending overflow beyond TP_MAX_ITEMS
|
|
43
|
+
|
|
44
|
+
Counts derive from the items themselves (the kernel republishes the FULL
|
|
45
|
+
list on every todo_write). Empty/malformed-only list → [] (panel hidden)."""
|
|
46
|
+
rows = _rows(todos)
|
|
47
|
+
if not rows:
|
|
48
|
+
return []
|
|
49
|
+
done = sum(1 for s, _ in rows if s == "completed")
|
|
50
|
+
frags = [("class:tp.header", f" 📋 Todos ({done}/{len(rows)})")]
|
|
51
|
+
show = rows
|
|
52
|
+
hidden_done = 0
|
|
53
|
+
if len(show) > TP_MAX_ITEMS:
|
|
54
|
+
# History compresses first: collapse the completed items into one
|
|
55
|
+
# summary row so the live part of the plan keeps its rows.
|
|
56
|
+
active = [r for r in show if r[0] != "completed"]
|
|
57
|
+
hidden_done = len(show) - len(active)
|
|
58
|
+
show = active
|
|
59
|
+
extra = 0
|
|
60
|
+
if len(show) > TP_MAX_ITEMS:
|
|
61
|
+
cut = show[TP_MAX_ITEMS:]
|
|
62
|
+
extra = len(cut)
|
|
63
|
+
show = show[:TP_MAX_ITEMS]
|
|
64
|
+
cur = next((r for r in cut if r[0] == "in_progress"), None)
|
|
65
|
+
if cur is not None:
|
|
66
|
+
show[-1] = cur # what's happening NOW is never hidden by the cap
|
|
67
|
+
if hidden_done:
|
|
68
|
+
frags.append(("class:tp.done", f"\n … ✓{hidden_done} done"))
|
|
69
|
+
for status, content in show:
|
|
70
|
+
text = one_line(content, width - 5 if width > 0 else 0)
|
|
71
|
+
if status == "completed":
|
|
72
|
+
frags.append(("class:tp.done", "\n ✓ "))
|
|
73
|
+
frags.append(("class:tp.done.text", text))
|
|
74
|
+
elif status == "in_progress":
|
|
75
|
+
frags.append(("class:tp.now", "\n ▶ " + text))
|
|
76
|
+
else: # pending / unknown -> not started yet
|
|
77
|
+
frags.append(("class:tp.item", "\n ○ " + text))
|
|
78
|
+
if extra:
|
|
79
|
+
frags.append(("class:tp.item", f"\n … +{extra} more"))
|
|
80
|
+
return frags
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def todo_height(todos) -> int:
|
|
84
|
+
"""PURE. Rows the panel needs — derived from the SAME fragments the panel
|
|
85
|
+
renders (header line + one per newline), so height can never desync from
|
|
86
|
+
the visible rows. 0 when the list is empty (the ConditionalContainer
|
|
87
|
+
hides the panel then anyway)."""
|
|
88
|
+
frags = todo_fragments(todos)
|
|
89
|
+
if not frags:
|
|
90
|
+
return 0
|
|
91
|
+
return 1 + sum(f[1].count("\n") for f in frags)
|