webbee 0.4.3__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.3 → webbee-0.4.4}/CHANGELOG.md +24 -0
  2. {webbee-0.4.3 → webbee-0.4.4}/PKG-INFO +1 -1
  3. {webbee-0.4.3 → webbee-0.4.4}/pyproject.toml +1 -1
  4. webbee-0.4.4/src/webbee/__init__.py +1 -0
  5. webbee-0.4.4/src/webbee/__main__.py +5 -0
  6. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/cli.py +4 -0
  7. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/frames.py +1 -1
  8. webbee-0.4.4/src/webbee/pricing_constants.py +68 -0
  9. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/render.py +61 -35
  10. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/repl.py +3 -1
  11. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/tui.py +9 -59
  12. {webbee-0.4.3 → webbee-0.4.4}/tests/test_repl.py +7 -5
  13. {webbee-0.4.3 → webbee-0.4.4}/tests/test_tui.py +8 -11
  14. webbee-0.4.3/src/webbee/__init__.py +0 -1
  15. {webbee-0.4.3 → webbee-0.4.4}/.github/workflows/publish.yml +0 -0
  16. {webbee-0.4.3 → webbee-0.4.4}/.gitignore +0 -0
  17. {webbee-0.4.3 → webbee-0.4.4}/LICENSE +0 -0
  18. {webbee-0.4.3 → webbee-0.4.4}/README.md +0 -0
  19. {webbee-0.4.3 → webbee-0.4.4}/install.sh +0 -0
  20. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/account.py +0 -0
  21. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/active_sessions.py +0 -0
  22. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/banner_art.py +0 -0
  23. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/boot.py +0 -0
  24. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/checkpoints.py +0 -0
  25. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/clipboard.py +0 -0
  26. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/clipboard_read.py +0 -0
  27. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/clipboard_session.py +0 -0
  28. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/coding_context.py +0 -0
  29. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/commands.py +0 -0
  30. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/config.py +0 -0
  31. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/consent.py +0 -0
  32. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/details.py +0 -0
  33. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/events.py +0 -0
  34. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/home.py +0 -0
  35. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/home_view.py +0 -0
  36. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/http.py +0 -0
  37. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/instance_lock.py +0 -0
  38. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/intel/__init__.py +0 -0
  39. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/intel/chunker.py +0 -0
  40. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/intel/contracts.py +0 -0
  41. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/intel/embed.py +0 -0
  42. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/intel/graph.py +0 -0
  43. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/intel/indexer.py +0 -0
  44. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/intel/models.py +0 -0
  45. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/intel/query.py +0 -0
  46. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/intel/service.py +0 -0
  47. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/intel/store.py +0 -0
  48. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/intel/vectors.py +0 -0
  49. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/intel/watch.py +0 -0
  50. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/media.py +0 -0
  51. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/modals.py +0 -0
  52. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/mode_store.py +0 -0
  53. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/native_files.py +0 -0
  54. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/native_read.py +0 -0
  55. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/newtab_mode.py +0 -0
  56. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/output_pane.py +0 -0
  57. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/queue_panel.py +0 -0
  58. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/reflow.py +0 -0
  59. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/remote.py +0 -0
  60. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/repo.py +0 -0
  61. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/selection.py +0 -0
  62. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/session.py +0 -0
  63. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/sessions.py +0 -0
  64. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/sizing.py +0 -0
  65. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/slots.py +0 -0
  66. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/steer.py +0 -0
  67. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/stream.py +0 -0
  68. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/tab_store.py +0 -0
  69. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/tabs.py +0 -0
  70. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/thread.py +0 -0
  71. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/tier_store.py +0 -0
  72. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/todo_panel.py +0 -0
  73. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/tokens.py +0 -0
  74. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/tools.py +0 -0
  75. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/update.py +0 -0
  76. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/urlopen.py +0 -0
  77. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/wallet.py +0 -0
  78. {webbee-0.4.3 → webbee-0.4.4}/src/webbee/worktrees.py +0 -0
  79. {webbee-0.4.3 → webbee-0.4.4}/tests/__init__.py +0 -0
  80. {webbee-0.4.3 → webbee-0.4.4}/tests/conftest.py +0 -0
  81. {webbee-0.4.3 → webbee-0.4.4}/tests/test_account.py +0 -0
  82. {webbee-0.4.3 → webbee-0.4.4}/tests/test_active_sessions.py +0 -0
  83. {webbee-0.4.3 → webbee-0.4.4}/tests/test_audit_2026_07_25_findings.py +0 -0
  84. {webbee-0.4.3 → webbee-0.4.4}/tests/test_checkpoints.py +0 -0
  85. {webbee-0.4.3 → webbee-0.4.4}/tests/test_cli.py +0 -0
  86. {webbee-0.4.3 → webbee-0.4.4}/tests/test_clipboard.py +0 -0
  87. {webbee-0.4.3 → webbee-0.4.4}/tests/test_clipboard_read.py +0 -0
  88. {webbee-0.4.3 → webbee-0.4.4}/tests/test_coding_context.py +0 -0
  89. {webbee-0.4.3 → webbee-0.4.4}/tests/test_commands.py +0 -0
  90. {webbee-0.4.3 → webbee-0.4.4}/tests/test_config.py +0 -0
  91. {webbee-0.4.3 → webbee-0.4.4}/tests/test_cpc_contract_stable.py +0 -0
  92. {webbee-0.4.3 → webbee-0.4.4}/tests/test_cross_loop_token.py +0 -0
  93. {webbee-0.4.3 → webbee-0.4.4}/tests/test_details.py +0 -0
  94. {webbee-0.4.3 → webbee-0.4.4}/tests/test_events.py +0 -0
  95. {webbee-0.4.3 → webbee-0.4.4}/tests/test_freeze_fix.py +0 -0
  96. {webbee-0.4.3 → webbee-0.4.4}/tests/test_home.py +0 -0
  97. {webbee-0.4.3 → webbee-0.4.4}/tests/test_home_view.py +0 -0
  98. {webbee-0.4.3 → webbee-0.4.4}/tests/test_instance_lock.py +0 -0
  99. {webbee-0.4.3 → webbee-0.4.4}/tests/test_intel_chunker.py +0 -0
  100. {webbee-0.4.3 → webbee-0.4.4}/tests/test_intel_contracts.py +0 -0
  101. {webbee-0.4.3 → webbee-0.4.4}/tests/test_intel_embed.py +0 -0
  102. {webbee-0.4.3 → webbee-0.4.4}/tests/test_intel_graph.py +0 -0
  103. {webbee-0.4.3 → webbee-0.4.4}/tests/test_intel_indexer.py +0 -0
  104. {webbee-0.4.3 → webbee-0.4.4}/tests/test_intel_no_numpy.py +0 -0
  105. {webbee-0.4.3 → webbee-0.4.4}/tests/test_intel_query.py +0 -0
  106. {webbee-0.4.3 → webbee-0.4.4}/tests/test_intel_service.py +0 -0
  107. {webbee-0.4.3 → webbee-0.4.4}/tests/test_intel_store.py +0 -0
  108. {webbee-0.4.3 → webbee-0.4.4}/tests/test_intel_vectors.py +0 -0
  109. {webbee-0.4.3 → webbee-0.4.4}/tests/test_intel_watch.py +0 -0
  110. {webbee-0.4.3 → webbee-0.4.4}/tests/test_marathon.py +0 -0
  111. {webbee-0.4.3 → webbee-0.4.4}/tests/test_modals_e2e.py +0 -0
  112. {webbee-0.4.3 → webbee-0.4.4}/tests/test_mode_store.py +0 -0
  113. {webbee-0.4.3 → webbee-0.4.4}/tests/test_native_files.py +0 -0
  114. {webbee-0.4.3 → webbee-0.4.4}/tests/test_newtab_mode.py +0 -0
  115. {webbee-0.4.3 → webbee-0.4.4}/tests/test_packaging.py +0 -0
  116. {webbee-0.4.3 → webbee-0.4.4}/tests/test_queue_manage_0337.py +0 -0
  117. {webbee-0.4.3 → webbee-0.4.4}/tests/test_reflow.py +0 -0
  118. {webbee-0.4.3 → webbee-0.4.4}/tests/test_render.py +0 -0
  119. {webbee-0.4.3 → webbee-0.4.4}/tests/test_repo.py +0 -0
  120. {webbee-0.4.3 → webbee-0.4.4}/tests/test_session.py +0 -0
  121. {webbee-0.4.3 → webbee-0.4.4}/tests/test_sessions.py +0 -0
  122. {webbee-0.4.3 → webbee-0.4.4}/tests/test_sizing.py +0 -0
  123. {webbee-0.4.3 → webbee-0.4.4}/tests/test_slots.py +0 -0
  124. {webbee-0.4.3 → webbee-0.4.4}/tests/test_steer.py +0 -0
  125. {webbee-0.4.3 → webbee-0.4.4}/tests/test_stream.py +0 -0
  126. {webbee-0.4.3 → webbee-0.4.4}/tests/test_tab_restore_0337.py +0 -0
  127. {webbee-0.4.3 → webbee-0.4.4}/tests/test_tabs.py +0 -0
  128. {webbee-0.4.3 → webbee-0.4.4}/tests/test_terminal_ux_0336.py +0 -0
  129. {webbee-0.4.3 → webbee-0.4.4}/tests/test_thread.py +0 -0
  130. {webbee-0.4.3 → webbee-0.4.4}/tests/test_tier_store.py +0 -0
  131. {webbee-0.4.3 → webbee-0.4.4}/tests/test_todo_panel.py +0 -0
  132. {webbee-0.4.3 → webbee-0.4.4}/tests/test_tokens.py +0 -0
  133. {webbee-0.4.3 → webbee-0.4.4}/tests/test_tools.py +0 -0
  134. {webbee-0.4.3 → webbee-0.4.4}/tests/test_tools_vision_websearch.py +0 -0
  135. {webbee-0.4.3 → webbee-0.4.4}/tests/test_tui_hardening.py +0 -0
  136. {webbee-0.4.3 → webbee-0.4.4}/tests/test_update.py +0 -0
  137. {webbee-0.4.3 → webbee-0.4.4}/tests/test_update_badge_0340.py +0 -0
  138. {webbee-0.4.3 → webbee-0.4.4}/tests/test_urlopen.py +0 -0
  139. {webbee-0.4.3 → webbee-0.4.4}/tests/test_version.py +0 -0
  140. {webbee-0.4.3 → webbee-0.4.4}/tests/test_version_badge_corner.py +0 -0
  141. {webbee-0.4.3 → webbee-0.4.4}/tests/test_wallet.py +0 -0
  142. {webbee-0.4.3 → webbee-0.4.4}/tests/test_worktrees.py +0 -0
