webbee 0.3.31__tar.gz → 0.3.33__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 (119) hide show
  1. {webbee-0.3.31 → webbee-0.3.33}/CHANGELOG.md +18 -0
  2. {webbee-0.3.31 → webbee-0.3.33}/PKG-INFO +1 -1
  3. {webbee-0.3.31 → webbee-0.3.33}/pyproject.toml +1 -1
  4. webbee-0.3.33/src/webbee/__init__.py +1 -0
  5. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/coding_context.py +13 -1
  6. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/repl.py +22 -0
  7. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/tools.py +6 -1
  8. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/tui.py +27 -1
  9. webbee-0.3.33/tests/test_coding_context.py +31 -0
  10. {webbee-0.3.31 → webbee-0.3.33}/tests/test_tools.py +16 -0
  11. {webbee-0.3.31 → webbee-0.3.33}/tests/test_tui.py +38 -0
  12. webbee-0.3.31/src/webbee/__init__.py +0 -1
  13. {webbee-0.3.31 → webbee-0.3.33}/.github/workflows/publish.yml +0 -0
  14. {webbee-0.3.31 → webbee-0.3.33}/.gitignore +0 -0
  15. {webbee-0.3.31 → webbee-0.3.33}/LICENSE +0 -0
  16. {webbee-0.3.31 → webbee-0.3.33}/README.md +0 -0
  17. {webbee-0.3.31 → webbee-0.3.33}/docs/superpowers/plans/2026-07-21-interactive-home-dashboard.md +0 -0
  18. {webbee-0.3.31 → webbee-0.3.33}/install.sh +0 -0
  19. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/account.py +0 -0
  20. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/active_sessions.py +0 -0
  21. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/banner_art.py +0 -0
  22. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/boot.py +0 -0
  23. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/checkpoints.py +0 -0
  24. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/cli.py +0 -0
  25. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/clipboard.py +0 -0
  26. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/commands.py +0 -0
  27. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/config.py +0 -0
  28. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/consent.py +0 -0
  29. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/details.py +0 -0
  30. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/events.py +0 -0
  31. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/frames.py +0 -0
  32. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/home.py +0 -0
  33. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/home_view.py +0 -0
  34. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/http.py +0 -0
  35. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/instance_lock.py +0 -0
  36. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/intel/__init__.py +0 -0
  37. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/intel/chunker.py +0 -0
  38. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/intel/embed.py +0 -0
  39. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/intel/graph.py +0 -0
  40. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/intel/indexer.py +0 -0
  41. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/intel/models.py +0 -0
  42. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/intel/query.py +0 -0
  43. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/intel/service.py +0 -0
  44. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/intel/store.py +0 -0
  45. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/intel/vectors.py +0 -0
  46. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/intel/watch.py +0 -0
  47. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/mode_store.py +0 -0
  48. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/newtab_mode.py +0 -0
  49. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/output_pane.py +0 -0
  50. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/queue_panel.py +0 -0
  51. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/reflow.py +0 -0
  52. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/remote.py +0 -0
  53. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/render.py +0 -0
  54. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/repo.py +0 -0
  55. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/selection.py +0 -0
  56. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/session.py +0 -0
  57. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/sessions.py +0 -0
  58. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/sizing.py +0 -0
  59. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/slots.py +0 -0
  60. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/steer.py +0 -0
  61. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/stream.py +0 -0
  62. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/tabs.py +0 -0
  63. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/thread.py +0 -0
  64. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/todo_panel.py +0 -0
  65. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/tokens.py +0 -0
  66. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/update.py +0 -0
  67. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/urlopen.py +0 -0
  68. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/wallet.py +0 -0
  69. {webbee-0.3.31 → webbee-0.3.33}/src/webbee/worktrees.py +0 -0
  70. {webbee-0.3.31 → webbee-0.3.33}/tests/__init__.py +0 -0
  71. {webbee-0.3.31 → webbee-0.3.33}/tests/conftest.py +0 -0
  72. {webbee-0.3.31 → webbee-0.3.33}/tests/test_account.py +0 -0
  73. {webbee-0.3.31 → webbee-0.3.33}/tests/test_active_sessions.py +0 -0
  74. {webbee-0.3.31 → webbee-0.3.33}/tests/test_checkpoints.py +0 -0
  75. {webbee-0.3.31 → webbee-0.3.33}/tests/test_cli.py +0 -0
  76. {webbee-0.3.31 → webbee-0.3.33}/tests/test_clipboard.py +0 -0
  77. {webbee-0.3.31 → webbee-0.3.33}/tests/test_commands.py +0 -0
  78. {webbee-0.3.31 → webbee-0.3.33}/tests/test_config.py +0 -0
  79. {webbee-0.3.31 → webbee-0.3.33}/tests/test_cpc_contract_stable.py +0 -0
  80. {webbee-0.3.31 → webbee-0.3.33}/tests/test_details.py +0 -0
  81. {webbee-0.3.31 → webbee-0.3.33}/tests/test_events.py +0 -0
  82. {webbee-0.3.31 → webbee-0.3.33}/tests/test_freeze_fix.py +0 -0
  83. {webbee-0.3.31 → webbee-0.3.33}/tests/test_home.py +0 -0
  84. {webbee-0.3.31 → webbee-0.3.33}/tests/test_home_view.py +0 -0
  85. {webbee-0.3.31 → webbee-0.3.33}/tests/test_instance_lock.py +0 -0
  86. {webbee-0.3.31 → webbee-0.3.33}/tests/test_intel_chunker.py +0 -0
  87. {webbee-0.3.31 → webbee-0.3.33}/tests/test_intel_embed.py +0 -0
  88. {webbee-0.3.31 → webbee-0.3.33}/tests/test_intel_graph.py +0 -0
  89. {webbee-0.3.31 → webbee-0.3.33}/tests/test_intel_indexer.py +0 -0
  90. {webbee-0.3.31 → webbee-0.3.33}/tests/test_intel_no_numpy.py +0 -0
  91. {webbee-0.3.31 → webbee-0.3.33}/tests/test_intel_query.py +0 -0
  92. {webbee-0.3.31 → webbee-0.3.33}/tests/test_intel_service.py +0 -0
  93. {webbee-0.3.31 → webbee-0.3.33}/tests/test_intel_store.py +0 -0
  94. {webbee-0.3.31 → webbee-0.3.33}/tests/test_intel_vectors.py +0 -0
  95. {webbee-0.3.31 → webbee-0.3.33}/tests/test_intel_watch.py +0 -0
  96. {webbee-0.3.31 → webbee-0.3.33}/tests/test_marathon.py +0 -0
  97. {webbee-0.3.31 → webbee-0.3.33}/tests/test_mode_store.py +0 -0
  98. {webbee-0.3.31 → webbee-0.3.33}/tests/test_newtab_mode.py +0 -0
  99. {webbee-0.3.31 → webbee-0.3.33}/tests/test_packaging.py +0 -0
  100. {webbee-0.3.31 → webbee-0.3.33}/tests/test_reflow.py +0 -0
  101. {webbee-0.3.31 → webbee-0.3.33}/tests/test_render.py +0 -0
  102. {webbee-0.3.31 → webbee-0.3.33}/tests/test_repl.py +0 -0
  103. {webbee-0.3.31 → webbee-0.3.33}/tests/test_repo.py +0 -0
  104. {webbee-0.3.31 → webbee-0.3.33}/tests/test_session.py +0 -0
  105. {webbee-0.3.31 → webbee-0.3.33}/tests/test_sessions.py +0 -0
  106. {webbee-0.3.31 → webbee-0.3.33}/tests/test_sizing.py +0 -0
  107. {webbee-0.3.31 → webbee-0.3.33}/tests/test_slots.py +0 -0
  108. {webbee-0.3.31 → webbee-0.3.33}/tests/test_steer.py +0 -0
  109. {webbee-0.3.31 → webbee-0.3.33}/tests/test_stream.py +0 -0
  110. {webbee-0.3.31 → webbee-0.3.33}/tests/test_tabs.py +0 -0
  111. {webbee-0.3.31 → webbee-0.3.33}/tests/test_thread.py +0 -0
  112. {webbee-0.3.31 → webbee-0.3.33}/tests/test_todo_panel.py +0 -0
  113. {webbee-0.3.31 → webbee-0.3.33}/tests/test_tokens.py +0 -0
  114. {webbee-0.3.31 → webbee-0.3.33}/tests/test_tui_hardening.py +0 -0
  115. {webbee-0.3.31 → webbee-0.3.33}/tests/test_update.py +0 -0
  116. {webbee-0.3.31 → webbee-0.3.33}/tests/test_urlopen.py +0 -0
  117. {webbee-0.3.31 → webbee-0.3.33}/tests/test_version.py +0 -0
  118. {webbee-0.3.31 → webbee-0.3.33}/tests/test_wallet.py +0 -0
  119. {webbee-0.3.31 → webbee-0.3.33}/tests/test_worktrees.py +0 -0
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.33
4
+
5
+ Performance.
6
+
7
+ - When nothing is happening, the terminal now wakes about once a second
8
+ instead of four times a second — easier on the CPU and the battery — while
9
+ staying instant the moment a task runs or you're drag-scrolling.
10
+ - The agent's file search and per-task project snapshot no longer crawl
11
+ `node_modules`, `vendor`, `dist`, `build`, and friends, so both stay fast on
12
+ large repositories.
13
+
14
+ ## 0.3.32
15
+
16
+ - Fixed the screen getting scrambled by stray output from background helper
17
+ programs (a git credential helper on macOS, a browser on Linux, and the
18
+ like). Their output is now fully redirected to a log for the whole session,
19
+ so nothing but Webbee Code can draw on your terminal.
20
+
3
21
  ## 0.3.31
4
22
 
5
23
  - Fixed a Linux bug where "Top up credits" / "Read our security" could dump a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webbee
3
- Version: 0.3.31
3
+ Version: 0.3.33
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.31"
3
+ version = "0.3.33"
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.33"
@@ -5,6 +5,17 @@ proof-of-done command a marathon carries in that context."""
5
5
  import os
6
6
  import subprocess
7
7
 
8
+ # Heavy dependency/build dirs the file-tree walk and the agent `grep` tool
9
+ # must NEVER descend: they blow up walk time on real repos and (with the tree's
10
+ # 200-file cap) would otherwise fill the snapshot with dependency junk instead
11
+ # of the user's own code. `.git` + all dotdirs are pruned separately by the
12
+ # callers; these are the non-dot offenders. Shared with tools._t_grep.
13
+ WALK_IGNORE_DIRS = frozenset({
14
+ "node_modules", "vendor", "dist", "build", "target", "__pycache__",
15
+ ".git", ".venv", "venv", ".next", ".cache", ".tox", ".mypy_cache",
16
+ ".pytest_cache",
17
+ })
18
+
8
19
 
9
20
  def build_coding_context(workspace_root: str, intel=None) -> dict:
10
21
  """Snapshot handed to the cloud brain: cwd (realpath), `git status -sb`
@@ -23,7 +34,8 @@ def build_coding_context(workspace_root: str, intel=None) -> dict:
23
34
  git = ""
24
35
  paths = []
25
36
  for dirpath, dirnames, filenames in os.walk(cwd):
26
- dirnames[:] = [d for d in dirnames if d != ".git" and not d.startswith(".")]
37
+ dirnames[:] = [d for d in dirnames
38
+ if d not in WALK_IGNORE_DIRS and not d.startswith(".")]
27
39
  for fn in filenames:
28
40
  paths.append(os.path.relpath(os.path.join(dirpath, fn), cwd))
29
41
  if len(paths) >= 200:
@@ -1221,6 +1221,20 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
1221
1221
  # renderer. Restored the instant the dock exits (the transcript dump
1222
1222
  # below writes to the REAL stdout).
1223
1223
  _errlog = boot._open_dock_stderr_log()
1224
+ # OS-LEVEL redirect of fd 2, not just Python's sys.stderr object:
1225
+ # SUBPROCESSES inherit the real fd 2, so a `contextlib.redirect_stderr`
1226
+ # alone (Python-level) does NOT stop a child's stderr from hitting the
1227
+ # terminal and scrambling the diff-based renderer — a Linux browser's
1228
+ # MESA/GCM logs, a macOS `Python … MallocStackLogging` git-helper, etc.
1229
+ # dup2 sends ALL of it (children + C libs + fd-2 writes) to the same
1230
+ # log; the contextlib redirect below additionally covers Python-level
1231
+ # sys.stderr writers. Fail-soft: on any failure leave fd 2 untouched.
1232
+ _saved_stderr_fd = None
1233
+ try:
1234
+ _saved_stderr_fd = os.dup(2)
1235
+ os.dup2(_errlog.fileno(), 2)
1236
+ except (OSError, ValueError, AttributeError):
1237
+ _saved_stderr_fd = None
1224
1238
  try:
1225
1239
  with contextlib.redirect_stderr(_errlog):
1226
1240
  from webbee import tui
@@ -1363,6 +1377,14 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
1363
1377
  except Exception:
1364
1378
  ok = False
1365
1379
  finally:
