webbee 0.3.15__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.
Files changed (88) hide show
  1. {webbee-0.3.15 → webbee-0.3.16}/CHANGELOG.md +7 -0
  2. {webbee-0.3.15 → webbee-0.3.16}/PKG-INFO +1 -1
  3. {webbee-0.3.15 → webbee-0.3.16}/pyproject.toml +1 -1
  4. webbee-0.3.16/src/webbee/__init__.py +1 -0
  5. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/render.py +36 -8
  6. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/repl.py +5 -0
  7. {webbee-0.3.15 → webbee-0.3.16}/tests/test_render.py +49 -0
  8. {webbee-0.3.15 → webbee-0.3.16}/tests/test_repl.py +9 -0
  9. webbee-0.3.15/src/webbee/__init__.py +0 -1
  10. {webbee-0.3.15 → webbee-0.3.16}/.github/workflows/publish.yml +0 -0
  11. {webbee-0.3.15 → webbee-0.3.16}/.gitignore +0 -0
  12. {webbee-0.3.15 → webbee-0.3.16}/LICENSE +0 -0
  13. {webbee-0.3.15 → webbee-0.3.16}/README.md +0 -0
  14. {webbee-0.3.15 → webbee-0.3.16}/install.sh +0 -0
  15. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/account.py +0 -0
  16. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/banner_art.py +0 -0
  17. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/boot.py +0 -0
  18. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/checkpoints.py +0 -0
  19. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/cli.py +0 -0
  20. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/clipboard.py +0 -0
  21. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/coding_context.py +0 -0
  22. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/commands.py +0 -0
  23. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/config.py +0 -0
  24. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/consent.py +0 -0
  25. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/details.py +0 -0
  26. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/events.py +0 -0
  27. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/frames.py +0 -0
  28. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/intel/__init__.py +0 -0
  29. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/intel/chunker.py +0 -0
  30. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/intel/embed.py +0 -0
  31. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/intel/graph.py +0 -0
  32. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/intel/indexer.py +0 -0
  33. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/intel/models.py +0 -0
  34. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/intel/query.py +0 -0
  35. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/intel/service.py +0 -0
  36. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/intel/store.py +0 -0
  37. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/intel/vectors.py +0 -0
  38. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/intel/watch.py +0 -0
  39. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/output_pane.py +0 -0
  40. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/queue_panel.py +0 -0
  41. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/remote.py +0 -0
  42. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/repo.py +0 -0
  43. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/session.py +0 -0
  44. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/sessions.py +0 -0
  45. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/steer.py +0 -0
  46. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/stream.py +0 -0
  47. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/thread.py +0 -0
  48. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/todo_panel.py +0 -0
  49. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/tokens.py +0 -0
  50. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/tools.py +0 -0
  51. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/tui.py +0 -0
  52. {webbee-0.3.15 → webbee-0.3.16}/src/webbee/update.py +0 -0
  53. {webbee-0.3.15 → webbee-0.3.16}/tests/__init__.py +0 -0
  54. {webbee-0.3.15 → webbee-0.3.16}/tests/test_account.py +0 -0
  55. {webbee-0.3.15 → webbee-0.3.16}/tests/test_checkpoints.py +0 -0
  56. {webbee-0.3.15 → webbee-0.3.16}/tests/test_cli.py +0 -0
  57. {webbee-0.3.15 → webbee-0.3.16}/tests/test_clipboard.py +0 -0
  58. {webbee-0.3.15 → webbee-0.3.16}/tests/test_commands.py +0 -0
  59. {webbee-0.3.15 → webbee-0.3.16}/tests/test_config.py +0 -0
  60. {webbee-0.3.15 → webbee-0.3.16}/tests/test_cpc_contract_stable.py +0 -0
  61. {webbee-0.3.15 → webbee-0.3.16}/tests/test_details.py +0 -0
  62. {webbee-0.3.15 → webbee-0.3.16}/tests/test_events.py +0 -0
  63. {webbee-0.3.15 → webbee-0.3.16}/tests/test_freeze_fix.py +0 -0
  64. {webbee-0.3.15 → webbee-0.3.16}/tests/test_intel_chunker.py +0 -0
  65. {webbee-0.3.15 → webbee-0.3.16}/tests/test_intel_embed.py +0 -0
  66. {webbee-0.3.15 → webbee-0.3.16}/tests/test_intel_graph.py +0 -0
  67. {webbee-0.3.15 → webbee-0.3.16}/tests/test_intel_indexer.py +0 -0
  68. {webbee-0.3.15 → webbee-0.3.16}/tests/test_intel_no_numpy.py +0 -0
  69. {webbee-0.3.15 → webbee-0.3.16}/tests/test_intel_query.py +0 -0
  70. {webbee-0.3.15 → webbee-0.3.16}/tests/test_intel_service.py +0 -0
  71. {webbee-0.3.15 → webbee-0.3.16}/tests/test_intel_store.py +0 -0
  72. {webbee-0.3.15 → webbee-0.3.16}/tests/test_intel_vectors.py +0 -0
  73. {webbee-0.3.15 → webbee-0.3.16}/tests/test_intel_watch.py +0 -0
  74. {webbee-0.3.15 → webbee-0.3.16}/tests/test_marathon.py +0 -0
  75. {webbee-0.3.15 → webbee-0.3.16}/tests/test_packaging.py +0 -0
  76. {webbee-0.3.15 → webbee-0.3.16}/tests/test_repo.py +0 -0
  77. {webbee-0.3.15 → webbee-0.3.16}/tests/test_session.py +0 -0
  78. {webbee-0.3.15 → webbee-0.3.16}/tests/test_sessions.py +0 -0
  79. {webbee-0.3.15 → webbee-0.3.16}/tests/test_steer.py +0 -0
  80. {webbee-0.3.15 → webbee-0.3.16}/tests/test_stream.py +0 -0
  81. {webbee-0.3.15 → webbee-0.3.16}/tests/test_thread.py +0 -0
  82. {webbee-0.3.15 → webbee-0.3.16}/tests/test_todo_panel.py +0 -0
  83. {webbee-0.3.15 → webbee-0.3.16}/tests/test_tokens.py +0 -0
  84. {webbee-0.3.15 → webbee-0.3.16}/tests/test_tools.py +0 -0
  85. {webbee-0.3.15 → webbee-0.3.16}/tests/test_tui.py +0 -0
  86. {webbee-0.3.15 → webbee-0.3.16}/tests/test_tui_hardening.py +0 -0
  87. {webbee-0.3.15 → webbee-0.3.16}/tests/test_update.py +0 -0
  88. {webbee-0.3.15 → webbee-0.3.16}/tests/test_version.py +0 -0
