webbee 0.7.7__tar.gz → 0.7.9__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 (160) hide show
  1. {webbee-0.7.7 → webbee-0.7.9}/CHANGELOG.md +19 -0
  2. {webbee-0.7.7 → webbee-0.7.9}/PKG-INFO +3 -3
  3. {webbee-0.7.7 → webbee-0.7.9}/pyproject.toml +3 -3
  4. webbee-0.7.9/src/webbee/__init__.py +1 -0
  5. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/autoupdate.py +19 -9
  6. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/checkpoints.py +25 -5
  7. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/repl.py +24 -0
  8. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/tools.py +2 -1
  9. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/update.py +24 -0
  10. {webbee-0.7.7 → webbee-0.7.9}/tests/test_autoupdate.py +18 -0
  11. {webbee-0.7.7 → webbee-0.7.9}/tests/test_checkpoints.py +23 -0
  12. {webbee-0.7.7 → webbee-0.7.9}/tests/test_update_badge_0340.py +24 -0
  13. webbee-0.7.7/src/webbee/__init__.py +0 -1
  14. {webbee-0.7.7 → webbee-0.7.9}/.github/workflows/publish.yml +0 -0
  15. {webbee-0.7.7 → webbee-0.7.9}/.gitignore +0 -0
  16. {webbee-0.7.7 → webbee-0.7.9}/LICENSE +0 -0
  17. {webbee-0.7.7 → webbee-0.7.9}/README.md +0 -0
  18. {webbee-0.7.7 → webbee-0.7.9}/install.sh +0 -0
  19. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/__main__.py +0 -0
  20. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/_launch.py +0 -0
  21. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/account.py +0 -0
  22. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/active_sessions.py +0 -0
  23. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/banner_art.py +0 -0
  24. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/boot.py +0 -0
  25. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/browser.py +0 -0
  26. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/cli.py +0 -0
  27. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/clipboard.py +0 -0
  28. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/clipboard_read.py +0 -0
  29. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/clipboard_session.py +0 -0
  30. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/coding_context.py +0 -0
  31. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/commands.py +0 -0
  32. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/config.py +0 -0
  33. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/consent.py +0 -0
  34. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/details.py +0 -0
  35. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/events.py +0 -0
  36. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/frames.py +0 -0
  37. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/home.py +0 -0
  38. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/home_view.py +0 -0
  39. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/http.py +0 -0
  40. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/instance_lock.py +0 -0
  41. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/intel/__init__.py +0 -0
  42. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/intel/graph.py +0 -0
  43. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/intel/outline.py +0 -0
  44. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/intel/query.py +0 -0
  45. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/intel/service.py +0 -0
  46. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/intel/store.py +0 -0
  47. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/intel/tree.py +0 -0
  48. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/intel/vectors.py +0 -0
  49. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/intel/watch.py +0 -0
  50. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/media.py +0 -0
  51. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/modals.py +0 -0
  52. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/mode_store.py +0 -0
  53. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/native_files.py +0 -0
  54. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/native_read.py +0 -0
  55. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/newtab_mode.py +0 -0
  56. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/output_pane.py +0 -0
  57. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/pricing_constants.py +0 -0
  58. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/queue_panel.py +0 -0
  59. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/reflow.py +0 -0
  60. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/remote.py +0 -0
  61. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/render.py +0 -0
  62. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/repo.py +0 -0
  63. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/selection.py +0 -0
  64. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/session.py +0 -0
  65. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/sessions.py +0 -0
  66. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/sizing.py +0 -0
  67. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/slots.py +0 -0
  68. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/steer.py +0 -0
  69. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/stream.py +0 -0
  70. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/tab_store.py +0 -0
  71. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/tabs.py +0 -0
  72. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/thread.py +0 -0
  73. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/tier_store.py +0 -0
  74. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/todo_panel.py +0 -0
  75. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/tokens.py +0 -0
  76. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/tui.py +0 -0
  77. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/urlopen.py +0 -0
  78. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/wallet.py +0 -0
  79. {webbee-0.7.7 → webbee-0.7.9}/src/webbee/worktrees.py +0 -0
  80. {webbee-0.7.7 → webbee-0.7.9}/tests/__init__.py +0 -0
  81. {webbee-0.7.7 → webbee-0.7.9}/tests/conftest.py +0 -0
  82. {webbee-0.7.7 → webbee-0.7.9}/tests/test_account.py +0 -0
  83. {webbee-0.7.7 → webbee-0.7.9}/tests/test_active_sessions.py +0 -0
  84. {webbee-0.7.7 → webbee-0.7.9}/tests/test_audit_2026_07_25_findings.py +0 -0
  85. {webbee-0.7.7 → webbee-0.7.9}/tests/test_checkpoints_nested_1030.py +0 -0
  86. {webbee-0.7.7 → webbee-0.7.9}/tests/test_cli.py +0 -0
  87. {webbee-0.7.7 → webbee-0.7.9}/tests/test_clipboard.py +0 -0
  88. {webbee-0.7.7 → webbee-0.7.9}/tests/test_clipboard_read.py +0 -0
  89. {webbee-0.7.7 → webbee-0.7.9}/tests/test_code_map_fresh.py +0 -0
  90. {webbee-0.7.7 → webbee-0.7.9}/tests/test_coding_context.py +0 -0
  91. {webbee-0.7.7 → webbee-0.7.9}/tests/test_commands.py +0 -0
  92. {webbee-0.7.7 → webbee-0.7.9}/tests/test_config.py +0 -0
  93. {webbee-0.7.7 → webbee-0.7.9}/tests/test_cpc_contract_stable.py +0 -0
  94. {webbee-0.7.7 → webbee-0.7.9}/tests/test_cpc_outline.py +0 -0
  95. {webbee-0.7.7 → webbee-0.7.9}/tests/test_cross_loop_token.py +0 -0
  96. {webbee-0.7.7 → webbee-0.7.9}/tests/test_details.py +0 -0
  97. {webbee-0.7.7 → webbee-0.7.9}/tests/test_events.py +0 -0
  98. {webbee-0.7.7 → webbee-0.7.9}/tests/test_freeze_fix.py +0 -0
  99. {webbee-0.7.7 → webbee-0.7.9}/tests/test_home.py +0 -0
  100. {webbee-0.7.7 → webbee-0.7.9}/tests/test_home_view.py +0 -0
  101. {webbee-0.7.7 → webbee-0.7.9}/tests/test_home_view_mouse.py +0 -0
  102. {webbee-0.7.7 → webbee-0.7.9}/tests/test_inspect_page.py +0 -0
  103. {webbee-0.7.7 → webbee-0.7.9}/tests/test_instance_lock.py +0 -0
  104. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_any_folder.py +0 -0
  105. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_cache_and_sync_worker.py +0 -0
  106. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_checksum_tree.py +0 -0
  107. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_chunker.py +0 -0
  108. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_embed.py +0 -0
  109. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_folder_1106.py +0 -0
  110. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_graph.py +0 -0
  111. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_no_numpy.py +0 -0
  112. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_query.py +0 -0
  113. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_service.py +0 -0
  114. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_store.py +0 -0
  115. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_vectors.py +0 -0
  116. {webbee-0.7.7 → webbee-0.7.9}/tests/test_intel_watch.py +0 -0
  117. {webbee-0.7.7 → webbee-0.7.9}/tests/test_launch.py +0 -0
  118. {webbee-0.7.7 → webbee-0.7.9}/tests/test_marathon.py +0 -0
  119. {webbee-0.7.7 → webbee-0.7.9}/tests/test_modals_e2e.py +0 -0
  120. {webbee-0.7.7 → webbee-0.7.9}/tests/test_mode_and_remote_0_6_8.py +0 -0
  121. {webbee-0.7.7 → webbee-0.7.9}/tests/test_mode_store.py +0 -0
  122. {webbee-0.7.7 → webbee-0.7.9}/tests/test_native_files.py +0 -0
  123. {webbee-0.7.7 → webbee-0.7.9}/tests/test_newtab_mode.py +0 -0
  124. {webbee-0.7.7 → webbee-0.7.9}/tests/test_packaging.py +0 -0
  125. {webbee-0.7.7 → webbee-0.7.9}/tests/test_probe_cost.py +0 -0
  126. {webbee-0.7.7 → webbee-0.7.9}/tests/test_queue_manage_0337.py +0 -0
  127. {webbee-0.7.7 → webbee-0.7.9}/tests/test_reflow.py +0 -0
  128. {webbee-0.7.7 → webbee-0.7.9}/tests/test_remote_code_reads.py +0 -0
  129. {webbee-0.7.7 → webbee-0.7.9}/tests/test_render.py +0 -0
  130. {webbee-0.7.7 → webbee-0.7.9}/tests/test_repl.py +0 -0
  131. {webbee-0.7.7 → webbee-0.7.9}/tests/test_repo.py +0 -0
  132. {webbee-0.7.7 → webbee-0.7.9}/tests/test_resilient_http.py +0 -0
  133. {webbee-0.7.7 → webbee-0.7.9}/tests/test_review_fixes_1030.py +0 -0
  134. {webbee-0.7.7 → webbee-0.7.9}/tests/test_session.py +0 -0
  135. {webbee-0.7.7 → webbee-0.7.9}/tests/test_session_review_1030.py +0 -0
  136. {webbee-0.7.7 → webbee-0.7.9}/tests/test_sessions.py +0 -0
  137. {webbee-0.7.7 → webbee-0.7.9}/tests/test_sizing.py +0 -0
  138. {webbee-0.7.7 → webbee-0.7.9}/tests/test_slots.py +0 -0
  139. {webbee-0.7.7 → webbee-0.7.9}/tests/test_steer.py +0 -0
  140. {webbee-0.7.7 → webbee-0.7.9}/tests/test_stream.py +0 -0
  141. {webbee-0.7.7 → webbee-0.7.9}/tests/test_tab_bar_stability.py +0 -0
  142. {webbee-0.7.7 → webbee-0.7.9}/tests/test_tab_restore_0337.py +0 -0
  143. {webbee-0.7.7 → webbee-0.7.9}/tests/test_tabs.py +0 -0
  144. {webbee-0.7.7 → webbee-0.7.9}/tests/test_terminal_ux_0336.py +0 -0
  145. {webbee-0.7.7 → webbee-0.7.9}/tests/test_thread.py +0 -0
  146. {webbee-0.7.7 → webbee-0.7.9}/tests/test_tier_invalidation.py +0 -0
  147. {webbee-0.7.7 → webbee-0.7.9}/tests/test_tier_store.py +0 -0
  148. {webbee-0.7.7 → webbee-0.7.9}/tests/test_todo_panel.py +0 -0
  149. {webbee-0.7.7 → webbee-0.7.9}/tests/test_tokens.py +0 -0
  150. {webbee-0.7.7 → webbee-0.7.9}/tests/test_tools.py +0 -0
  151. {webbee-0.7.7 → webbee-0.7.9}/tests/test_tools_eol_cancel_1030.py +0 -0
  152. {webbee-0.7.7 → webbee-0.7.9}/tests/test_tools_vision_websearch.py +0 -0
  153. {webbee-0.7.7 → webbee-0.7.9}/tests/test_tui.py +0 -0
  154. {webbee-0.7.7 → webbee-0.7.9}/tests/test_tui_hardening.py +0 -0
  155. {webbee-0.7.7 → webbee-0.7.9}/tests/test_update.py +0 -0
  156. {webbee-0.7.7 → webbee-0.7.9}/tests/test_urlopen.py +0 -0
  157. {webbee-0.7.7 → webbee-0.7.9}/tests/test_version.py +0 -0
  158. {webbee-0.7.7 → webbee-0.7.9}/tests/test_version_badge_corner.py +0 -0
  159. {webbee-0.7.7 → webbee-0.7.9}/tests/test_wallet.py +0 -0
  160. {webbee-0.7.7 → webbee-0.7.9}/tests/test_worktrees.py +0 -0
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.9
4
+
5
+ Field report 06.11 #3 (a folder of nine repositories on Kubuntu):
6
+ * A failed `checkpoint` says why: the 30 s budget of a big folder, or git's own error. Before, every failure read
7
+ "checkpoint failed (shadow git error)".
8
+ * An upgrade while a session runs (pipx, uv, another terminal) is shown in the toolbar until you restart webbee.
9
+ Before, the old process ran on and `webbee --version` read the new one from disk.
10
+ * Needs imperal-mcp 0.9.6 and imperal-sdk 6.5.3. With them, `contract_slice` reaches endpoints through their
11
+ handlers' calls, and `graph_slice` / `contract_slice` work at a folder of repositories.
12
+ * The kernel side of the same report: co-changed files stand apart from real users in `impact_of_change`,
13
+ `search_code` follows one score, `orient` shows matches first, repository rows are named, `contract_slice` is in
14
+ the agent's tools, and `checkpoint` / `diff` / `rollback` come with the code tools.
15
+
16
+ ## 0.7.8
17
+
18
+ * The webbee beside the shell's imperal-mcp (its code tools) follows webbee's releases. Live: webbee 0.7.7 was out,
19
+ imperal-mcp had no new release, and its environment kept webbee 0.7.6 because only imperal-mcp's version was
20
+ compared.
21
+
3
22
  ## 0.7.7
4
23
 
5
24
  * `pipx reinstall webbee` brings the newest release for real (live check of 0.7.6: it still gave 0.7.5).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: webbee
3
- Version: 0.7.7
3
+ Version: 0.7.9
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
@@ -31,8 +31,8 @@ Requires-Python: >=3.7
31
31
  Requires-Dist: asyncssh>=2.14; python_version >= '3.10'
32
32
  Requires-Dist: httpx-sse>=0.4; python_version >= '3.10'
33
33
  Requires-Dist: httpx>=0.27; python_version >= '3.10'
34
- Requires-Dist: imperal-mcp>=0.9.5; python_version >= '3.10'
35
- Requires-Dist: imperal-sdk[universal-index]>=6.5.2; python_version >= '3.10'
34
+ Requires-Dist: imperal-mcp>=0.9.6; python_version >= '3.10'
35
+ Requires-Dist: imperal-sdk[universal-index]>=6.5.3; python_version >= '3.10'
36
36
  Requires-Dist: model2vec>=0.8.2; python_version >= '3.10' and (sys_platform != 'darwin' or platform_machine != 'x86_64' or python_version < '3.13')
37
37
  Requires-Dist: numpy>=2; python_version >= '3.10' and (sys_platform != 'darwin' or platform_machine != 'x86_64' or python_version < '3.13')
38
38
  Requires-Dist: prompt-toolkit>=3; python_version >= '3.10'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webbee"
3
- version = "0.7.7"
3
+ version = "0.7.9"
4
4
  description = "Webbee 🐝 — the Imperal Cloud coding agent in your terminal"
5
5
  readme = "README.md"
6
6
  license = "GPL-3.0-or-later"
@@ -13,8 +13,8 @@ dependencies = [
13
13
  # release on a Python 3.12 it sets up once with uv -- `uv` comes as a wheel on 3.8+,
14
14
  # 3.7 fetches it with uv's own installer.
15
15
  'uv>=0.4; python_version >= "3.8" and python_version < "3.10"',
16
- 'imperal-mcp>=0.9.5; python_version >= "3.10"', # raised to the newest at each release
17
- 'imperal-sdk[universal-index]>=6.5.2; python_version >= "3.10"',
16
+ 'imperal-mcp>=0.9.6; python_version >= "3.10"', # raised to the newest at each release
17
+ 'imperal-sdk[universal-index]>=6.5.3; python_version >= "3.10"',
18
18
  'asyncssh>=2.14; python_version >= "3.10"',
19
19
  'httpx>=0.27; python_version >= "3.10"',
20
20
  'httpx-sse>=0.4; python_version >= "3.10"',
@@ -0,0 +1 @@
1
+ __version__ = "0.7.9"
@@ -243,16 +243,17 @@ def mcp_installed(which=safe_which, run=subprocess.run) -> dict:
243
243
  real = os.path.realpath(exe)
244
244
  py = os.path.join(os.path.dirname(real), "python.exe" if _WINDOWS else "python")
245
245
  out = {"tool": tool_of(real), "python": py, "prefix": os.path.dirname(os.path.dirname(real)), "version": "",
246
- "code": False}
246
+ "code": False, "webbee": ""}
247
247
  # asked twice when it says "no code extra": a reinstall is never made on one odd answer (live 2026-10-25: right after
248
248
  # another install the first answer said no webbee, the next ones yes)
249
249
  for _ in range(2):
250
250
  try:
251
251
  got = run([py, "-c", _NO_CWD + "import importlib.metadata as m, importlib.util as u; "
252
- "print(m.version('imperal-mcp'), u.find_spec('webbee') is not None)"],
252
+ "w = u.find_spec('webbee') is not None; "
253
+ "print(m.version('imperal-mcp'), w, m.version('webbee') if w else '')"],
253
254
  capture_output=True, text=True, timeout=20)
254
- version, code = (str(got.stdout or "").split() + ["", ""])[:2]
255
- out.update(version=version, code=code == "True")
255
+ version, code, webbee = (str(got.stdout or "").split() + ["", "", ""])[:3]
256
+ out.update(version=version, code=code == "True", webbee=webbee)
256
257
  except Exception:
257
258
  pass
258
259
  if out["code"] or not out["version"]:
@@ -260,14 +261,19 @@ def mcp_installed(which=safe_which, run=subprocess.run) -> dict:
260
261
  return out
261
262
 
262
263
 
263
- def mcp_command(inst: dict, newest, uv, pipx, wheel=None, source: str = "pypi"):
264
+ def mcp_command(inst: dict, newest, uv, pipx, wheel=None, source: str = "pypi", webbee_newest=None):
264
265
  """The one command that makes the shell's imperal-mcp the newest with its code extra -- by the tool that installed
265
266
  it (uv, else pipx, for a new install) -- or None when it already is, or nothing can do it. ``source`` "git": from
266
267
  the stable branches, imperal-sdk and webbee beside it, and an install recorded by a version is moved onto the
267
- branch even when its version is the newest (what ``pipx reinstall`` / ``uv tool upgrade`` would put back)."""
268
+ branch even when its version is the newest (what ``pipx reinstall`` / ``uv tool upgrade`` would put back). The
269
+ webbee beside it (its code tools) follows webbee's own releases: one newer than it holds upgrades the environment
270
+ too (live 2026-11-06: webbee 0.7.7 out, imperal-mcp unchanged, its environment kept webbee 0.7.6)."""
268
271
  git = source == "git"
269
272
  recorded = bool(inst) and on_stable(inst.get("tool") or "", inst.get("prefix") or "", MCP_ENV_SPECS)
270
- if inst and inst.get("version") and inst.get("version") == newest and inst.get("code") and (recorded or not git):
273
+ behind = git and bool(inst) and bool(inst.get("webbee")) and bool(webbee_newest) \
274
+ and _ver(inst.get("webbee")) < _ver(webbee_newest)
275
+ if inst and inst.get("version") and inst.get("version") == newest and inst.get("code") and (recorded or not git) \
276
+ and not behind:
271
277
  return None
272
278
  if inst and not inst.get("version"):
273
279
  return None # it did not answer: a working install is never replaced blind
@@ -512,9 +518,13 @@ def upgrade_before_start(argv, current: str, say=print, fetch=latest, which=safe
512
518
  say(f"🐝 webbee could not update itself -- run: {_show(cmd)}")
513
519
  mcp_newest, mcp_source = _newest(MCP, git, fetch, stable)
514
520
  inst = mcp_installed(which, run)
515
- cmd = mcp_command(inst, mcp_newest, uv, pipx, wheel, source=mcp_source)
521
+ cmd = mcp_command(inst, mcp_newest, uv, pipx, wheel, source=mcp_source,
522
+ webbee_newest=newest if source == "git" else None)
516
523
  if cmd:
517
- what = (f"imperal-mcp {inst.get('version') or '?'} -> {mcp_newest or 'source'}" if inst
524
+ what = (f"imperal-mcp {inst.get('version') or '?'} -> {mcp_newest or 'source'}"
525
+ + (f", webbee beside it {inst.get('webbee')} -> {newest}"
526
+ if source == "git" and inst.get("webbee") and _ver(inst.get("webbee")) < _ver(newest) else "")
527
+ if inst
518
528
  else f"imperal-mcp {mcp_newest or 'from source'}") + (" (git, stable)" if mcp_source == "git" else "")
519
529
  say(f"🐝 updating {what} ...")
520
530
  if _run(cmd, run):
@@ -75,6 +75,12 @@ class _Budget(Exception):
75
75
  """The snapshot ran past _SNAPSHOT_BUDGET_S."""
76
76
 
77
77
 
78
+ def _why(r) -> str:
79
+ """A failed git run's own words, one short line."""
80
+ text = " ".join(str(getattr(r, "stderr", "") or getattr(r, "stdout", "") or "").split())
81
+ return (text[:240] or f"git exited {getattr(r, 'returncode', '?')}")
82
+
83
+
78
84
  class ShadowGit:
79
85
  _DIFF_CAP = 20000
80
86
 
@@ -86,6 +92,9 @@ class ShadowGit:
86
92
  self.auto_ok = True # auto-checkpointing enabled (F8 latch below)
87
93
  self._auto_fail_streak = 0 # consecutive failed AUTO snapshots
88
94
  self.pause_reason = "" # why auto-checkpointing stopped at once (a too-slow snapshot)
95
+ # why the last checkpoint failed, said to the agent (field report 06.11 #3: five "checkpoint failed (shadow git
96
+ # error)" at a folder of nine repositories, the cause -- a budget, a git error -- never shown)
97
+ self.last_error = ""
89
98
 
90
99
  def _git(self, *args: str) -> "subprocess.CompletedProcess":
91
100
  return self._git_io(list(args))
@@ -243,15 +252,20 @@ class ShadowGit:
243
252
  keep, nested, uncovered = self._collect(deadline)
244
253
  r = self._git_io(["ls-files", "-z", "-c"], deadline=deadline)
245
254
  if r.returncode != 0:
255
+ self.last_error = f"listing the shadow index failed: {_why(r)}"
246
256
  return False, nested, uncovered
247
257
  gone = sorted(set(_split_z(r.stdout)) - set(keep))
248
258
  for i in range(0, len(gone), _CHUNK): # first: a gitlink must leave before its files come in
249
- if self._git_io(["update-index", "-z", "--force-remove", "--stdin"],
250
- stdin="\0".join(gone[i:i + _CHUNK]) + "\0", deadline=deadline).returncode != 0:
259
+ g = self._git_io(["update-index", "-z", "--force-remove", "--stdin"],
260
+ stdin="\0".join(gone[i:i + _CHUNK]) + "\0", deadline=deadline)
261
+ if g.returncode != 0:
262
+ self.last_error = f"removing gone files from the snapshot failed: {_why(g)}"
251
263
  return False, nested, uncovered
252
264
  for i in range(0, len(keep), _CHUNK):
253
- if self._git_io(["update-index", "-z", "--add", "--remove", "--stdin"],
254
- stdin="\0".join(keep[i:i + _CHUNK]) + "\0", deadline=deadline).returncode != 0:
265
+ a = self._git_io(["update-index", "-z", "--add", "--remove", "--stdin"],
266
+ stdin="\0".join(keep[i:i + _CHUNK]) + "\0", deadline=deadline)
267
+ if a.returncode != 0:
268
+ self.last_error = f"adding {len(keep)} files to the snapshot failed: {_why(a)}"
255
269
  return False, nested, uncovered
256
270
  return True, nested, uncovered
257
271
 
@@ -294,7 +308,9 @@ class ShadowGit:
294
308
  `force=True` always records a new checkpoint even with nothing dirty
295
309
  -- used by rollback() for its mandatory pre-rollback safety snapshot,
296
310
  which must exist even when the workspace already matches HEAD."""
311
+ self.last_error = ""
297
312
  if not self.available:
313
+ self.last_error = "the shadow history could not be set up for this workspace"
298
314
  return None
299
315
  try:
300
316
  deadline = time.monotonic() + _SNAPSHOT_BUDGET_S
@@ -320,6 +336,7 @@ class ShadowGit:
320
336
  args.append("--allow-empty") # first-ever baseline / forced snapshot
321
337
  c = self._git(*args)
322
338
  if c.returncode != 0:
339
+ self.last_error = f"the snapshot commit failed: {_why(c)}"
323
340
  return None
324
341
  sha = self._git("rev-parse", "--short", "HEAD").stdout.strip()
325
342
  n = self._count() + 1
@@ -332,8 +349,11 @@ class ShadowGit:
332
349
  self._drop_stale_lock()
333
350
  self.auto_ok = False
334
351
  self.pause_reason = (f"a snapshot of this workspace took longer than {int(_SNAPSHOT_BUDGET_S)}s")
352
+ self.last_error = (self.pause_reason + " (the staged part is kept: checkpoint again to go on, or "
353
+ "checkpoint inside one repository of this folder)")
335
354
  return None
336
- except Exception:
355
+ except Exception as exc:
356
+ self.last_error = f"{type(exc).__name__}: {str(exc)[:200]}"
337
357
  return None
338
358
 
339
359
  def list_checkpoints(self, limit: int = 10) -> list:
@@ -669,6 +669,28 @@ async def _live_session_watch(cfg, token_provider, workspace, live: dict,
669
669
  await _sleep(interval)
670
670
 
671
671
 
672
+ async def _installed_watch(update_state: dict, *, invalidate=None, every: float = 300.0, read=None,
673
+ sleep=None) -> None:
674
+ """Every few minutes: the webbee on disk against the one running -- an upgrade during the session (pipx, uv, another
675
+ terminal's start) is said in the toolbar until the restart (field report 06.11 #3). Never raises."""
676
+ from webbee import __version__
677
+ from webbee.update import installed_version, restart_notice
678
+ read = read or installed_version
679
+ sleep = sleep or asyncio.sleep
680
+ while True:
681
+ try:
682
+ await sleep(every)
683
+ notice = restart_notice(__version__, await asyncio.to_thread(read))
684
+ if notice and update_state.get("notice") != notice:
685
+ update_state["notice"] = notice
686
+ if invalidate is not None:
687
+ invalidate()
688
+ except asyncio.CancelledError:
689
+ raise
690
+ except Exception:
691
+ pass
692
+
693
+
672
694
  async def _check_update_bg(update_state: dict, *, invalidate=None,
673
695
  fetch=None, cache_path=None) -> None:
674
696
  """One-shot background PyPI freshness check for the toolbar's version
@@ -1928,6 +1950,8 @@ async def run_repl(cfg, mode: str = "default", *, once: bool = False, sink=None,
1928
1950
  _update_state: dict = {"notice": "", "checked": None}
1929
1951
  home_slot.bg_tasks.append(asyncio.ensure_future(
1930
1952
  _check_update_bg(_update_state, invalidate=_invalidate_dock)))
1953
+ home_slot.bg_tasks.append(asyncio.ensure_future(
1954
+ _installed_watch(_update_state, invalidate=_invalidate_dock)))
1931
1955
 
1932
1956
  async def _on_line(text: str, slot: SessionSlot) -> None:
1933
1957
  if await _handle(text, slot) == "exit":
@@ -724,7 +724,8 @@ class LocalToolExecutor:
724
724
  return {"ok": False, "content": "reversibility is unavailable (no shadow git)"}
725
725
  cp = self.shadow.checkpoint(str(a.get("label", "") or "manual"))
726
726
  if cp is None:
727
- return {"ok": False, "content": "checkpoint failed (shadow git error)"}
727
+ why = str(getattr(self.shadow, "last_error", "") or "") or "shadow git error"
728
+ return {"ok": False, "content": f"checkpoint failed: {why}"}
728
729
  note = "created" if cp.get("changed") else "no changes since the last checkpoint"
729
730
  # what it holds and what it does not, said (2026-10-30: a checkpoint of a folder of repositories held none of
730
731
  # their files and said "created")
@@ -80,3 +80,27 @@ def check_for_update(current: str, *, cache_path, now: float, fetch, ttl: float
80
80
  Kept as-is (boot's banner path); `check_update_status` adds the verdict."""
81
81
  return _notice_for(current, _resolve_latest(
82
82
  current, cache_path=cache_path, now=now, fetch=fetch, ttl=ttl)) or None
83
+
84
+
85
+ def installed_version(python: str = "", run=None) -> str:
86
+ """The webbee version on disk now, read by a fresh interpreter (this process's own metadata is what it started
87
+ with). "" when it cannot be read. Never raises."""
88
+ import subprocess
89
+ import sys
90
+ run = run or subprocess.run
91
+ try:
92
+ got = run([python or sys.executable, "-c", "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; "
93
+ "import importlib.metadata as m; print(m.version('webbee'))"],
94
+ capture_output=True, text=True, timeout=20)
95
+ return str(got.stdout or "").strip() if got.returncode == 0 else ""
96
+ except Exception:
97
+ return ""
98
+
99
+
100
+ def restart_notice(running: str, installed: str) -> str:
101
+ """Field report 06.11 #3: a pipx upgrade during a session left the old process running, and `webbee --version`
102
+ (read from disk) said the new one. Said in the toolbar until the session restarts."""
103
+ if installed and _ver(installed) and _ver(installed) != _ver(running):
104
+ return (f"\U0001f41d webbee v{installed} is installed, this session still runs v{running} "
105
+ "\u2014 restart webbee to use it")
106
+ return ""
@@ -469,3 +469,21 @@ def test_a_git_release_that_does_not_import_is_put_back_by_its_own_tag(tmp_path,
469
469
  stable=lambda d, git: "0.7.5", prefix=venv)
470
470
  assert ["/bin/pipx", "install", "--force", au.git_spec("webbee", "v0.7.4")] in run.calls
471
471
  assert any("put 0.7.4 back" in s for s in said)
472
+
473
+
474
+ def test_the_webbee_beside_imperal_mcp_follows_webbee_s_releases(tmp_path):
475
+ uv = tmp_path / "uv" / "tools" / "imperal-mcp"
476
+ uv.mkdir(parents=True)
477
+ (uv / "uv-receipt.toml").write_text("".join(
478
+ f'{{ name = "x", git = "{u}?rev=stable" }}\n' for u in au.REPOS.values()))
479
+ inst = {"tool": "uv", "python": str(uv / "bin" / "python"), "prefix": str(uv), "version": "0.9.5", "code": True,
480
+ "webbee": "0.7.6"}
481
+ assert au.mcp_command(inst, "0.9.5", "/bin/uv", None, source="git", webbee_newest="0.7.6") is None
482
+ assert au.mcp_command(inst, "0.9.5", "/bin/uv", None, source="git", webbee_newest="0.7.7") == [
483
+ "/bin/uv", "tool", "install", "--force", "--refresh", "--with", SDK_G, "--with", WB, MCP_G]
484
+ run = _Run(stdout="0.9.5 True 0.7.6")
485
+ exe = uv / "bin" / "imperal-mcp"
486
+ exe.parent.mkdir()
487
+ exe.write_text("")
488
+ got = au.mcp_installed(which=lambda n: str(exe), run=run)
489
+ assert got["webbee"] == "0.7.6" and got["code"] is True
@@ -309,3 +309,26 @@ def test_a_path_keeps_its_first_letter_when_the_first_line_is_unstaged(tmp_path,
309
309
  return real(*args)
310
310
  monkeypatch.setattr(sg, "_git", fake)
311
311
  assert sg.checkpoint("x")["paths"] == ["imperal_kernel/x.py", "tests/t.py"]
312
+
313
+
314
+ # ── field report 06.11 #3: "checkpoint failed (shadow git error)" five times, its cause never shown ──
315
+ def test_a_failed_checkpoint_says_why(tmp_path, monkeypatch):
316
+ from webbee import checkpoints as cpm
317
+ from webbee.tools import LocalToolExecutor
318
+ sg, root = _sg(tmp_path)
319
+ _write(root, "a.py", "x = 1\n")
320
+
321
+ def slow(deadline):
322
+ raise cpm._Budget()
323
+ monkeypatch.setattr(sg, "_stage", slow)
324
+ assert sg.checkpoint("x") is None and "took longer than" in sg.last_error
325
+ out = LocalToolExecutor(str(root), shadow=sg).run("checkpoint", {"label": "x"})
326
+ assert out["ok"] is False and "took longer than" in out["content"] and "shadow git error" not in out["content"]
327
+
328
+ def git_fails(deadline):
329
+ sg.last_error = "adding 3 files to the snapshot failed: fatal: unable to index file"
330
+ return False, [], []
331
+ monkeypatch.setattr(sg, "_stage", git_fails)
332
+ assert sg.checkpoint("y") is None and "unable to index file" in sg.last_error
333
+ monkeypatch.undo()
334
+ assert sg.checkpoint("z") and sg.last_error == "" # a good one clears it
@@ -68,3 +68,27 @@ def test_check_update_bg_reuses_the_same_cache_file_home_uses(tmp_path):
68
68
  asyncio.run(_check_update_bg(state2, fetch=_fetch, cache_path=cache))
69
69
  assert calls["n"] == 1
70
70
  assert state2["checked"] is True
71
+
72
+
73
+ # ── field report 06.11 #3: upgraded during the session, the old process ran on unsaid ──
74
+ def test_an_upgrade_during_the_session_is_said_until_the_restart():
75
+ import asyncio
76
+
77
+ from webbee import __version__
78
+ from webbee.repl import _installed_watch
79
+ from webbee.update import installed_version, restart_notice
80
+ assert restart_notice("0.7.8", "0.7.9").startswith("\U0001f41d webbee v0.7.9 is installed, this session still runs v0.7.8")
81
+ assert restart_notice("0.7.8", "0.7.8") == "" and restart_notice("0.7.8", "") == ""
82
+ assert installed_version() # this test's own environment answers
83
+ state, said = {"notice": ""}, []
84
+ ticks = iter([None, asyncio.CancelledError()])
85
+
86
+ async def sleep(_s):
87
+ t = next(ticks)
88
+ if t is not None:
89
+ raise t
90
+ try:
91
+ asyncio.run(_installed_watch(state, invalidate=lambda: said.append(1), read=lambda: "99.0.0", sleep=sleep))
92
+ except asyncio.CancelledError:
93
+ pass
94
+ assert f"v99.0.0 is installed, this session still runs v{__version__}" in state["notice"] and said
@@ -1 +0,0 @@
1
- __version__ = "0.7.7"
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
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