@@ -1,5 +1,29 @@
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
+
3
27
  ## 0.4.3
4
28
 
5
29
  **Webbee v0.4.3: Non-Blocking Voice STT, Cross-Event-Loop Token Bridge, Dynamic Composer Geometry & Config Hardening.**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: webbee
3
- Version: 0.4.3
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webbee"
3
- version = "0.4.3"
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())
@@ -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,
@@ -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:
@@ -895,10 +895,8 @@ def _arrow_up_action(event, buf, sel: dict, n: int, busy: bool, pending=None,
895
895
  pulled["text"], pulled["iid"] = str(item), getattr(item, "iid", "")
896
896
  event.app.invalidate()
897
897
  return
898
- if n and not buf.text and not busy:
899
- sel["i"] = (n - 1) if sel["i"] is None else max(0, sel["i"] - 1)
900
- event.app.invalidate()
901
- return
898
+ # Legacy step-navigation on Up arrow completely removed:
899
+ # Up always recalls command history readline-style (or pulls from queue).
902
900
  buf.history_backward()
903
901
  event.app.invalidate()
904
902
 
@@ -906,10 +904,8 @@ def _arrow_up_action(event, buf, sel: dict, n: int, busy: bool, pending=None,
906
904
  def _arrow_down_action(event, buf, sel: dict, n: int, busy: bool) -> None:
907
905
  """Down key — mirror of _arrow_up_action: step-nav on the same exact
908
906
  gate, otherwise cycle history forward."""
909
- if n and not buf.text and not busy:
910
- sel["i"] = 0 if sel["i"] is None else min(n - 1, sel["i"] + 1)
911
- event.app.invalidate()
912
- return
907
+ # Legacy step-navigation on Down arrow completely removed:
908
+ # Down always cycles command history forward.
913
909
  buf.history_forward()
914
910
  event.app.invalidate()
915
911
 
@@ -1243,10 +1239,7 @@ async def run_session(*, slots, on_line, on_cycle, on_tier_cycle=None,
1243
1239
  if rc:
1244
1240
  rc(text) # ICNLI: relay the raw reply verbatim
1245
1241
  return
1246
- if not text.strip() and sel["i"] is not None and steps_nav and not _busy_live():
1247
- idx, sel["i"] = sel["i"], None
1248
- event.app.create_background_task(steps_nav["expand"](idx, slot))
1249
- return
1242
+
1250
1243
  if not text.strip():
1251
1244
  return
1252
1245
  if _is_queue_command(text):
@@ -1431,51 +1424,9 @@ async def run_session(*, slots, on_line, on_cycle, on_tier_cycle=None,
1431
1424
  get_app().create_background_task(_do_right_paste())
1432
1425
 
1433
1426
  def _line_click(abs_line: int) -> None:
1434
- """webbee-code-click-to-expand-v1: a PLAIN click on a transcript line
1435
- (see selection.py's `on_line_click` hook) asks \"what happened here?\"
1436
- without touching the keyboard at all -- the exact same reveal /steps
1437
- N (and Up/Down + Enter) already show, just one gesture shorter. A
1438
- click resolves to a RECORD via the pane's own reflow-anchoring math
1439
- (`_record_at_line`, already used for reflow + scroll-anchoring), then
1440
- to a step number via the sink's `step_for_record` (set right after
1441
- each tool_result print, see render.py) -- so a click on a banner,
1442
- a progress line, or blank space (no step there) is a harmless no-op,
1443
- never an error. Busy mid-turn is ALSO a no-op (steps_nav reflects the
1444
- LAST finished turn only -- clicking during a live one would expand
1445
- stale data), same gate Enter's own steps_nav path already uses.
1446
- Compact by construction: dispatches through steps_nav[\"expand\"],
1447
- the SAME `/steps N` -> step_detail bordered panel Enter already
1448
- shows -- never a new UI surface, never full-screen, nothing to
1449
- keep in sync twice."""
1450
- if not steps_nav or _busy_live():
1451
- return
1452
- slot = _a()
1453
- sink = slot.sink
1454
- step_fn = getattr(sink, "step_for_record", None) if sink is not None else None
1455
- if step_fn is None:
1456
- return
1457
- pane = _pane()
1458
- record_idx = pane._record_at_line(abs_line)
1459
- step_no = step_fn(record_idx)
1460
- if not step_no:
1461
- return
1462
- # webbee-code-step-toggle-v1: a SECOND click on the SAME already-open
1463
- # step line folds the detail panel back away instead of re-printing
1464
- # it -- a plain flash_note toast (same mechanism as copy-confirm)
1465
- # says so out loud, tool-agnostic (works identically whether the
1466
- # step was bash/read_file/write_file/anything else): raising and
1467
- # lowering the detail is now a real, visible round-trip, not a
1468
- # one-way reveal.
1469
- if slot.expanded_steps.get(step_no):
1470
- slot.expanded_steps.pop(step_no, None)
1471
- pane.flash_note(f"▸ step {step_no} collapsed")
1472
- get_app_or_none() and get_app_or_none().invalidate()
1473
- return
1474
- slot.expanded_steps[step_no] = True
1475
- event = get_app_or_none()
1476
- if event is None:
1477
- return
1478
- event.create_background_task(steps_nav["expand"](step_no - 1, slot))
1427
+ # Clicking lines is a no-op: legacy step expansion removed.
1428
+ # Kept as a stub because _pane() still wires pane.on_line_click.
1429
+ pass
1479
1430
 
1480
1431
  @kb.add("c-v")
1481
1432
  def _paste_key(event):
@@ -1791,8 +1742,7 @@ async def run_session(*, slots, on_line, on_cycle, on_tier_cycle=None,
1791
1742
  f = pane.flash()
1792
1743
  if f:
1793
1744
  frags = [("class:tb.working", " " + f)] # transient copy confirmation
1794
- elif sel["i"] is not None and steps_nav:
1795
- frags = [("class:tb.dim", f" step {sel['i'] + 1}/{_nav_count()} · Enter to expand · Esc to cancel")]
1745
+
1796
1746
  else:
1797
1747
  st_fn = _sink_attr("status")
1798
1748
  if callable(st_fn):
@@ -1592,7 +1592,7 @@ def test_make_session_slot_shares_resources_bundle_on_same_workspace(monkeypatch
1592
1592
 
1593
1593
  # ── W4b T5 item 1: slot_id minting -- tab-1 legacy id vs later short hex ────
1594
1594
 
1595
- def test_make_session_slot_first_true_keeps_slot_id_empty():
1595
+ def test_make_session_slot_first_true_mints_unique_slot_id():
1596
1596
  async def _drive():
1597
1597
  return await _make_session_slot(
1598
1598
  _mk_cfg(), _noop_token_provider, os.getcwd(), "default",
@@ -1602,8 +1602,9 @@ def test_make_session_slot_first_true_keeps_slot_id_empty():
1602
1602
  shadow_factory=lambda cfg, ws: None, first=True)
1603
1603
 
1604
1604
  slot = asyncio.run(_drive())
1605
- assert slot.slot_id == ""
1606
- assert slot.agent.slot_id == ""
1605
+ assert slot.slot_id and len(slot.slot_id) == 6
1606
+ assert all(c in "0123456789abcdef" for c in slot.slot_id)
1607
+ assert slot.agent.slot_id == slot.slot_id
1607
1608
 
1608
1609
 
1609
1610
  # ── 0.3.25 Part C: per-repo instance lock wiring in _make_session_slot ─────
@@ -1617,7 +1618,7 @@ class _FakeLock:
1617
1618
  self.closed = True
1618
1619
 
1619
1620
 
1620
- def test_first_true_primary_lock_keeps_legacy_slot_id_and_rides_on_the_slot(monkeypatch):
1621
+ def test_first_true_primary_lock_mints_slot_id_and_rides_on_the_slot(monkeypatch):
1621
1622
  import webbee.instance_lock as IL
1622
1623
  lock = _FakeLock(held=False)
1623
1624
  monkeypatch.setattr(IL, "acquire", lambda repo_key: lock)
@@ -1631,7 +1632,8 @@ def test_first_true_primary_lock_keeps_legacy_slot_id_and_rides_on_the_slot(monk
1631
1632
  shadow_factory=lambda cfg, ws: None, first=True)
1632
1633
 
1633
1634
  slot = asyncio.run(_drive())
1634
- assert slot.slot_id == "" # primary keeps the legacy id
1635
+ assert slot.slot_id and len(slot.slot_id) == 6
1636
+ assert all(c in "0123456789abcdef" for c in slot.slot_id)
1635
1637
  assert slot.instance_lock is lock # rides on the slot for later teardown
1636
1638
  assert "already running" not in slot.pane.dump()
1637
1639
 
@@ -2240,20 +2240,18 @@ def test_up_arrow_with_text_present_pulls_history_not_steps():
2240
2240
 
2241
2241
 
2242
2242
  def test_up_arrow_during_step_nav_still_navigates_steps():
2243
- # Idle + empty input + steps present = EXACTLY today's step-navigation.
2243
+ # Legacy step-navigation on arrow keys is removed: sel['i'] stays None, history handles it.
2244
2244
  from prompt_toolkit.buffer import Buffer
2245
2245
 
2246
2246
  from webbee.tui import _arrow_down_action, _arrow_up_action
2247
2247
 
2248
- buf = Buffer(multiline=False) # empty input, no history touch
2248
+ buf = Buffer(multiline=False)
2249
2249
  sel = {"i": None}
2250
2250
  event = _FakeEvent()
2251
2251
  _arrow_up_action(event, buf, sel, 3, False)
2252
- assert sel["i"] == 2 and buf.text == "" # selection moves, no history pull
2253
- _arrow_up_action(event, buf, sel, 3, False)
2254
- assert sel["i"] == 1
2252
+ assert sel["i"] is None
2255
2253
  _arrow_down_action(event, buf, sel, 3, False)
2256
- assert sel["i"] == 2
2254
+ assert sel["i"] is None
2257
2255
  assert event.app.invalidated is True
2258
2256
 
2259
2257
 
@@ -5643,11 +5641,10 @@ def test_badge_style_override_breathes_only_when_confirmed_up_to_date():
5643
5641
 
5644
5642
 
5645
5643
  # --------------------------------------------------------------------------
5646
- # webbee-code-click-to-expand-v1: a plain mouse click on a printed tool_result
5647
- # line expands that step's detail card -- the SAME reveal /steps N (and
5648
- # Up/Down + Enter) already give, one gesture shorter, never a new surface.
5644
+ # Legacy click-to-expand is removed: a click on a printed tool_result line is
5645
+ # now a plain no-op -- the pane hook still exists, it just expands nothing.
5649
5646
  # --------------------------------------------------------------------------
5650
- def test_clicking_a_tool_result_line_expands_its_step():
5647
+ def test_clicking_a_tool_result_line_is_a_noop():
5651
5648
  from prompt_toolkit.application import create_app_session, get_app
5652
5649
  from prompt_toolkit.data_structures import Point
5653
5650
  from prompt_toolkit.input import create_pipe_input
@@ -5685,7 +5682,7 @@ def test_clicking_a_tool_result_line_expands_its_step():
5685
5682
  # absolute line 1, not 0, in a freshly-booted pane at offset 0.
5686
5683
  pane.on_line_click(1)
5687
5684
  await asyncio.sleep(0.02)
5688
- assert expanded == [0] # step 1 -> steps_nav index 0
5685
+ assert expanded == [] # no expansion, no crash
5689
5686
  pipe.send_text("\x04")
5690
5687
  ok = await asyncio.wait_for(task, 5)
5691
5688
  assert ok is True
@@ -1 +0,0 @@
1
- __version__ = "0.4.3"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes