webbee 0.4.2__tar.gz → 0.4.4__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 (142) hide show
  1. {webbee-0.4.2 → webbee-0.4.4}/CHANGELOG.md +50 -0
  2. {webbee-0.4.2 → webbee-0.4.4}/PKG-INFO +2 -4
  3. {webbee-0.4.2 → webbee-0.4.4}/README.md +1 -3
  4. {webbee-0.4.2 → webbee-0.4.4}/pyproject.toml +1 -1
  5. webbee-0.4.4/src/webbee/__init__.py +1 -0
  6. webbee-0.4.4/src/webbee/__main__.py +5 -0
  7. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/cli.py +4 -0
  8. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/config.py +4 -0
  9. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/frames.py +1 -1
  10. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/modals.py +78 -15
  11. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/native_read.py +26 -6
  12. webbee-0.4.4/src/webbee/pricing_constants.py +68 -0
  13. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/render.py +61 -35
  14. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/repl.py +3 -1
  15. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/session.py +28 -0
  16. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/tui.py +42 -70
  17. webbee-0.4.4/tests/test_cross_loop_token.py +54 -0
  18. {webbee-0.4.2 → webbee-0.4.4}/tests/test_repl.py +7 -5
  19. {webbee-0.4.2 → webbee-0.4.4}/tests/test_tui.py +23 -11
  20. webbee-0.4.2/src/webbee/__init__.py +0 -1
  21. {webbee-0.4.2 → webbee-0.4.4}/.github/workflows/publish.yml +0 -0
  22. {webbee-0.4.2 → webbee-0.4.4}/.gitignore +0 -0
  23. {webbee-0.4.2 → webbee-0.4.4}/LICENSE +0 -0
  24. {webbee-0.4.2 → webbee-0.4.4}/install.sh +0 -0
  25. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/account.py +0 -0
  26. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/active_sessions.py +0 -0
  27. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/banner_art.py +0 -0
  28. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/boot.py +0 -0
  29. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/checkpoints.py +0 -0
  30. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/clipboard.py +0 -0
  31. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/clipboard_read.py +0 -0
  32. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/clipboard_session.py +0 -0
  33. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/coding_context.py +0 -0
  34. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/commands.py +0 -0
  35. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/consent.py +0 -0
  36. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/details.py +0 -0
  37. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/events.py +0 -0
  38. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/home.py +0 -0
  39. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/home_view.py +0 -0
  40. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/http.py +0 -0
  41. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/instance_lock.py +0 -0
  42. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/__init__.py +0 -0
  43. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/chunker.py +0 -0
  44. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/contracts.py +0 -0
  45. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/embed.py +0 -0
  46. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/graph.py +0 -0
  47. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/indexer.py +0 -0
  48. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/models.py +0 -0
  49. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/query.py +0 -0
  50. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/service.py +0 -0
  51. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/store.py +0 -0
  52. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/vectors.py +0 -0
  53. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/watch.py +0 -0
  54. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/media.py +0 -0
  55. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/mode_store.py +0 -0
  56. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/native_files.py +0 -0
  57. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/newtab_mode.py +0 -0
  58. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/output_pane.py +0 -0
  59. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/queue_panel.py +0 -0
  60. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/reflow.py +0 -0
  61. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/remote.py +0 -0
  62. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/repo.py +0 -0
  63. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/selection.py +0 -0
  64. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/sessions.py +0 -0
  65. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/sizing.py +0 -0
  66. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/slots.py +0 -0
  67. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/steer.py +0 -0
  68. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/stream.py +0 -0
  69. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/tab_store.py +0 -0
  70. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/tabs.py +0 -0
  71. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/thread.py +0 -0
  72. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/tier_store.py +0 -0
  73. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/todo_panel.py +0 -0
  74. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/tokens.py +0 -0
  75. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/tools.py +0 -0
  76. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/update.py +0 -0
  77. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/urlopen.py +0 -0
  78. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/wallet.py +0 -0
  79. {webbee-0.4.2 → webbee-0.4.4}/src/webbee/worktrees.py +0 -0
  80. {webbee-0.4.2 → webbee-0.4.4}/tests/__init__.py +0 -0
  81. {webbee-0.4.2 → webbee-0.4.4}/tests/conftest.py +0 -0
  82. {webbee-0.4.2 → webbee-0.4.4}/tests/test_account.py +0 -0
  83. {webbee-0.4.2 → webbee-0.4.4}/tests/test_active_sessions.py +0 -0
  84. {webbee-0.4.2 → webbee-0.4.4}/tests/test_audit_2026_07_25_findings.py +0 -0
  85. {webbee-0.4.2 → webbee-0.4.4}/tests/test_checkpoints.py +0 -0
  86. {webbee-0.4.2 → webbee-0.4.4}/tests/test_cli.py +0 -0
  87. {webbee-0.4.2 → webbee-0.4.4}/tests/test_clipboard.py +0 -0
  88. {webbee-0.4.2 → webbee-0.4.4}/tests/test_clipboard_read.py +0 -0
  89. {webbee-0.4.2 → webbee-0.4.4}/tests/test_coding_context.py +0 -0
  90. {webbee-0.4.2 → webbee-0.4.4}/tests/test_commands.py +0 -0
  91. {webbee-0.4.2 → webbee-0.4.4}/tests/test_config.py +0 -0
  92. {webbee-0.4.2 → webbee-0.4.4}/tests/test_cpc_contract_stable.py +0 -0
  93. {webbee-0.4.2 → webbee-0.4.4}/tests/test_details.py +0 -0
  94. {webbee-0.4.2 → webbee-0.4.4}/tests/test_events.py +0 -0
  95. {webbee-0.4.2 → webbee-0.4.4}/tests/test_freeze_fix.py +0 -0
  96. {webbee-0.4.2 → webbee-0.4.4}/tests/test_home.py +0 -0
  97. {webbee-0.4.2 → webbee-0.4.4}/tests/test_home_view.py +0 -0
  98. {webbee-0.4.2 → webbee-0.4.4}/tests/test_instance_lock.py +0 -0
  99. {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_chunker.py +0 -0
  100. {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_contracts.py +0 -0
  101. {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_embed.py +0 -0
  102. {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_graph.py +0 -0
  103. {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_indexer.py +0 -0
  104. {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_no_numpy.py +0 -0
  105. {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_query.py +0 -0
  106. {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_service.py +0 -0
  107. {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_store.py +0 -0
  108. {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_vectors.py +0 -0
  109. {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_watch.py +0 -0
  110. {webbee-0.4.2 → webbee-0.4.4}/tests/test_marathon.py +0 -0
  111. {webbee-0.4.2 → webbee-0.4.4}/tests/test_modals_e2e.py +0 -0
  112. {webbee-0.4.2 → webbee-0.4.4}/tests/test_mode_store.py +0 -0
  113. {webbee-0.4.2 → webbee-0.4.4}/tests/test_native_files.py +0 -0
  114. {webbee-0.4.2 → webbee-0.4.4}/tests/test_newtab_mode.py +0 -0
  115. {webbee-0.4.2 → webbee-0.4.4}/tests/test_packaging.py +0 -0
  116. {webbee-0.4.2 → webbee-0.4.4}/tests/test_queue_manage_0337.py +0 -0
  117. {webbee-0.4.2 → webbee-0.4.4}/tests/test_reflow.py +0 -0
  118. {webbee-0.4.2 → webbee-0.4.4}/tests/test_render.py +0 -0
  119. {webbee-0.4.2 → webbee-0.4.4}/tests/test_repo.py +0 -0
  120. {webbee-0.4.2 → webbee-0.4.4}/tests/test_session.py +0 -0
  121. {webbee-0.4.2 → webbee-0.4.4}/tests/test_sessions.py +0 -0
  122. {webbee-0.4.2 → webbee-0.4.4}/tests/test_sizing.py +0 -0
  123. {webbee-0.4.2 → webbee-0.4.4}/tests/test_slots.py +0 -0
  124. {webbee-0.4.2 → webbee-0.4.4}/tests/test_steer.py +0 -0
  125. {webbee-0.4.2 → webbee-0.4.4}/tests/test_stream.py +0 -0
  126. {webbee-0.4.2 → webbee-0.4.4}/tests/test_tab_restore_0337.py +0 -0
  127. {webbee-0.4.2 → webbee-0.4.4}/tests/test_tabs.py +0 -0
  128. {webbee-0.4.2 → webbee-0.4.4}/tests/test_terminal_ux_0336.py +0 -0
  129. {webbee-0.4.2 → webbee-0.4.4}/tests/test_thread.py +0 -0
  130. {webbee-0.4.2 → webbee-0.4.4}/tests/test_tier_store.py +0 -0
  131. {webbee-0.4.2 → webbee-0.4.4}/tests/test_todo_panel.py +0 -0
  132. {webbee-0.4.2 → webbee-0.4.4}/tests/test_tokens.py +0 -0
  133. {webbee-0.4.2 → webbee-0.4.4}/tests/test_tools.py +0 -0
  134. {webbee-0.4.2 → webbee-0.4.4}/tests/test_tools_vision_websearch.py +0 -0
  135. {webbee-0.4.2 → webbee-0.4.4}/tests/test_tui_hardening.py +0 -0
  136. {webbee-0.4.2 → webbee-0.4.4}/tests/test_update.py +0 -0
  137. {webbee-0.4.2 → webbee-0.4.4}/tests/test_update_badge_0340.py +0 -0
  138. {webbee-0.4.2 → webbee-0.4.4}/tests/test_urlopen.py +0 -0
  139. {webbee-0.4.2 → webbee-0.4.4}/tests/test_version.py +0 -0
  140. {webbee-0.4.2 → webbee-0.4.4}/tests/test_version_badge_corner.py +0 -0
  141. {webbee-0.4.2 → webbee-0.4.4}/tests/test_wallet.py +0 -0
  142. {webbee-0.4.2 → webbee-0.4.4}/tests/test_worktrees.py +0 -0
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.4
4
+
5
+ **Webbee v0.4.4: Clean Tool Rendering, Real-Time Action Cost Estimation & Terminal Robustness.**
6
+
7
+ This release streamlines the Terminal Coding Experience (TUI) with modernized action indicators, integrates real-time credit cost previews for system and connector actions, passes rich tool invocation arguments directly to stream frames, and polishes CLI/REPL startup and execution ergonomics.
8
+
9
+ ### Highlights & New Capabilities
10
+
11
+ * **Streamlined Tool & Action Rendering (`webbee.render`)**:
12
+ - Replaced legacy multi-step badges with unified, high-contrast action indicators (`SSH Exec`, `Read File`, `Edit File`, `Run Shell`, `Web Search`, etc.).
13
+ - Added dedicated styling and icons for remote connector calls (`conn-ssh`, `wordpress-hub`, `stripe-connector`, `mail`, `tasks`, etc.).
14
+ - Clean tool start/finish status feedback without redundant UI step wrappers.
15
+
16
+ * **Real-Time Action Cost Transparency (`webbee.pricing_constants`)**:
17
+ - Introduced transparent action cost estimation for external and cloud operations directly in the terminal interface.
18
+ - Zero-credit badge display for local disk and memory operations (`0.00 cr`), with accurate pricing for remote server execution and marketplace connectors.
19
+
20
+ * **Rich Frame Argument Propagation (`webbee.frames`)**:
21
+ - Enhanced `handle_step_started` to forward invocation parameters (`args`) into tool lifecycle hooks, providing instant visibility into file paths, target hosts, and search queries.
22
+
23
+ * **CLI & Module Execution Polish (`webbee.cli`, `webbee.__main__`)**:
24
+ - Added native `python -m webbee` execution entry point via `__main__.py`.
25
+ - Added direct execution guard to `cli.py` for streamlined standalone launches.
26
+
27
+ ## 0.4.3
28
+
29
+ **Webbee v0.4.3: Non-Blocking Voice STT, Cross-Event-Loop Token Bridge, Dynamic Composer Geometry & Config Hardening.**
30
+
31
+ This release makes voice notes truly seamless by decoupling modal teardown from transcription (zero TUI freezes), adds a thread-safe cross-event-loop token bridge for worker-thread file reading across Python 3.6 - 3.14+, accurately aligns multi-line composer wrap bounds to prevent prompt overflow, and eliminates filesystem path hardcoding.
32
+
33
+ ### Highlights & New Capabilities
34
+
35
+ * **Non-Blocking Background Voice STT (`webbee.modals`)**:
36
+ - The voice recording dialog now dismisses immediately upon clicking `[▶ Send]`, releasing the UI modal lock with zero delay.
37
+ - Displays a clean transient status note (`🎙️ transcribing voice note…`) in the output pane while background audio decoding executes.
38
+ - Audio transcription via `/v1/voice/stt` runs asynchronously in a worker thread and safely schedules its result back to prompt_toolkit via `loop.call_soon_threadsafe`.
39
+
40
+ * **Cross-Event-Loop Token Bridge for Native File Reads (`webbee.session`, `webbee.native_read`)**:
41
+ - Resolved `RuntimeError: Task attached to a different loop` when background worker threads invoke `read_native` with the session client.
42
+ - Implemented an `asyncio.run_coroutine_threadsafe` bridge for token provider acquisition across thread boundaries.
43
+ - Universal `_run_sync` helper supporting Python 3.6 through 3.14+ seamlessly.
44
+
45
+ * **Accurate Composer Geometry & Proactive Wrap Calculation (`webbee.tui`)**:
46
+ - Refactored `input_rows` to strictly account for `BeforeInput` prefix buttons (`📎 🎤 ⋮ ❯ `, 16 display columns) and line continuation gutters (`┊ `, 2 display columns).
47
+ - Uses true display-cell widths (`get_cwidth` / `wcwidth`) for Unicode and emojis, ensuring the multi-line input box dynamically grows the exact moment text reaches the frame border rather than hiding behind a 2-4 word scroll lag.
48
+
49
+ * **Configuration & Credential Storage Hardening (`webbee.config`)**:
50
+ - Added dedicated `voice_dir` and `credentials_file` configuration hooks to `Config` with environment overrides (`IMPERAL_VOICE_DIR`, `IMPERAL_CREDENTIALS_FILE`).
51
+ - Removed hardcoded filesystem paths across modals and voice components, respecting session configuration and OS cache directories.
52
+
3
53
  ## 0.4.2
4
54
 
5
55
  **Webbee v0.4.2: Universal `shell` Action Across Windows/macOS/Linux, Interactive TUI Modals, Cloud Voice STT & Complete Hover Engine.**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: webbee
3
- Version: 0.4.2
3
+ Version: 0.4.4
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
@@ -51,7 +51,7 @@ Description-Content-Type: text/markdown
51
51
  # Webbee 🐝 — The Open-Source ICNLI Coding Agent
52
52
 
53
53
  [![PyPI](https://img.shields.io/pypi/v/webbee)](https://pypi.org/project/webbee/)
54
- [![Python](https://img.shields.io/pypi/pyversions/webbee)](https://pypi.org/project/webbee/)
54
+ [![Python](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-3776ab?logo=python&logoColor=white)](https://pypi.org/project/webbee/)
55
55
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)
56
56
  [![Docs](https://img.shields.io/badge/docs-imperal.io-00afd7)](https://docs.imperal.io)
57
57
 
@@ -92,8 +92,6 @@ python3 -m venv .venv && source .venv/bin/activate
92
92
  pip install webbee
93
93
  ```
94
94
 
95
- > **Note**: Python 3.11+ is required.
96
-
97
95
  ---
98
96
 
99
97
  ## 💡 Quick Start
@@ -1,7 +1,7 @@
1
1
  # Webbee 🐝 — The Open-Source ICNLI Coding Agent
2
2
 
3
3
  [![PyPI](https://img.shields.io/pypi/v/webbee)](https://pypi.org/project/webbee/)
4
- [![Python](https://img.shields.io/pypi/pyversions/webbee)](https://pypi.org/project/webbee/)
4
+ [![Python](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-3776ab?logo=python&logoColor=white)](https://pypi.org/project/webbee/)
5
5
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)
6
6
  [![Docs](https://img.shields.io/badge/docs-imperal.io-00afd7)](https://docs.imperal.io)
7
7
 
@@ -42,8 +42,6 @@ python3 -m venv .venv && source .venv/bin/activate
42
42
  pip install webbee
43
43
  ```
44
44
 
45
- > **Note**: Python 3.11+ is required.
46
-
47
45
  ---
48
46
 
49
47
  ## 💡 Quick Start
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webbee"
3
- version = "0.4.2"
3
+ version = "0.4.4"
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"
@@ -0,0 +1 @@
1
+ __version__ = "0.4.4"
@@ -0,0 +1,5 @@
1
+ import sys
2
+ from webbee.cli import main
3
+
4
+ if __name__ == "__main__":
5
+ sys.exit(main())
@@ -127,3 +127,7 @@ def _maybe_print_update_notice() -> None:
127
127
  print(notice)
128
128
  except Exception:
129
129
  pass # update-check must never block or crash startup
130
+
131
+ if __name__ == "__main__":
132
+ import sys
133
+ sys.exit(main())
@@ -7,6 +7,8 @@ class Config:
7
7
  panel_url: str
8
8
  intel_enabled: bool = True # off switch for the on-disk index/watcher (env IMPERAL_INTEL=false)
9
9
  cache_dir: str = "" # "" -> IntelService's own default (~/.cache/webbee/intel)
10
+ voice_dir: str = "" # "" -> ~/.webbee/voice
11
+ credentials_file: str = "" # "" -> ~/.imperal/credentials.json
10
12
 
11
13
  @classmethod
12
14
  def from_env(cls) -> "Config":
@@ -15,4 +17,6 @@ class Config:
15
17
  panel_url=os.environ.get("IMPERAL_PANEL_URL", "https://panel.imperal.io").rstrip("/"),
16
18
  intel_enabled=os.environ.get("IMPERAL_INTEL", "true") != "false",
17
19
  cache_dir=os.environ.get("IMPERAL_INTEL_CACHE_DIR", ""),
20
+ voice_dir=os.environ.get("IMPERAL_VOICE_DIR", ""),
21
+ credentials_file=os.environ.get("IMPERAL_CREDENTIALS_FILE", ""),
18
22
  )
@@ -87,7 +87,7 @@ def handle_step_started(frame: dict, sink, started: set, step_labels: dict, loca
87
87
  if sid:
88
88
  step_labels[sid] = label
89
89
  if _first_time(sid, started):
90
- sink.tool_start(label, {})
90
+ sink.tool_start(label, frame.get("args") or {})
91
91
 
92
92
 
93
93
  def handle_step_finished(frame: dict, sink, finished: set, step_labels: dict, steps: list,
@@ -237,7 +237,7 @@ def get_system_audio_capture_args(output_path: str) -> List[str]:
237
237
  return ["ffmpeg", "-y", "-i", "default", output_path]
238
238
 
239
239
 
240
- def transcribe_audio_file(audio_path: str) -> str:
240
+ def transcribe_audio_file(audio_path: str, api_url: str = "", token: str = "") -> str:
241
241
  """Transcribe audio file via Imperal Cloud System Voice STT endpoint (/v1/voice/stt).
242
242
 
243
243
  Works on ANY OS (macOS, Linux, Windows) seamlessly without local dependencies.
@@ -249,16 +249,25 @@ def transcribe_audio_file(audio_path: str) -> str:
249
249
  try:
250
250
  import json, urllib.request, urllib.error, uuid
251
251
 
252
- cred_path = os.path.expanduser("~/.imperal/credentials.json")
253
- token = ""
254
- if os.path.exists(cred_path):
252
+ if not token:
255
253
  try:
256
- data = json.load(open(cred_path))
257
- token = data.get("access_token", "")
254
+ from webbee.config import Config
255
+ cfg = Config.from_env()
256
+ cred_path = getattr(cfg, "credentials_file", "") or os.path.expanduser("~/.imperal/credentials.json")
257
+ if os.path.exists(cred_path):
258
+ data = json.load(open(cred_path))
259
+ token = data.get("access_token", "")
258
260
  except Exception:
259
261
  pass
260
262
 
261
- api_url = os.environ.get("IMPERAL_API_URL", "https://auth.imperal.io").rstrip("/")
263
+ if not api_url:
264
+ try:
265
+ from webbee.config import Config
266
+ api_url = Config.from_env().api_url
267
+ except Exception:
268
+ api_url = os.environ.get("IMPERAL_API_URL", "https://auth.imperal.io")
269
+
270
+ api_url = api_url.rstrip("/")
262
271
  url = f"{api_url}/v1/voice/stt"
263
272
 
264
273
  with open(audio_path, "rb") as f:
@@ -367,10 +376,22 @@ def create_mic_dialog(
367
376
  on_transcribe: Callable[[str], None],
368
377
  on_send_direct: Callable[[str], None],
369
378
  on_close: Callable[[], None],
379
+ *,
380
+ api_url: str = "",
381
+ token: str = "",
382
+ voice_dir: str = "",
383
+ on_status: Optional[Callable[[str], None]] = None,
370
384
  ) -> Dialog:
371
385
  """Create Voice Recording (STT) dialog matching standard modal design with exactly 2 buttons: [▶ Send] and [✕ Cancel]."""
386
+ import threading
372
387
  start_time = time.time()
373
- voice_dir = os.path.expanduser("~/.webbee/voice")
388
+ if not voice_dir:
389
+ try:
390
+ from webbee.config import Config
391
+ cfg = Config.from_env()
392
+ voice_dir = getattr(cfg, "voice_dir", "") or os.path.expanduser("~/.webbee/voice")
393
+ except Exception:
394
+ voice_dir = os.path.expanduser("~/.webbee/voice")
374
395
  os.makedirs(voice_dir, exist_ok=True)
375
396
  voice_filename = f"voice_{int(time.time())}.wav"
376
397
  voice_path = os.path.join(voice_dir, voice_filename)
@@ -410,13 +431,55 @@ def create_mic_dialog(
410
431
  def _handle_send():
411
432
  rec_seconds = _stop_rec()
412
433
  on_close()
413
- # Direct STT via Imperal Cloud STT: transcribe audio into plain text instruction
414
- text = transcribe_audio_file(voice_path)
415
- if text and text.strip() and text.strip().lower() not in ("you", "thank you", "silence", "."):
416
- on_send_direct(text.strip())
417
- else:
418
- # If STT returns empty or silence, send informative turn
419
- on_send_direct(f"🎙️ [Voice note: {rec_seconds}s audio — {text.strip() if text else 'audio recorded'}]")
434
+
435
+ if on_status is not None:
436
+ try:
437
+ on_status("🎙️ transcribing voice note…")
438
+ except Exception:
439
+ pass
440
+
441
+ def _worker():
442
+ try:
443
+ text = transcribe_audio_file(voice_path, api_url=api_url, token=token)
444
+ def _apply():
445
+ if on_status is not None:
446
+ try:
447
+ on_status("")
448
+ except Exception:
449
+ pass
450
+ if text and text.strip() and text.strip().lower() not in ("you", "thank you", "silence", "."):
451
+ on_send_direct(text.strip())
452
+ else:
453
+ desc = text.strip() if text else "audio recorded"
454
+ on_send_direct(f"🎙️ [Voice note: {rec_seconds}s audio — {desc}]")
455
+
456
+ app = get_app_or_none()
457
+ if app is not None and hasattr(app, "loop") and app.loop is not None and app.loop.is_running():
458
+ app.loop.call_soon_threadsafe(_apply)
459
+ else:
460
+ _apply()
461
+ except Exception:
462
+ app = get_app_or_none()
463
+ def _fail():
464
+ if on_status is not None:
465
+ try:
466
+ on_status("")
467
+ except Exception:
468
+ pass
469
+ on_send_direct(f"🎙️ [Voice note: {rec_seconds}s audio — transcription unavailable]")
470
+ if app is not None and hasattr(app, "loop") and app.loop is not None and app.loop.is_running():
471
+ app.loop.call_soon_threadsafe(_fail)
472
+ else:
473
+ _fail()
474
+ finally:
475
+ try:
476
+ if os.path.exists(voice_path):
477
+ os.remove(voice_path)
478
+ except Exception:
479
+ pass
480
+
481
+ t = threading.Thread(target=_worker, daemon=True)
482
+ t.start()
420
483
 
421
484
  def _handle_cancel():
422
485
  _stop_rec()
@@ -184,9 +184,9 @@ async def read_native_async(client, abs_path: str, rel: str, *,
184
184
  if size > MAX_UPLOAD_BYTES:
185
185
  return {"ok": False, "native": True, "kind": kind,
186
186
  "content": (f"⟦ {rel} · {human_size(size)} · {kind} ⟧\n"
187
- f"Too large to read inline (limit {human_size(MAX_UPLOAD_BYTES)}). "
188
- f"Upload it in the panel's File Reader and read it by file_id, "
189
- f"or point me at a smaller export.")}
187
+ f"File size exceeds inline buffer limit ({human_size(MAX_UPLOAD_BYTES)}). "
188
+ f"To prevent memory bloat and payload limits, use windowed reading with offset/limit, "
189
+ f"inspect metadata via shell, or upload in the panel's File Reader to read by file_id.")}
190
190
 
191
191
  with open(abs_path, "rb") as f:
192
192
  data = f.read()
@@ -253,13 +253,33 @@ def _shape(item: dict, *, rel: str, kind: str, size: int, file_id: str,
253
253
  "truncated": has_more, "total_chars": total}
254
254
 
255
255
 
256
+ def _run_sync(coro):
257
+ """Run a coroutine to completion synchronously in a worker thread.
258
+
259
+ Fully compatible across Python 3.6 - 3.14+:
260
+ * Python 3.7 - 3.14+: uses standard asyncio.run().
261
+ * Python 3.6 fallback: creates a new event loop and runs until complete.
262
+ """
263
+ if hasattr(asyncio, "run"):
264
+ return asyncio.run(coro)
265
+ loop = asyncio.new_event_loop()
266
+ try:
267
+ asyncio.set_event_loop(loop)
268
+ return loop.run_until_complete(coro)
269
+ finally:
270
+ try:
271
+ loop.close()
272
+ finally:
273
+ asyncio.set_event_loop(None)
274
+
275
+
256
276
  def read_native(client_factory, abs_path: str, rel: str, *,
257
277
  offset: int = 0, limit: int = 0) -> dict:
258
278
  """Synchronous entry point for LocalToolExecutor (which runs in a worker
259
279
  thread, never on the event loop -- so a fresh loop here is safe and cannot
260
280
  deadlock the REPL).
261
281
 
262
- ``client_factory`` is a zero-arg callable returning an ImperalClient; it is
282
+ client_factory is a zero-arg callable returning an ImperalClient; it is
263
283
  a factory (not a client) so a plain local run with no cloud reach simply
264
284
  has none, and this whole path degrades to one honest sentence.
265
285
  """
@@ -275,8 +295,8 @@ def read_native(client_factory, abs_path: str, rel: str, *,
275
295
  f"which this session cannot reach right now.")}
276
296
  try:
277
297
  client = client_factory()
278
- return asyncio.run(read_native_async(client, abs_path, rel,
279
- offset=offset, limit=limit))
298
+ return _run_sync(read_native_async(client, abs_path, rel,
299
+ offset=offset, limit=limit))
280
300
  except Exception as e: # noqa: BLE001 -- a read must never crash the turn
281
301
  return {"ok": False, "native": True, "kind": kind, "content": (
282
302
  f"⟦ {rel} · {human_size(size)} · {kind} ⟧\n"
@@ -0,0 +1,68 @@
1
+ """Tool pricing constants and lookup for Webbee actions."""
2
+
3
+ DEFAULT_ACTION_PRICES = {
4
+ # Local workspace operations (always 0.00 cr)
5
+ "read_file": 0.0,
6
+ "write_file": 0.0,
7
+ "edit_file": 0.0,
8
+ "multi_edit": 0.0,
9
+ "diff": 0.0,
10
+ "rollback": 0.0,
11
+ "checkpoint": 0.0,
12
+ "list_dir": 0.0,
13
+ "grep": 0.0,
14
+ "glob": 0.0,
15
+ "search_code": 0.0,
16
+ "graph_slice": 0.0,
17
+ "impact_of_change": 0.0,
18
+ "orient": 0.0,
19
+ "repo_profile": 0.0,
20
+ "view_image": 0.0,
21
+
22
+ # Server / remote / external connector operations
23
+ "conn-ssh": 0.15,
24
+ "bash": 0.05,
25
+ "shell": 0.05,
26
+ "web_search": 0.05,
27
+ "read_url": 0.02,
28
+ "read_url_rendered": 0.10,
29
+ "wordpress-hub": 0.10,
30
+ "stripe-connector": 0.15,
31
+ "google-analytics-bluebee": 0.10,
32
+ "mail": 0.05,
33
+ "tasks": 0.02,
34
+ "docusign-connector": 0.20,
35
+ "plaid-connector": 0.25,
36
+ "proxmox-connector": 0.15,
37
+ "se-ranking-connector": 0.15,
38
+ "bing-webmaster-connector": 0.05,
39
+ "google-search-console-connector": 0.05,
40
+ "google-drive-connector": 0.05,
41
+ "telegram-publisher-extension": 0.05,
42
+ "x-connector": 0.10,
43
+ "thoughts": 0.01,
44
+ "memory-index": 0.01,
45
+ }
46
+
47
+ def estimate_action_cost(tool_name: str) -> float:
48
+ """Estimate credit cost for an action by tool name."""
49
+ if not tool_name:
50
+ return 0.0
51
+ t = tool_name.strip()
52
+ if t in DEFAULT_ACTION_PRICES:
53
+ return DEFAULT_ACTION_PRICES[t]
54
+ if "·" in t:
55
+ app, op = t.split("·", 1)
56
+ if app in DEFAULT_ACTION_PRICES:
57
+ return DEFAULT_ACTION_PRICES[app]
58
+ if op in DEFAULT_ACTION_PRICES:
59
+ return DEFAULT_ACTION_PRICES[op]
60
+ elif "__" in t:
61
+ app, op = t.split("__", 1)
62
+ if app in DEFAULT_ACTION_PRICES:
63
+ return DEFAULT_ACTION_PRICES[app]
64
+ if op in DEFAULT_ACTION_PRICES:
65
+ return DEFAULT_ACTION_PRICES[op]
66
+ if any(k in t for k in ("ssh", "remote", "http", "api", "conn")):
67
+ return 0.15
68
+ return 0.0
@@ -11,6 +11,7 @@ from rich.text import Text
11
11
 
12
12
  from webbee.banner_art import WEBBEE_CODE
13
13
  from webbee.sizing import trunc
14
+ from webbee.pricing_constants import estimate_action_cost
14
15
 
15
16
  _GUTTER = 2 # left margin (cols) — the single consistent transcript gutter.
16
17
  # Chrome lines prefix a 2-space string (" "); block renderables
@@ -75,6 +76,33 @@ def _clean(s) -> str:
75
76
  _ICON = {"read_file": "📖", "grep": "🔎", "glob": "🗂️ ", "write_file": "✏️",
76
77
  "edit_file": "🔧", "shell": "⚡", "bash": "⚡", "web_search": "🌐 ", "read_url": "📄",
77
78
  "view_image": "🖼️"}
79
+ def _tool_meta(tool: str) -> tuple[str, str]:
80
+ t = (tool or "").strip()
81
+ if t.startswith("conn-ssh"):
82
+ return "🌐", "SSH Exec"
83
+ if t in ("read_file", "read_document"):
84
+ return "📖", "Read File"
85
+ if t in ("edit_file", "multi_edit"):
86
+ return "🔧", "Edit File"
87
+ if t == "write_file":
88
+ return "✏️", "Write File"
89
+ if t in ("grep", "search_code"):
90
+ return "🔎", "Search Code"
91
+ if t in ("glob", "list_dir"):
92
+ return "🗂️", "List Files"
93
+ if t in ("bash", "shell"):
94
+ return "⚡", "Shell Exec"
95
+ if t in ("web_search", "read_url", "read_url_rendered"):
96
+ return "🌐", "Web Search"
97
+ if t.startswith("tasks"):
98
+ return "⚡", "Tasks"
99
+ if t.startswith("notes"):
100
+ return "📝", "Notes"
101
+ if t.startswith("mail"):
102
+ return "✉️", "Mail"
103
+ return "⚡", t
104
+
105
+
78
106
  _BEE = "yellow" # bee-yellow brand accent — logo / 🐝 / notes / busy dot ONLY
79
107
  _ACCENT = "cyan" # interactive chrome ONLY — live caret / mode / panel url
80
108
 
@@ -588,46 +616,44 @@ class RichSink:
588
616
  self._nudge() # the completed line is printed in tool_result
589
617
 
590
618
  def tool_result(self, tool: str, ok: bool, summary: str) -> None:
591
- # One calm dim line: icon + tool (+arg), then the ✓/✗ RIGHT NEXT TO the
592
- # action (not pinned to the far right), then a dim summary. Only the
593
- # ✓/✗ carries colour — the rest recedes (dim). Truncation lengths are
594
- # a PROPORTION of the live console width (W2 front-2), floored at the
595
- # old hardcoded counts so a narrow terminal never sees an empty line.
596
- # DEFERRED (W2 final-review): the clip is baked into the recorded
597
- # Text at PRINT time, not re-derived live — after a WIDEN, pre-resize
598
- # history keeps its OLD (narrower) clip; cosmetic only, and always
599
- # shrink-safe (a shrink still never overflows the new width).
600
- # A render-time clipping renderable (re-clips from the live width on
601
- # every redraw instead of baking it in) remains a future polish.
602
619
  _tool, arg = self._pending if self._pending[0] else (tool, "")
603
- icon = _ICON.get(_tool, "⚡")
620
+ icon, category = _tool_meta(_tool)
604
621
  mark = "✓" if ok else "✗"
622
+ mark_color = "bold green" if ok else "bold red"
605
623
  w = self.console.width
606
- # webbee-code-tool-result-gutter-v1: this is the MOST frequently
607
- # printed line in the whole transcript (one per action), and on a
608
- # narrow terminal (mobile SSH client, a slim tmux pane) the summary
609
- # regularly wraps to a second visual row -- without _pad() that
610
- # continuation line hugged column 0 with no left margin at all, an
611
- # ugly seam against every OTHER line in the feed, which all keep the
612
- # 2-col gutter (Valentin, live 2026-07-31: "клеится к самому левому
613
- # краю"). _pad() indents every wrapped row consistently, the same
614
- # fix note()/progress()/thinking() already got.
615
- t_now = time.strftime("%H:%M:%S")
616
- self.console.print(_pad(Text.assemble(
617
- (f"{t_now} ", "dim grey50"),
618
- (icon + " ", "dim"),
619
- (_clean(_tool), "dim"),
620
- ((" " + _clean(arg)[:trunc(w, 0.3, 40)]) if arg else "", "dim"),
621
- (" ", ""),
622
- (mark + " ", "green" if ok else "red"),
623
- (_clean(summary)[:trunc(w, 0.35, 50)], "dim"),
624
- )))
624
+ duration_ms = 0
625
+ if hasattr(self, "_pending_start") and self._pending_start > 0:
626
+ duration_ms = max(0, int((self._clock() - self._pending_start) * 1000))
625
627
  self._pending = ("", "")
626
- # webbee-code-click-to-expand-v1: tag the record JUST printed as
627
- # "step N" (1-based, matching /steps' own numbering) -- record_count()
628
- # already reflects this print (Rich prints synchronously), so the
629
- # index this line landed at is exactly len(pane_records) - 1.
628
+ self._pending_start = 0.0
629
+ cost_cr = estimate_action_cost(_tool)
630
+ dur_str = f"{duration_ms}ms" if duration_ms > 0 else "<1ms"
631
+ cost_str = f"{cost_cr:.2f} cr"
630
632
  self._step_index += 1
633
+ idx = self._step_index
634
+ line = Text()
635
+ line.append(f"{idx}. ", style="dim")
636
+ line.append(f"{icon} ")
637
+ line.append(f"{category:<12} ", style="bold white" if cost_cr > 0 else "none")
638
+ prefix_len = len(f"{idx}. ") + 4 + 13 + _GUTTER
639
+ right_len = 8 + 9 + 3 + len(_tool)
640
+ available_arg = max(16, w - prefix_len - right_len - 2)
641
+ arg_clean = _clean(arg).strip()
642
+ if len(arg_clean) > available_arg:
643
+ arg_disp = arg_clean[:available_arg - 1] + "…"
644
+ else:
645
+ arg_disp = arg_clean
646
+ line.append(f"{arg_disp:<{available_arg}} ", style="cyan" if cost_cr > 0 else "none")
647
+ line.append(f"{dur_str:>6} ", style="dim")
648
+ line.append(f"{cost_str:>7} ", style="cyan" if cost_cr > 0 else "dim")
649
+ line.append(f"{mark} ", style=mark_color)
650
+ line.append(f"{_tool}", style="dim grey50")
651
+ clean_summary = _clean(summary).strip()
652
+ if clean_summary:
653
+ line.append(chr(10))
654
+ line.append(" └─ ", style="dim grey50")
655
+ line.append(clean_summary, style="dim")
656
+ self.console.print(_pad(line))
631
657
  rc = getattr(self.console, "record_count", None)
632
658
  if callable(rc):
633
659
  self._step_records[rc() - 1] = self._step_index
@@ -838,7 +838,9 @@ async def _make_session_slot(cfg, token_provider, workspace, mode, *, resources:
838
838
 
839
839
  auto_slot_id = slot_id is None
840
840
  if auto_slot_id:
841
- slot_id = "" if first else uuid4().hex[:6]
841
+ # Every terminal session receives a unique slot_id (hex[:6]) so its
842
+ # session ID is isolated and never collides with or leaks into old tasks.
843
+ slot_id = uuid4().hex[:6]
842
844
 
843
845
  repo_key_task = None
844
846
  if first:
@@ -132,13 +132,41 @@ class AgentSession:
132
132
  its own event loop, so the client must be built there, not shared from
133
133
  here. Returns None when this session has no cloud config -- the read
134
134
  then degrades to one honest sentence instead of a crash.
135
+
136
+ Threadsafe Loop-Bridge (Python 3.6 - 3.14+):
137
+ token_provider is bound to the main event loop's lock. We bridge it across
138
+ thread boundaries via asyncio.run_coroutine_threadsafe so worker event loops
139
+ never raise RuntimeError: Lock is bound to a different event loop.
135
140
  """
136
141
  cfg, token_provider = self.cfg, self.token_provider
137
142
  if cfg is None or token_provider is None:
138
143
  return None
139
144
 
145
+ import asyncio
146
+
147
+ try:
148
+ main_loop = asyncio.get_running_loop()
149
+ except RuntimeError:
150
+ try:
151
+ main_loop = asyncio.get_event_loop()
152
+ except RuntimeError:
153
+ main_loop = None
154
+
140
155
  def _make():
141
156
  from imperal_mcp.client import ImperalClient
157
+
158
+ if main_loop is not None and main_loop.is_running():
159
+ async def _bridged_token_provider() -> str:
160
+ try:
161
+ current_loop = asyncio.get_running_loop()
162
+ except RuntimeError:
163
+ current_loop = None
164
+ if current_loop is main_loop:
165
+ return await token_provider()
166
+ fut = asyncio.run_coroutine_threadsafe(token_provider(), main_loop)
167
+ return await asyncio.wrap_future(fut)
168
+ return ImperalClient(cfg, _bridged_token_provider)
169
+
142
170
  return ImperalClient(cfg, token_provider)
143
171
 
144
172
  return _make