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.
- {webbee-0.4.2 → webbee-0.4.4}/CHANGELOG.md +50 -0
- {webbee-0.4.2 → webbee-0.4.4}/PKG-INFO +2 -4
- {webbee-0.4.2 → webbee-0.4.4}/README.md +1 -3
- {webbee-0.4.2 → webbee-0.4.4}/pyproject.toml +1 -1
- webbee-0.4.4/src/webbee/__init__.py +1 -0
- webbee-0.4.4/src/webbee/__main__.py +5 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/cli.py +4 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/config.py +4 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/frames.py +1 -1
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/modals.py +78 -15
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/native_read.py +26 -6
- webbee-0.4.4/src/webbee/pricing_constants.py +68 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/render.py +61 -35
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/repl.py +3 -1
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/session.py +28 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/tui.py +42 -70
- webbee-0.4.4/tests/test_cross_loop_token.py +54 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_repl.py +7 -5
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_tui.py +23 -11
- webbee-0.4.2/src/webbee/__init__.py +0 -1
- {webbee-0.4.2 → webbee-0.4.4}/.github/workflows/publish.yml +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/.gitignore +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/LICENSE +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/install.sh +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/account.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/active_sessions.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/banner_art.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/boot.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/checkpoints.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/clipboard.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/clipboard_read.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/clipboard_session.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/coding_context.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/commands.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/consent.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/details.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/events.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/home.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/home_view.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/http.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/instance_lock.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/chunker.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/contracts.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/embed.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/graph.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/indexer.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/models.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/query.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/service.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/store.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/intel/watch.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/media.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/mode_store.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/native_files.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/newtab_mode.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/output_pane.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/queue_panel.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/reflow.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/remote.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/repo.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/selection.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/sessions.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/sizing.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/slots.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/steer.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/stream.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/tab_store.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/tabs.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/thread.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/tier_store.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/todo_panel.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/tokens.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/tools.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/update.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/urlopen.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/wallet.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/src/webbee/worktrees.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/__init__.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/conftest.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_account.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_active_sessions.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_audit_2026_07_25_findings.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_checkpoints.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_cli.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_clipboard.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_clipboard_read.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_coding_context.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_commands.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_config.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_details.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_events.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_freeze_fix.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_home.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_home_view.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_instance_lock.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_chunker.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_contracts.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_embed.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_graph.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_indexer.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_query.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_service.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_store.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_vectors.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_intel_watch.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_marathon.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_modals_e2e.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_mode_store.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_native_files.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_newtab_mode.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_packaging.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_queue_manage_0337.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_reflow.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_render.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_repo.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_session.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_sessions.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_sizing.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_slots.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_steer.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_stream.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_tab_restore_0337.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_tabs.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_terminal_ux_0336.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_thread.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_tier_store.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_todo_panel.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_tokens.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_tools.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_tools_vision_websearch.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_tui_hardening.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_update.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_update_badge_0340.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_urlopen.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_version.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_version_badge_corner.py +0 -0
- {webbee-0.4.2 → webbee-0.4.4}/tests/test_wallet.py +0 -0
- {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.
|
|
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
|
[](https://pypi.org/project/webbee/)
|
|
54
|
-
[](https://pypi.org/project/webbee/)
|
|
55
55
|
[](LICENSE)
|
|
56
56
|
[](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
|
[](https://pypi.org/project/webbee/)
|
|
4
|
-
[](https://pypi.org/project/webbee/)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.4"
|
|
@@ -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
|
-
|
|
253
|
-
token = ""
|
|
254
|
-
if os.path.exists(cred_path):
|
|
252
|
+
if not token:
|
|
255
253
|
try:
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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"
|
|
188
|
-
f"
|
|
189
|
-
f"or
|
|
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
|
-
|
|
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
|
|
279
|
-
|
|
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 =
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
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
|