1380
+ # Restore the real fd 2 BEFORE the transcript dump / any later
1381
+ # console output, then close the log.
1382
+ if _saved_stderr_fd is not None:
1383
+ try:
1384
+ os.dup2(_saved_stderr_fd, 2)
1385
+ os.close(_saved_stderr_fd)
1386
+ except OSError:
1387
+ pass
1366
1388
  try:
1367
1389
  _errlog.close()
1368
1390
  except Exception:
@@ -327,12 +327,17 @@ class LocalToolExecutor:
327
327
  return {"ok": proc.returncode == 0, "content": out or f"(exit {proc.returncode})"}
328
328
 
329
329
  def _t_grep(self, a: dict) -> dict:
330
+ from webbee.coding_context import WALK_IGNORE_DIRS
330
331
  pat = re.compile(a["pattern"])
331
332
  base = self.resolve_in_workspace(a.get("path", "."))
332
333
  hits = []
333
- for dp, _dn, fns in os.walk(base):
334
+ for dp, dns, fns in os.walk(base):
334
335
  if _is_git_dir(dp):
336
+ dns[:] = []
335
337
  continue
338
+ # Prune heavy dependency/build dirs (node_modules, vendor, dist,
339
+ # …) so a grep on a real repo doesn't crawl megabytes of deps.
340
+ dns[:] = [d for d in dns if d not in WALK_IGNORE_DIRS]
336
341
  for fn in fns:
337
342
  fp = os.path.join(dp, fn)
338
343
  try:
@@ -211,6 +211,32 @@ def _width_watch(pane, app) -> None:
211
211
  pass
212
212
 
213
213
 
214
+ def _ticker_busy(slots, is_busy) -> bool:
215
+ """Whether the dock's animation loop must stay at the FAST (0.25s) cadence:
216
+ a turn is running, a copy-flash toast is live, OR an edge-drag auto-scroll
217
+ is in flight (`pane._edge_drag` — a drag-select past the viewport edge that
218
+ `pane.edge_tick()` keeps scrolling every tick). All three need smooth ~4x/s
219
+ updates; miss the edge-drag one and idle drag-scrolling crawls 4x slower.
220
+ Otherwise the loop is idle → the caller uses the slow 1.0s cadence."""
221
+ try:
222
+ pane = slots.active().pane
223
+ if bool(pane.flash()) or bool(getattr(pane, "_edge_drag", 0)):
224
+ return True
225
+ except Exception:
226
+ pass
227
+ return bool(is_busy())
228
+
229
+
230
+ def _tick_interval(busy: bool) -> float:
231
+ """The dock's animation-loop sleep. 0.25s while a turn is running (or a
232
+ copy-flash is live) so the spinner/elapsed-clock animate smoothly; 1.0s
233
+ when fully idle, so the loop wakes 1×/s instead of 4×/s (less CPU/battery)
234
+ with no visible cost — an idle tick only resize-detects + re-syncs hover,
235
+ both of which tolerate a 1s lag (and a turn's first frame still shows
236
+ instantly via the submit's own invalidate)."""
237
+ return 0.25 if busy else 1.0
238
+
239
+
214
240
  def _tick_once(slots, app, is_busy) -> None:
215
241
  """One iteration of run_session's `_ticker` loop, extracted module-level
216
242
  so the wiring itself is directly unit-testable (an `async def` infinite
@@ -1272,7 +1298,7 @@ async def run_session(*, slots, on_line, on_cycle, steps_nav=None,
1272
1298
  # is_busy this ticker feeds it, since the old top-level is_busy
1273
1299
  # param died with the rest of the sink-shaped params.
1274
1300
  while True:
1275
- await asyncio.sleep(0.25)
1301
+ await asyncio.sleep(_tick_interval(_ticker_busy(slots, _busy_live)))
1276
1302
  _sync_hover_mode()
1277
1303
  _tick_once(slots, app, _busy_live)
1278
1304
 
@@ -0,0 +1,31 @@
1
+ from webbee.coding_context import WALK_IGNORE_DIRS, build_coding_context
2
+
3
+
4
+ def test_walk_ignore_dirs_has_the_heavy_hitters():
5
+ for d in ("node_modules", "vendor", "dist", "build", "target", "__pycache__"):
6
+ assert d in WALK_IGNORE_DIRS
7
+
8
+
9
+ def test_build_coding_context_prunes_heavy_dirs(tmp_path):
10
+ # W6: the per-turn file-tree walk must not descend node_modules/etc. — on a
11
+ # real repo it's slow and (with the 200-file cap) would fill the snapshot
12
+ # with dependency junk instead of the user's own code.
13
+ (tmp_path / "src").mkdir()
14
+ (tmp_path / "src" / "a.py").write_text("x\n")
15
+ nm = tmp_path / "node_modules" / "pkg"
16
+ nm.mkdir(parents=True)
17
+ (nm / "index.js").write_text("y\n")
18
+ tree = build_coding_context(str(tmp_path))["tree"]
19
+ assert "src/a.py" in tree
20
+ assert "node_modules" not in tree
21
+
22
+
23
+ def test_build_coding_context_still_prunes_dotdirs(tmp_path):
24
+ (tmp_path / "src").mkdir()
25
+ (tmp_path / "src" / "a.py").write_text("x\n")
26
+ hidden = tmp_path / ".secret"
27
+ hidden.mkdir()
28
+ (hidden / "k.txt").write_text("y\n")
29
+ tree = build_coding_context(str(tmp_path))["tree"]
30
+ assert "src/a.py" in tree
31
+ assert ".secret" not in tree
@@ -49,6 +49,22 @@ def test_grep_skips_git_dir(tmp_path):
49
49
  assert r["ok"] and "a.py" in r["content"] and ".git" not in r["content"]
50
50
 
51
51
 
52
+ def test_grep_prunes_heavy_dirs(tmp_path):
53
+ # W6: a code grep must never crawl node_modules/vendor/dist/etc. — huge and
54
+ # never what the user meant. A match in src surfaces; one in node_modules
55
+ # does not (the walk is pruned, not just filtered).
56
+ ex = _ex(tmp_path)
57
+ (tmp_path / "src").mkdir()
58
+ (tmp_path / "src" / "a.py").write_text("needle\n")
59
+ nm = tmp_path / "node_modules" / "pkg"
60
+ nm.mkdir(parents=True)
61
+ (nm / "index.js").write_text("needle\n")
62
+ r = ex.run("grep", {"pattern": "needle"})
63
+ assert r["ok"]
64
+ assert "a.py" in r["content"]
65
+ assert "node_modules" not in r["content"]
66
+
67
+
52
68
  def test_grep_skips_git_dir_windows_style():
53
69
  # The os.walk-driven .git skip in _t_grep normalizes dp with os.sep so it
54
70
  # also matches on Windows, where os.walk yields backslash paths (an
@@ -5206,3 +5206,41 @@ def test_hover_scoping_present_and_home_only():
5206
5206
  src = inspect.getsource(tui.run_session)
5207
5207
  assert "?1003h" in src and "?1003l" in src
5208
5208
  assert "_sync_hover_mode" in src
5209
+
5210
+
5211
+ def test_tick_interval_fast_when_busy_slow_when_idle():
5212
+ # W6: the animation loop wakes 4x/s only while busy/flash; 1x/s when idle.
5213
+ from webbee.tui import _tick_interval
5214
+ assert _tick_interval(True) == 0.25
5215
+ assert _tick_interval(False) == 1.0
5216
+
5217
+
5218
+ def test_ticker_busy_includes_turn_flash_and_edge_drag():
5219
+ # W6 review fix: the fast-cadence predicate must fire for a running turn,
5220
+ # a live copy-flash, AND an in-flight edge-drag auto-scroll (else idle
5221
+ # drag-scrolling crawls 4x slower). A pane lacking _edge_drag (HomeView)
5222
+ # must not crash.
5223
+ from webbee.tui import _ticker_busy
5224
+
5225
+ class _Pane:
5226
+ def __init__(self, flash="", edge=0):
5227
+ self._flash, self._edge_drag = flash, edge
5228
+ def flash(self):
5229
+ return self._flash
5230
+
5231
+ class _Slots:
5232
+ def __init__(self, pane):
5233
+ self._p = pane
5234
+ def active(self):
5235
+ return type("S", (), {"pane": self._p})()
5236
+
5237
+ idle, running = (lambda: False), (lambda: True)
5238
+ assert _ticker_busy(_Slots(_Pane()), idle) is False
5239
+ assert _ticker_busy(_Slots(_Pane()), running) is True
5240
+ assert _ticker_busy(_Slots(_Pane(flash="copied")), idle) is True
5241
+ assert _ticker_busy(_Slots(_Pane(edge=1)), idle) is True
5242
+
5243
+ class _Bare:
5244
+ def flash(self):
5245
+ return ""
5246
+ assert _ticker_busy(_Slots(_Bare()), idle) is False
@@ -1 +0,0 @@
1
- __version__ = "0.3.31"
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
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