@@ -1,5 +1,12 @@
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
+
3
10
  ## 0.3.15
4
11
 
5
12
  - Type a follow-up while Webbee is working and it now flies into the current
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webbee
3
- Version: 0.3.15
3
+ Version: 0.3.16
4
4
  Summary: Webbee 🐝 — the Imperal Cloud coding agent in your terminal
5
5
  Project-URL: Homepage, https://imperal.io
6
6
  Project-URL: Documentation, https://docs.imperal.io
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webbee"
3
- version = "0.3.15"
3
+ version = "0.3.16"
4
4
  description = "Webbee 🐝 — the Imperal Cloud coding agent in your terminal"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1 @@
1
+ __version__ = "0.3.16"
@@ -139,6 +139,13 @@ 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
142
149
  # The current coding checklist (sticky todo panel, 0.3.15) — the twin
143
150
  # of remote_pending: todos() replaces its contents in place on every
144
151
  # todo frame; the dock's todo panel reads THIS list object every
@@ -325,25 +332,46 @@ class RichSink:
325
332
  queue panel the instant it queues — tagged `[origin]`, "as if typed".
326
333
  DISPLAY-ONLY: it renders above the local rows but is never pullable
327
334
  (↑/click) — the kernel owns it; only a `task_dequeued` (or turn end)
328
- 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
329
355
  self.remote_pending.append({"origin": _clean(str(origin or "")),
330
356
  "text": _clean(str(text or "")),
331
- "iid": str(iid or "")})
357
+ "iid": iid})
332
358
  self._nudge()
333
359
 
334
360
  def remote_dequeued(self, origin: str, iid: str) -> None:
335
361
  """The kernel drained (or dedup-dropped) one queued item
336
- (`task_dequeued` frame): remove its panel row by `iid` when it
337
- matches, else the OLDEST row of that origin (the kernel queue is
338
- FIFO, so when an iid is missing/lost the oldest same-origin row is
339
- the one that just started). Nothing matched no-op (the row was
340
- already cleared or its announce was never seen)."""
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)."""
341
369
  iid = str(iid or "")
342
370
  origin = _clean(str(origin or ""))
343
371
  rows = self.remote_pending
344
372
  idx = next((i for i, r in enumerate(rows) if iid and r.get("iid") == iid),
345
373
  None)
346
- if idx is None:
374
+ if idx is None and not iid:
347
375
  idx = next((i for i, r in enumerate(rows) if r.get("origin") == origin),
348
376
  None)
349
377
  if idx is None:
@@ -344,6 +344,11 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
344
344
  async def _boot(s) -> None:
345
345
  nonlocal _sink, agent, intel, watcher_task, shadow, steer_task
346
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
347
352
  # Cache git branch OFF the event loop (subprocess.run blocks it). Only
348
353
  # /status reads it; recomputing it per input line froze the dock.
349
354
  state["git_branch"] = await asyncio.to_thread(_git_branch, workspace)
@@ -603,6 +603,55 @@ def test_end_turn_and_abort_clear_remote_rows_in_place():
603
603
  assert rows == [] and s.remote_pending is rows
604
604
 
605
605
 
606
+ # ── 0.3.16: queue-panel single-source dedup (steer_iid reconciliation) ────────
607
+ # The steer_iid minted at enqueue time is the ONE key both legs carry; the
608
+ # display layer now consumes it too: a duplicated task_queued frame
609
+ # (at-least-once delivery) never doubles a row, and the kernel echo of a
610
+ # terminal-injected line REPLACES its local fallback twin — one message,
611
+ # one owner (the kernel), one panel row.
612
+
613
+ def test_remote_queued_ignores_duplicate_frame_same_iid():
614
+ s = _sink()
615
+ s.remote_queued("terminal", "ship it", "i1")
616
+ s.remote_queued("terminal", "ship it", "i1") # publish retry / SSE resume
617
+ assert [r["iid"] for r in s.remote_pending] == ["i1"]
618
+
619
+
620
+ def test_remote_queued_empty_iid_never_dedups():
621
+ # Legacy kernels emit steer_iid="" — identical re-typed lines must keep
622
+ # appending (text is NOT a dedup key).
623
+ s = _sink()
624
+ s.remote_queued("telegram", "same text", "")
625
+ s.remote_queued("telegram", "same text", "")
626
+ assert len(s.remote_pending) == 2
627
+
628
+
629
+ def test_remote_queued_promotes_local_twin_to_kernel_owned():
630
+ # A failed-LOOKING inject that actually landed leaves a local QueuedLine
631
+ # twin; the kernel echo (same iid) is positive proof it landed — the
632
+ # local row goes, the message shows exactly once (kernel-owned).
633
+ from collections import deque
634
+
635
+ from webbee.tui import QueuedLine
636
+ s = _sink()
637
+ lp = deque([QueuedLine("ship it", "i1"), QueuedLine("other", "i9")])
638
+ s.local_pending = lp
639
+ s.remote_queued("terminal", "ship it", "i1")
640
+ assert [str(x) for x in lp] == ["other"]
641
+ assert [r["iid"] for r in s.remote_pending] == ["i1"]
642
+
643
+
644
+ def test_remote_dequeued_nonempty_iid_unmatched_is_noop():
645
+ # task_dequeued twins (the kernel emits one per pop, dedup-dropped twins
646
+ # included) must never eat a DIFFERENT same-origin row: the origin-FIFO
647
+ # fallback is legacy-only (empty iid).
648
+ s = _sink()
649
+ s.remote_queued("terminal", "a", "i1")
650
+ s.remote_queued("terminal", "b", "i2")
651
+ s.remote_dequeued("terminal", "i-unknown")
652
+ assert [r["iid"] for r in s.remote_pending] == ["i1", "i2"]
653
+
654
+
606
655
  # ── 0.3.14: the terminal-local yes/no confirm (autopilot safe-asymmetry) ──────
607
656
 
608
657
  def test_ask_yes_no_sync_fallback_strict_yes_only():
@@ -395,6 +395,15 @@ def test_boot_survives_intel_import_failure_base_install(monkeypatch):
395
395
  assert sink.turns == [] # boot completed cleanly, no crash mid-boot
396
396
 
397
397
 
398
+ def test_boot_shares_local_queue_with_sink():
399
+ # The repl hands the sink the SAME type-ahead deque tui mutates, so the
400
+ # kernel's task_queued echo can promote a local twin into the single
401
+ # kernel-owned row (queue-panel single-source dedup, 0.3.16).
402
+ from collections import deque
403
+ sink, agent = _run(read_line=_lines("/exit"))
404
+ assert isinstance(getattr(sink, "local_pending", None), deque)
405
+
406
+
398
407
  def test_boot_skips_intel_when_disabled_in_config(monkeypatch):
399
408
  monkeypatch.setattr("webbee.repl.AgentSession", _SpyAgent)
400
409
  called = []
@@ -1 +0,0 @@
1
- __version__ = "0.3.15"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes