webbee 0.4.1__tar.gz → 0.4.2__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 (139) hide show
  1. {webbee-0.4.1 → webbee-0.4.2}/CHANGELOG.md +33 -0
  2. {webbee-0.4.1 → webbee-0.4.2}/PKG-INFO +6 -3
  3. {webbee-0.4.1 → webbee-0.4.2}/README.md +1 -1
  4. {webbee-0.4.1 → webbee-0.4.2}/pyproject.toml +5 -2
  5. webbee-0.4.2/src/webbee/__init__.py +1 -0
  6. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/clipboard.py +24 -4
  7. webbee-0.4.2/src/webbee/media.py +137 -0
  8. webbee-0.4.2/src/webbee/modals.py +504 -0
  9. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/render.py +18 -4
  10. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/repl.py +22 -2
  11. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/selection.py +3 -0
  12. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/session.py +12 -1
  13. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/tabs.py +64 -44
  14. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/tools.py +8 -1
  15. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/tui.py +472 -124
  16. webbee-0.4.2/tests/test_modals_e2e.py +188 -0
  17. {webbee-0.4.1 → webbee-0.4.2}/tests/test_render.py +9 -0
  18. {webbee-0.4.1 → webbee-0.4.2}/tests/test_repl.py +1 -1
  19. {webbee-0.4.1 → webbee-0.4.2}/tests/test_tui.py +30 -3
  20. {webbee-0.4.1 → webbee-0.4.2}/tests/test_version_badge_corner.py +21 -0
  21. webbee-0.4.1/src/webbee/__init__.py +0 -1
  22. {webbee-0.4.1 → webbee-0.4.2}/.github/workflows/publish.yml +0 -0
  23. {webbee-0.4.1 → webbee-0.4.2}/.gitignore +0 -0
  24. {webbee-0.4.1 → webbee-0.4.2}/LICENSE +0 -0
  25. {webbee-0.4.1 → webbee-0.4.2}/install.sh +0 -0
  26. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/account.py +0 -0
  27. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/active_sessions.py +0 -0
  28. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/banner_art.py +0 -0
  29. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/boot.py +0 -0
  30. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/checkpoints.py +0 -0
  31. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/cli.py +0 -0
  32. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/clipboard_read.py +0 -0
  33. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/clipboard_session.py +0 -0
  34. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/coding_context.py +0 -0
  35. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/commands.py +0 -0
  36. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/config.py +0 -0
  37. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/consent.py +0 -0
  38. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/details.py +0 -0
  39. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/events.py +0 -0
  40. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/frames.py +0 -0
  41. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/home.py +0 -0
  42. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/home_view.py +0 -0
  43. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/http.py +0 -0
  44. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/instance_lock.py +0 -0
  45. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/__init__.py +0 -0
  46. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/chunker.py +0 -0
  47. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/contracts.py +0 -0
  48. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/embed.py +0 -0
  49. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/graph.py +0 -0
  50. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/indexer.py +0 -0
  51. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/models.py +0 -0
  52. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/query.py +0 -0
  53. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/service.py +0 -0
  54. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/store.py +0 -0
  55. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/vectors.py +0 -0
  56. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/intel/watch.py +0 -0
  57. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/mode_store.py +0 -0
  58. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/native_files.py +0 -0
  59. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/native_read.py +0 -0
  60. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/newtab_mode.py +0 -0
  61. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/output_pane.py +0 -0
  62. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/queue_panel.py +0 -0
  63. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/reflow.py +0 -0
  64. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/remote.py +0 -0
  65. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/repo.py +0 -0
  66. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/sessions.py +0 -0
  67. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/sizing.py +0 -0
  68. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/slots.py +0 -0
  69. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/steer.py +0 -0
  70. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/stream.py +0 -0
  71. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/tab_store.py +0 -0
  72. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/thread.py +0 -0
  73. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/tier_store.py +0 -0
  74. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/todo_panel.py +0 -0
  75. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/tokens.py +0 -0
  76. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/update.py +0 -0
  77. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/urlopen.py +0 -0
  78. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/wallet.py +0 -0
  79. {webbee-0.4.1 → webbee-0.4.2}/src/webbee/worktrees.py +0 -0
  80. {webbee-0.4.1 → webbee-0.4.2}/tests/__init__.py +0 -0
  81. {webbee-0.4.1 → webbee-0.4.2}/tests/conftest.py +0 -0
  82. {webbee-0.4.1 → webbee-0.4.2}/tests/test_account.py +0 -0
  83. {webbee-0.4.1 → webbee-0.4.2}/tests/test_active_sessions.py +0 -0
  84. {webbee-0.4.1 → webbee-0.4.2}/tests/test_audit_2026_07_25_findings.py +0 -0
  85. {webbee-0.4.1 → webbee-0.4.2}/tests/test_checkpoints.py +0 -0
  86. {webbee-0.4.1 → webbee-0.4.2}/tests/test_cli.py +0 -0
  87. {webbee-0.4.1 → webbee-0.4.2}/tests/test_clipboard.py +0 -0
  88. {webbee-0.4.1 → webbee-0.4.2}/tests/test_clipboard_read.py +0 -0
  89. {webbee-0.4.1 → webbee-0.4.2}/tests/test_coding_context.py +0 -0
  90. {webbee-0.4.1 → webbee-0.4.2}/tests/test_commands.py +0 -0
  91. {webbee-0.4.1 → webbee-0.4.2}/tests/test_config.py +0 -0
  92. {webbee-0.4.1 → webbee-0.4.2}/tests/test_cpc_contract_stable.py +0 -0
  93. {webbee-0.4.1 → webbee-0.4.2}/tests/test_details.py +0 -0
  94. {webbee-0.4.1 → webbee-0.4.2}/tests/test_events.py +0 -0
  95. {webbee-0.4.1 → webbee-0.4.2}/tests/test_freeze_fix.py +0 -0
  96. {webbee-0.4.1 → webbee-0.4.2}/tests/test_home.py +0 -0
  97. {webbee-0.4.1 → webbee-0.4.2}/tests/test_home_view.py +0 -0
  98. {webbee-0.4.1 → webbee-0.4.2}/tests/test_instance_lock.py +0 -0
  99. {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_chunker.py +0 -0
  100. {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_contracts.py +0 -0
  101. {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_embed.py +0 -0
  102. {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_graph.py +0 -0
  103. {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_indexer.py +0 -0
  104. {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_no_numpy.py +0 -0
  105. {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_query.py +0 -0
  106. {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_service.py +0 -0
  107. {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_store.py +0 -0
  108. {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_vectors.py +0 -0
  109. {webbee-0.4.1 → webbee-0.4.2}/tests/test_intel_watch.py +0 -0
  110. {webbee-0.4.1 → webbee-0.4.2}/tests/test_marathon.py +0 -0
  111. {webbee-0.4.1 → webbee-0.4.2}/tests/test_mode_store.py +0 -0
  112. {webbee-0.4.1 → webbee-0.4.2}/tests/test_native_files.py +0 -0
  113. {webbee-0.4.1 → webbee-0.4.2}/tests/test_newtab_mode.py +0 -0
  114. {webbee-0.4.1 → webbee-0.4.2}/tests/test_packaging.py +0 -0
  115. {webbee-0.4.1 → webbee-0.4.2}/tests/test_queue_manage_0337.py +0 -0
  116. {webbee-0.4.1 → webbee-0.4.2}/tests/test_reflow.py +0 -0
  117. {webbee-0.4.1 → webbee-0.4.2}/tests/test_repo.py +0 -0
  118. {webbee-0.4.1 → webbee-0.4.2}/tests/test_session.py +0 -0
  119. {webbee-0.4.1 → webbee-0.4.2}/tests/test_sessions.py +0 -0
  120. {webbee-0.4.1 → webbee-0.4.2}/tests/test_sizing.py +0 -0
  121. {webbee-0.4.1 → webbee-0.4.2}/tests/test_slots.py +0 -0
  122. {webbee-0.4.1 → webbee-0.4.2}/tests/test_steer.py +0 -0
  123. {webbee-0.4.1 → webbee-0.4.2}/tests/test_stream.py +0 -0
  124. {webbee-0.4.1 → webbee-0.4.2}/tests/test_tab_restore_0337.py +0 -0
  125. {webbee-0.4.1 → webbee-0.4.2}/tests/test_tabs.py +0 -0
  126. {webbee-0.4.1 → webbee-0.4.2}/tests/test_terminal_ux_0336.py +0 -0
  127. {webbee-0.4.1 → webbee-0.4.2}/tests/test_thread.py +0 -0
  128. {webbee-0.4.1 → webbee-0.4.2}/tests/test_tier_store.py +0 -0
  129. {webbee-0.4.1 → webbee-0.4.2}/tests/test_todo_panel.py +0 -0
  130. {webbee-0.4.1 → webbee-0.4.2}/tests/test_tokens.py +0 -0
  131. {webbee-0.4.1 → webbee-0.4.2}/tests/test_tools.py +0 -0
  132. {webbee-0.4.1 → webbee-0.4.2}/tests/test_tools_vision_websearch.py +0 -0
  133. {webbee-0.4.1 → webbee-0.4.2}/tests/test_tui_hardening.py +0 -0
  134. {webbee-0.4.1 → webbee-0.4.2}/tests/test_update.py +0 -0
  135. {webbee-0.4.1 → webbee-0.4.2}/tests/test_update_badge_0340.py +0 -0
  136. {webbee-0.4.1 → webbee-0.4.2}/tests/test_urlopen.py +0 -0
  137. {webbee-0.4.1 → webbee-0.4.2}/tests/test_version.py +0 -0
  138. {webbee-0.4.1 → webbee-0.4.2}/tests/test_wallet.py +0 -0
  139. {webbee-0.4.1 → webbee-0.4.2}/tests/test_worktrees.py +0 -0
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2
4
+
5
+ **Webbee v0.4.2: Universal `shell` Action Across Windows/macOS/Linux, Interactive TUI Modals, Cloud Voice STT & Complete Hover Engine.**
6
+
7
+ This release introduces the cross-platform `shell` tool (replacing the platform-specific `bash` terminology for complete Windows, macOS, and Linux clarity), brings responsive modal dialogs (`Attach File`, `Voice Note`, `Session Menu`) with fluid mouse hover highlights and keyboard navigation, integrates zero-dependency cloud speech-to-text (STT) via `/v1/voice/stt`, and guarantees zero mouse escape garbage in the prompt.
8
+
9
+ ### Highlights & New Capabilities
10
+
11
+ * **Universal Cross-Platform `shell` Tool**:
12
+ - Renamed the core command execution tool from `bash` to `shell` to accurately reflect universal execution across Windows (`cmd`/`PowerShell`), macOS (`zsh`), and Linux (`bash`/`sh`).
13
+ - Retains transparent backward compatibility for existing scripts, tests, and model tool calls with aliased dispatch.
14
+ - Automatic UTF-8 environment forcing, path resolution for Homebrew (Apple Silicon + Intel) and Linuxbrew, and fail-soft non-interactive safeguards against password prompt deadlocks.
15
+
16
+ * **Interactive TUI Modals & Dialog System (`webbee.modals`)**:
17
+ - Responsive, centered modal dialogs for **File Attachment** (`📎`), **Voice Recording** (`🎤`), and **Session / Remote Routing** (`⋮`).
18
+ - Adaptive layout dynamically scaling to terminal dimensions (compact on 80-column terminals, comfortably expansive on ultra-wide screens).
19
+ - Native OS File Picker integration (macOS Finder, Linux Zenity/Kdialog, Windows PowerShell file dialog) with terminal list fallback.
20
+
21
+ * **Fluid Mouse Hover Engine & Keyboard Navigation**:
22
+ - Full support for `?1003h` any-event mouse tracking during active interactive sessions.
23
+ - Distinct 3-tier visual hierarchy: idle controls (`class:button`), crisp white hover highlight on pointer hover (`class:button.hover`), and signature Imperal yellow keyboard focus (`class:button.focused`).
24
+ - Interactive hover support enabled across tabs, tab close buttons (`✕`), new tab button (`+`), prompt action icons (`📎`, `🎤`, `⋮`), and footer mode/tier chips.
25
+
26
+ * **Cloud-Native Speech-To-Text (STT) Voice Notes**:
27
+ - In-terminal audio recording directly from default system microphones (Apple Silicon / Intel AVFoundation, Linux ALSA/PulseAudio, Windows DirectShow).
28
+ - Audio payloads sent to Imperal Core STT endpoint `/v1/voice/stt` and transcribed directly into clean conversational text in the input buffer before turn dispatch.
29
+
30
+ * **Zero-Garbage Prompt Protection (`scrub_mouse_residue`)**:
31
+ - Hardened input buffer against stray VT100 / DEC escape sequence leaks (`[<...` mouse coordinates or `` / `` focus reports).
32
+ - Reactive `on_text_changed` filter and modal-close buffer sanitizer instantly scrub residue before rendering.
33
+
34
+ ---
35
+
3
36
  ## 0.4.1
4
37
 
5
38
  **Webbee v0.4.1: Cross-Platform Clipboard Resilience, Icon Gutter Alignment & Refined Terminal UX.**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: webbee
3
- Version: 0.4.1
3
+ Version: 0.4.2
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
@@ -16,12 +16,15 @@ Classifier: Environment :: Console
16
16
  Classifier: Intended Audience :: Developers
17
17
  Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
19
21
  Classifier: Programming Language :: Python :: 3.11
20
22
  Classifier: Programming Language :: Python :: 3.12
21
23
  Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
22
25
  Classifier: Topic :: Software Development
23
26
  Classifier: Topic :: Utilities
24
- Requires-Python: >=3.11
27
+ Requires-Python: >=3.9
25
28
  Requires-Dist: httpx-sse>=0.4
26
29
  Requires-Dist: httpx>=0.27
27
30
  Requires-Dist: imperal-mcp>=0.5.0
@@ -120,7 +123,7 @@ Inside the interactive terminal:
120
123
  | 📖 | `read_file` | Read text and source files byte-exact, with offset/limit paging |
121
124
  | ✏️ | `write_file` | Create or overwrite files atomically with snapshot tracking |
122
125
  | 🔧 | `edit_file` / `multi_edit` | Precise find-and-replace edits across one or multiple files |
123
- | ⚡ | `bash` | Execute shell commands in the workspace root with real-time streaming |
126
+ | ⚡ | `shell` | Execute shell/terminal commands across Windows, macOS and Linux with streaming |
124
127
  | 🔎 | `grep` / `glob` | Fast regex code search and workspace file pattern matching |
125
128
  | 🌐 | `web_search` / `read_url` | Search the web and read web documentation in clean Markdown |
126
129
  | 🖼️ | `view_image` | Inspect images with native multimodal vision + OCR fallback |
@@ -73,7 +73,7 @@ Inside the interactive terminal:
73
73
  | 📖 | `read_file` | Read text and source files byte-exact, with offset/limit paging |
74
74
  | ✏️ | `write_file` | Create or overwrite files atomically with snapshot tracking |
75
75
  | 🔧 | `edit_file` / `multi_edit` | Precise find-and-replace edits across one or multiple files |
76
- | ⚡ | `bash` | Execute shell commands in the workspace root with real-time streaming |
76
+ | ⚡ | `shell` | Execute shell/terminal commands across Windows, macOS and Linux with streaming |
77
77
  | 🔎 | `grep` / `glob` | Fast regex code search and workspace file pattern matching |
78
78
  | 🌐 | `web_search` / `read_url` | Search the web and read web documentation in clean Markdown |
79
79
  | 🖼️ | `view_image` | Inspect images with native multimodal vision + OCR fallback |
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "webbee"
3
- version = "0.4.1"
3
+ version = "0.4.2"
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"
7
7
  license-files = ["LICENSE"]
8
- requires-python = ">=3.11"
8
+ requires-python = ">=3.9"
9
9
  authors = [{ name = "Imperal, Inc.", email = "hello@imperal.io" }]
10
10
  dependencies = [
11
11
  "imperal-mcp>=0.5.0",
@@ -41,9 +41,12 @@ classifiers = [
41
41
  "Environment :: Console",
42
42
  "Intended Audience :: Developers",
43
43
  "Programming Language :: Python :: 3",
44
+ "Programming Language :: Python :: 3.9",
45
+ "Programming Language :: Python :: 3.10",
44
46
  "Programming Language :: Python :: 3.11",
45
47
  "Programming Language :: Python :: 3.12",
46
48
  "Programming Language :: Python :: 3.13",
49
+ "Programming Language :: Python :: 3.14",
47
50
  "Operating System :: OS Independent",
48
51
  "Topic :: Software Development",
49
52
  "Topic :: Utilities",
@@ -0,0 +1 @@
1
+ __version__ = "0.4.2"
@@ -60,15 +60,35 @@ def _local_copy_cmd() -> list[str] | None:
60
60
 
61
61
  def _try_local_copy(text: str) -> bool:
62
62
  """Feed `text` to the local clipboard tool via stdin. True only on a
63
- clean (returncode 0) run — never raises."""
63
+ clean (returncode 0) run — never raises.
64
+ On Linux desktop (Pop!_OS / GNOME / Cosmic), also mirrors to the primary
65
+ selection buffer so middle-click paste and Shift+Insert work seamlessly
66
+ alongside Ctrl+Shift+V."""
64
67
  cmd = _local_copy_cmd()
65
68
  if cmd is None:
66
69
  return False
70
+ payload = text.encode("utf-8", "replace")
71
+ success = False
67
72
  try:
68
- proc = subprocess.run(cmd, input=text.encode("utf-8", "replace"), timeout=2)
69
- return proc.returncode == 0
73
+ proc = subprocess.run(cmd, input=payload, timeout=2)
74
+ success = (proc.returncode == 0)
70
75
  except Exception:
71
- return False
76
+ success = False
77
+
78
+ if success and sys.platform not in ("darwin", "win32"):
79
+ # Linux primary selection dual-write
80
+ try:
81
+ from webbee.clipboard_session import is_wayland_session
82
+ if is_wayland_session() and shutil.which("wl-copy"):
83
+ subprocess.run(["wl-copy", "--primary"], input=payload, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=1)
84
+ elif shutil.which("xclip"):
85
+ subprocess.run(["xclip", "-selection", "primary"], input=payload, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=1)
86
+ elif shutil.which("xsel"):
87
+ subprocess.run(["xsel", "--primary", "--input"], input=payload, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=1)
88
+ except Exception:
89
+ pass
90
+
91
+ return success
72
92
 
73
93
 
74
94
  def _osc52_emit(text: str) -> bool:
@@ -0,0 +1,137 @@
1
+ """Enterprise Cross-Platform Audio Playback & Terminal Image Viewer for Webbee Code.
2
+
3
+ Provides robust, zero-crash audio playback and in-terminal image viewing across:
4
+ - macOS (afplay, AVFoundation, iTerm2/Kitty image protocol)
5
+ - Linux (paplay, aplay, pw-play, ffplay, Sixel, Kitty, ANSI half-blocks)
6
+ - Windows (PowerShell System.Media.SoundPlayer, Windows Media Player, WT inline)
7
+ - Universal ANSI/Half-block fallback for any terminal on any OS.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ import threading
17
+ from typing import Optional, Tuple
18
+
19
+
20
+ def play_audio_file(file_path: str, *, block: bool = False) -> Optional[subprocess.Popen]:
21
+ """Play an audio file (.wav, .mp3, .m4a, .ogg) across macOS, Linux, and Windows.
22
+
23
+ Non-blocking by default (runs in background process).
24
+ """
25
+ if not os.path.exists(file_path):
26
+ return None
27
+
28
+ cmd = []
29
+ if sys.platform == "darwin":
30
+ if shutil.which("afplay"):
31
+ cmd = ["afplay", file_path]
32
+ elif shutil.which("ffplay"):
33
+ cmd = ["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", file_path]
34
+ elif sys.platform.startswith("linux"):
35
+ if shutil.which("paplay") and file_path.endswith(".wav"):
36
+ cmd = ["paplay", file_path]
37
+ elif shutil.which("pw-play"):
38
+ cmd = ["pw-play", file_path]
39
+ elif shutil.which("aplay") and file_path.endswith(".wav"):
40
+ cmd = ["aplay", "-q", file_path]
41
+ elif shutil.which("ffplay"):
42
+ cmd = ["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", file_path]
43
+ elif shutil.which("mpv"):
44
+ cmd = ["mpv", "--no-video", "--really-quiet", file_path]
45
+ elif sys.platform in ("win32", "cygwin"):
46
+ ps = shutil.which("powershell") or shutil.which("pwsh")
47
+ if ps:
48
+ abs_p = os.path.abspath(file_path).replace("'", "''")
49
+ cmd = [ps, "-c", f"(New-Object System.Media.SoundPlayer '{abs_p}').PlaySync()"]
50
+ elif shutil.which("ffplay"):
51
+ cmd = ["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", file_path]
52
+
53
+ if not cmd:
54
+ return None
55
+
56
+ try:
57
+ if block:
58
+ subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
59
+ return None
60
+ proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
61
+ return proc
62
+ except Exception:
63
+ return None
64
+
65
+
66
+ def render_image_in_terminal(image_path: str, max_cols: int = 60, max_rows: int = 30) -> str:
67
+ """Render an image directly inside the terminal.
68
+
69
+ Supported rendering protocols:
70
+ 1. iTerm2 / WezTerm / Ghostty inline image protocol (OSC 1337)
71
+ 2. Kitty terminal graphics protocol (APC G ...)
72
+ 3. High-quality ANSI half-block (▀ / ▄) TrueColor fallback using PIL if available
73
+ 4. Text description fallback if binary / non-renderable
74
+ """
75
+ if not os.path.exists(image_path):
76
+ return f"[Image not found: {image_path}]"
77
+
78
+ # Check terminal protocol support
79
+ term = os.environ.get("TERM", "").lower()
80
+ term_prog = os.environ.get("TERM_PROGRAM", "").lower()
81
+ lc_term = os.environ.get("LC_TERMINAL", "").lower()
82
+
83
+ # 1. iTerm2 / WezTerm / Ghostty OSC 1337 Inline Image Protocol
84
+ if "iterm" in term_prog or "wezterm" in term_prog or "ghostty" in term_prog or "iterm" in lc_term:
85
+ try:
86
+ with open(image_path, "rb") as f:
87
+ b64_data = base64.b64encode(f.read()).decode("ascii")
88
+ filename_b64 = base64.b64encode(os.path.basename(image_path).encode("utf-8")).decode("ascii")
89
+ # OSC 1337 ; File=name=...;inline=1;width=auto;height=auto : <base64> ^G
90
+ return f"\033]1337;File=name={filename_b64};inline=1;width=auto;height={max_rows}:{b64_data}\007"
91
+ except Exception:
92
+ pass
93
+
94
+ # 2. Kitty Graphics Protocol
95
+ if "kitty" in term or "kitty" in term_prog:
96
+ try:
97
+ with open(image_path, "rb") as f:
98
+ b64_data = base64.b64encode(f.read()).decode("ascii")
99
+ # Kitty chunked graphics transmission
100
+ return f"\033_Ga=T,f=100,t=d;{b64_data}\033\\"
101
+ except Exception:
102
+ pass
103
+
104
+ # 3. ANSI TrueColor Half-Block Fallback (Works in ANY 24-bit terminal on macOS, Linux, Windows)
105
+ try:
106
+ from PIL import Image
107
+ img = Image.open(image_path).convert("RGB")
108
+ # Scale to max_cols x (max_rows * 2) because each character cell has 2 vertical subpixels (▀)
109
+ w, h = img.size
110
+ aspect = w / max(1, h)
111
+ target_w = min(max_cols, w)
112
+ target_h = int(target_w / max(0.1, aspect * 2))
113
+ target_h = min(max_rows, max(1, target_h)) * 2
114
+
115
+ img = img.resize((target_w, target_h), Image.Resampling.BILINEAR)
116
+ pix = img.load()
117
+
118
+ lines = []
119
+ for y in range(0, target_h, 2):
120
+ line_parts = []
121
+ for x in range(target_w):
122
+ r_top, g_top, b_top = pix[x, y]
123
+ if y + 1 < target_h:
124
+ r_bot, g_bot, b_bot = pix[x, y + 1]
125
+ # ▀ with fg=top, bg=bot
126
+ line_parts.append(f"\033[38;2;{r_top};{g_top};{b_top}m\033[48;2;{r_bot};{g_bot};{b_bot}m▀")
127
+ else:
128
+ line_parts.append(f"\033[38;2;{r_top};{g_top};{b_top}m▀")
129
+ line_parts.append("\033[0m")
130
+ lines.append("".join(line_parts))
131
+ return "\n".join(lines)
132
+ except Exception:
133
+ pass
134
+
135
+ # 4. Graceful metadata banner
136
+ size_kb = os.path.getsize(image_path) / 1024.0
137
+ return f"🖼️ [Image: {os.path.basename(image_path)} ({size_kb:.1f} KB)]"