webbee 0.2.1__tar.gz → 0.2.3__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 (73) hide show
  1. {webbee-0.2.1 → webbee-0.2.3}/CHANGELOG.md +17 -0
  2. {webbee-0.2.1 → webbee-0.2.3}/PKG-INFO +1 -1
  3. {webbee-0.2.1 → webbee-0.2.3}/pyproject.toml +1 -1
  4. webbee-0.2.3/src/webbee/__init__.py +1 -0
  5. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/render.py +21 -2
  6. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/session.py +3 -0
  7. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/tui.py +20 -11
  8. {webbee-0.2.1 → webbee-0.2.3}/tests/test_marathon.py +11 -0
  9. {webbee-0.2.1 → webbee-0.2.3}/tests/test_tui.py +44 -14
  10. webbee-0.2.1/src/webbee/__init__.py +0 -1
  11. {webbee-0.2.1 → webbee-0.2.3}/.github/workflows/publish.yml +0 -0
  12. {webbee-0.2.1 → webbee-0.2.3}/.gitignore +0 -0
  13. {webbee-0.2.1 → webbee-0.2.3}/LICENSE +0 -0
  14. {webbee-0.2.1 → webbee-0.2.3}/README.md +0 -0
  15. {webbee-0.2.1 → webbee-0.2.3}/install.sh +0 -0
  16. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/account.py +0 -0
  17. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/banner_art.py +0 -0
  18. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/cli.py +0 -0
  19. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/clipboard.py +0 -0
  20. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/commands.py +0 -0
  21. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/config.py +0 -0
  22. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/details.py +0 -0
  23. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/events.py +0 -0
  24. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/frames.py +0 -0
  25. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/intel/__init__.py +0 -0
  26. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/intel/chunker.py +0 -0
  27. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/intel/embed.py +0 -0
  28. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/intel/graph.py +0 -0
  29. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/intel/indexer.py +0 -0
  30. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/intel/models.py +0 -0
  31. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/intel/query.py +0 -0
  32. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/intel/service.py +0 -0
  33. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/intel/store.py +0 -0
  34. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/intel/vectors.py +0 -0
  35. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/intel/watch.py +0 -0
  36. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/output_pane.py +0 -0
  37. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/repl.py +0 -0
  38. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/repo.py +0 -0
  39. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/sessions.py +0 -0
  40. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/stream.py +0 -0
  41. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/tools.py +0 -0
  42. {webbee-0.2.1 → webbee-0.2.3}/src/webbee/update.py +0 -0
  43. {webbee-0.2.1 → webbee-0.2.3}/tests/__init__.py +0 -0
  44. {webbee-0.2.1 → webbee-0.2.3}/tests/test_account.py +0 -0
  45. {webbee-0.2.1 → webbee-0.2.3}/tests/test_cli.py +0 -0
  46. {webbee-0.2.1 → webbee-0.2.3}/tests/test_clipboard.py +0 -0
  47. {webbee-0.2.1 → webbee-0.2.3}/tests/test_commands.py +0 -0
  48. {webbee-0.2.1 → webbee-0.2.3}/tests/test_config.py +0 -0
  49. {webbee-0.2.1 → webbee-0.2.3}/tests/test_cpc_contract_stable.py +0 -0
  50. {webbee-0.2.1 → webbee-0.2.3}/tests/test_details.py +0 -0
  51. {webbee-0.2.1 → webbee-0.2.3}/tests/test_events.py +0 -0
  52. {webbee-0.2.1 → webbee-0.2.3}/tests/test_freeze_fix.py +0 -0
  53. {webbee-0.2.1 → webbee-0.2.3}/tests/test_intel_chunker.py +0 -0
  54. {webbee-0.2.1 → webbee-0.2.3}/tests/test_intel_embed.py +0 -0
  55. {webbee-0.2.1 → webbee-0.2.3}/tests/test_intel_graph.py +0 -0
  56. {webbee-0.2.1 → webbee-0.2.3}/tests/test_intel_indexer.py +0 -0
  57. {webbee-0.2.1 → webbee-0.2.3}/tests/test_intel_no_numpy.py +0 -0
  58. {webbee-0.2.1 → webbee-0.2.3}/tests/test_intel_query.py +0 -0
  59. {webbee-0.2.1 → webbee-0.2.3}/tests/test_intel_service.py +0 -0
  60. {webbee-0.2.1 → webbee-0.2.3}/tests/test_intel_store.py +0 -0
  61. {webbee-0.2.1 → webbee-0.2.3}/tests/test_intel_vectors.py +0 -0
  62. {webbee-0.2.1 → webbee-0.2.3}/tests/test_intel_watch.py +0 -0
  63. {webbee-0.2.1 → webbee-0.2.3}/tests/test_packaging.py +0 -0
  64. {webbee-0.2.1 → webbee-0.2.3}/tests/test_render.py +0 -0
  65. {webbee-0.2.1 → webbee-0.2.3}/tests/test_repl.py +0 -0
  66. {webbee-0.2.1 → webbee-0.2.3}/tests/test_repo.py +0 -0
  67. {webbee-0.2.1 → webbee-0.2.3}/tests/test_session.py +0 -0
  68. {webbee-0.2.1 → webbee-0.2.3}/tests/test_sessions.py +0 -0
  69. {webbee-0.2.1 → webbee-0.2.3}/tests/test_stream.py +0 -0
  70. {webbee-0.2.1 → webbee-0.2.3}/tests/test_tools.py +0 -0
  71. {webbee-0.2.1 → webbee-0.2.3}/tests/test_tui_hardening.py +0 -0
  72. {webbee-0.2.1 → webbee-0.2.3}/tests/test_update.py +0 -0
  73. {webbee-0.2.1 → webbee-0.2.3}/tests/test_version.py +0 -0
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.3
4
+
5
+ - **Readable big numbers.** Token and credit counts in the toolbar now scale
6
+ (`1.5M tok`, `2M credits`) instead of an awkward `1500.0k`.
7
+ - **Esc really stops a turn now.** Previously only Ctrl-C worked; Esc now cancels
8
+ the running turn too, matching the "Esc/Ctrl-C to stop" hint.
9
+ - **The `❯` prompt takes your mode's colour** — cyan (default), purple (plan),
10
+ yellow (autopilot) — so the current mode is obvious from the input line.
11
+
12
+ ## 0.2.2
13
+
14
+ - **See Webbee's reasoning as a distinct 💭 block.** Before each step, Webbee now
15
+ shows a genuine line of thinking — what it's weighing and why — rendered as its
16
+ own 💭 block, separate from status lines. Works on every model.
17
+ - **SSH steps are visible.** When Webbee runs a command on one of your connected
18
+ servers, the terminal now shows that step (command + result), like any other.
19
+
3
20
  ## 0.2.1
4
21
 
5
22
  - **Fixes a marathon that could hang on "working" and quietly burn credits.** A
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webbee
3
- Version: 0.2.1
3
+ Version: 0.2.3
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.2.1"
3
+ version = "0.2.3"
4
4
  description = "Webbee 🐝 — the Imperal Cloud coding agent in your terminal"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1 @@
1
+ __version__ = "0.2.3"
@@ -27,8 +27,20 @@ _ACCENT = "cyan" # interactive chrome ONLY — live caret / mode / panel ur
27
27
 
28
28
 
29
29
  def _fmt_tokens(n: int) -> str:
30
- """Compact token count: 2100 -> '2.1k', 900 -> '900'."""
31
- return f"{n / 1000:.1f}k" if n >= 1000 else str(int(n))
30
+ """Compact count for the live toolbar: 900 -> '900', 2_100 -> '2.1k',
31
+ 1_500_000 -> '1.5M', 2_000_000 -> '2M', 3_200_000_000 -> '3.2B'. Used for
32
+ both token counts and (integer) credits so big numbers stay readable."""
33
+ try:
34
+ n = int(n or 0)
35
+ except (TypeError, ValueError):
36
+ return "0"
37
+ a = abs(n)
38
+ if a < 1000:
39
+ return str(n)
40
+ for div, suf in ((1_000_000_000, "B"), (1_000_000, "M"), (1000, "k")):
41
+ if a >= div:
42
+ return f"{n / div:.1f}".rstrip("0").rstrip(".") + suf
43
+ return str(n)
32
44
 
33
45
 
34
46
  def _salient_arg(args: dict) -> str:
@@ -325,6 +337,13 @@ class RichSink:
325
337
  self.console.print(Text(" " + text, style="dim italic"))
326
338
  self._nudge()
327
339
 
340
+ def thinking(self, text: str) -> None:
341
+ # System-driven reasoning as a distinct 💭 block — visually apart from the
342
+ # dim `progress` line (which stays reserved for status like low-balance).
343
+ if text:
344
+ self.console.print(Text(" 💭 " + text, style="italic medium_purple3"))
345
+ self._nudge()
346
+
328
347
  def plan_blocked(self, tool: str) -> None:
329
348
  """Plan mode auto-declines writes/destructive. Tell the user WHY and how
330
349
  to allow it (Shift+Tab). Autopilot and default never reach this."""
@@ -272,6 +272,9 @@ class AgentSession:
272
272
  elif ftype == "step_finished": # v2 (Slice-5 T8 dual-emit)
273
273
  handle_step_finished(frame, sink, finished, step_labels, self.steps, local_ids)
274
274
 
275
+ elif ftype == "thinking": # system-driven reasoning -> the 💭 block
276
+ (getattr(sink, "thinking", None) or sink.progress)(_progress_text(frame))
277
+
275
278
  elif ftype == "progress": # P2 — dual-reads llm_text (v2) / text (legacy)
276
279
  sink.progress(_progress_text(frame))
277
280
 
@@ -44,15 +44,19 @@ def build_toolbar(mode: str, tokens: int, credits: int, *, busy: bool = False,
44
44
  return [("class:tb.dim", " mode: "),
45
45
  (f"class:tb.mode.{mode}", mode),
46
46
  ("class:tb.dim",
47
- f" · {_fmt_tokens(tokens)} tok · {credits} credits · Shift + TAB: switch mode")]
48
-
49
-
50
- def _escape_action(sel: dict, is_busy, stop_turn, event) -> None:
51
- """Esc key binding (P5g). While a turn is running, ask the server to stop
52
- it (`stop_turn` posts the cancel; fail-soft, best-effort) and leave the
53
- step-selection untouched while idle (unchanged), clear the selection."""
54
- if is_busy() and stop_turn is not None:
55
- event.app.create_background_task(stop_turn())
47
+ f" · {_fmt_tokens(tokens)} tok · {_fmt_tokens(credits)} credits · Shift + TAB: switch mode")]
48
+
49
+
50
+ def _escape_action(sel: dict, turn: dict, is_busy, stop_turn, event) -> None:
51
+ """Esc key binding (P5g). While a turn is running, STOP it cancel the LOCAL
52
+ turn task (what actually tears the turn down, same as Ctrl-C) AND ask the
53
+ server to stop (so it stops spending). While idle, clear the step-selection."""
54
+ if is_busy():
55
+ t = turn.get("task")
56
+ if t is not None and not t.done():
57
+ if stop_turn is not None:
58
+ event.app.create_background_task(stop_turn())
59
+ t.cancel() # cancel the running turn; dock survives
56
60
  return
57
61
  sel["i"] = None
58
62
  event.app.invalidate()
@@ -157,7 +161,7 @@ async def run_session(*, pane, on_line, mode_getter, on_cycle, status,
157
161
 
158
162
  @kb.add("escape")
159
163
  def _step_clear(event):
160
- _escape_action(sel, is_busy, stop_turn, event)
164
+ _escape_action(sel, turn, is_busy, stop_turn, event)
161
165
 
162
166
  @kb.add("pageup")
163
167
  def _pgup(event):
@@ -190,8 +194,13 @@ async def run_session(*, pane, on_line, mode_getter, on_cycle, status,
190
194
  rows = sum(max(1, -(-len(ln) // cols)) for ln in text.split("\n"))
191
195
  return min(10, max(1, rows))
192
196
 
197
+ def _prompt_fragments():
198
+ # The ❯ takes the CURRENT mode's colour (same classes the toolbar uses)
199
+ # so the mode is obvious from the input line itself, not just the toolbar.
200
+ return [(f"class:tb.mode.{mode_getter()}", "❯ ")]
201
+
193
202
  input_win = Window(
194
- BufferControl(buffer=buf, input_processors=[BeforeInput("❯ ", style="class:prompt")]),
203
+ BufferControl(buffer=buf, input_processors=[BeforeInput(_prompt_fragments)]),
195
204
  height=_input_height, wrap_lines=True)
196
205
  toolbar = Window(FormattedTextControl(_toolbar), height=1, always_hide_cursor=True)
197
206
  root = HSplit([pane.window, Frame(input_win), toolbar])
@@ -154,6 +154,7 @@ def _run_marathon_capture_post(monkeypatch, tmp_path, goal="build X", frames=Non
154
154
  class NoteSink:
155
155
  def __init__(self): self.notes = []
156
156
  def note(self, msg): self.notes.append(msg)
157
+ def thinking(self, msg): self.thinks = getattr(self, "thinks", []) + [msg]
157
158
  def tool_start(self, *a): ...
158
159
  def tool_result(self, *a): ...
159
160
  def ask_consent(self, *a): return "y"
@@ -202,6 +203,16 @@ def test_marathon_complete_is_terminal(monkeypatch, tmp_path):
202
203
  assert result == "goal done"
203
204
 
204
205
 
206
+ def test_thinking_frame_renders_as_distinct_channel(monkeypatch, tmp_path):
207
+ # A `thinking` frame routes to the distinct 💭 reasoning channel
208
+ # (sink.thinking), NOT the plain progress/note lines.
209
+ _, _, sink = _run_marathon_capture_post(monkeypatch, tmp_path, frames=[
210
+ {"type": "thinking", "task_id": "OURS", "text": "assessing the repo before editing"},
211
+ {"type": "marathon_complete", "task_id": "OURS", "text": "done"},
212
+ ])
213
+ assert getattr(sink, "thinks", []) == ["assessing the repo before editing"]
214
+
215
+
205
216
  def test_marathon_paused_is_terminal_and_noted(monkeypatch, tmp_path):
206
217
  _, result, sink = _run_marathon_capture_post(monkeypatch, tmp_path, frames=[
207
218
  {"type": "marathon_paused", "task_id": "OURS", "reason": "out of credits"},
@@ -22,13 +22,30 @@ def test_next_mode_unknown_resets():
22
22
  def test_toolbar_idle_has_mode_tokens_cost_and_hint():
23
23
  t = _txt(build_toolbar("plan", 51000, 66))
24
24
  assert "plan" in t
25
- assert "51.0k" in t
25
+ assert "51k" in t
26
26
  assert "66 credits" in t
27
27
  assert "Shift + TAB" in t # spelled in words, no glyph
28
28
  assert "⇧⇥" not in t # the ⇧⇥ glyph must NOT appear
29
29
  assert not NO_CYRILLIC.search(t)
30
30
 
31
31
 
32
+ def test_toolbar_humanizes_millions_and_credits():
33
+ # Big numbers stay readable: tokens + credits both scale to k/M/B.
34
+ t = _txt(build_toolbar("default", 1_500_000, 2_000_000))
35
+ assert "1.5M tok" in t
36
+ assert "2M credits" in t
37
+
38
+
39
+ def test_fmt_tokens_scales_k_m_b():
40
+ from webbee.render import _fmt_tokens
41
+ assert _fmt_tokens(900) == "900"
42
+ assert _fmt_tokens(2100) == "2.1k"
43
+ assert _fmt_tokens(51000) == "51k"
44
+ assert _fmt_tokens(1_500_000) == "1.5M"
45
+ assert _fmt_tokens(2_000_000) == "2M"
46
+ assert _fmt_tokens(3_200_000_000) == "3.2B"
47
+
48
+
32
49
  def test_toolbar_busy_state_shows_working_dot_and_stop_hint():
33
50
  t = _txt(build_toolbar("default", 1200, 0.0143, busy=True,
34
51
  current="notes·delete_note", elapsed=4, tools=3))
@@ -203,7 +220,15 @@ def _run_and_drain(coro):
203
220
  asyncio.run(coro)
204
221
 
205
222
 
206
- def test_escape_while_busy_schedules_server_stop_and_leaves_selection():
223
+ class _FakeTask:
224
+ def __init__(self): self.cancelled = []
225
+ def done(self): return False
226
+ def cancel(self): self.cancelled.append(1)
227
+
228
+
229
+ def test_escape_while_busy_cancels_turn_and_schedules_server_stop():
230
+ # Esc now REALLY stops a running turn: cancel the local turn task (what tears
231
+ # it down) AND ask the server to stop — matching the "Esc/Ctrl-C" hint.
207
232
  from webbee.tui import _escape_action
208
233
 
209
234
  stop_calls = []
@@ -213,13 +238,15 @@ def test_escape_while_busy_schedules_server_stop_and_leaves_selection():
213
238
 
214
239
  event = _FakeEvent()
215
240
  sel = {"i": 2}
216
- _escape_action(sel, lambda: True, stop_turn, event)
241
+ task = _FakeTask()
242
+ turn = {"task": task}
243
+ _escape_action(sel, turn, lambda: True, stop_turn, event)
217
244
 
245
+ assert task.cancelled == [1] # the local turn is actually cancelled
218
246
  assert len(event.app.background_tasks) == 1
219
247
  _run_and_drain(event.app.background_tasks[0])
220
- assert stop_calls == [1]
221
- # Esc-while-busy interrupts — it does NOT also clear the step-selection
222
- # or invalidate the toolbar (that's the idle-path job, tested below).
248
+ assert stop_calls == [1] # AND the server is asked to stop
249
+ # Busy-path stops the turn — it does NOT also clear the step-selection.
223
250
  assert sel["i"] == 2
224
251
  assert event.app.invalidated is False
225
252
 
@@ -234,7 +261,8 @@ def test_escape_while_idle_clears_step_selection_unchanged():
234
261
 
235
262
  event = _FakeEvent()
236
263
  sel = {"i": 2}
237
- _escape_action(sel, lambda: False, stop_turn, event)
264
+ turn = {"task": None}
265
+ _escape_action(sel, turn, lambda: False, stop_turn, event)
238
266
 
239
267
  assert sel["i"] is None
240
268
  assert event.app.invalidated is True
@@ -242,18 +270,20 @@ def test_escape_while_idle_clears_step_selection_unchanged():
242
270
  assert stop_calls == []
243
271
 
244
272
 
245
- def test_escape_while_busy_with_no_stop_turn_falls_back_to_clearing():
246
- # stop_turn=None (e.g. no repl wiring) must never crash Esc — it just
247
- # keeps the pre-P5g behavior of clearing the step-selection.
273
+ def test_escape_while_busy_with_no_stop_turn_still_cancels_locally():
274
+ # stop_turn=None (e.g. no repl wiring) must never crash Esc — it still cancels
275
+ # the local turn task, so Esc reliably stops even without the server hook.
248
276
  from webbee.tui import _escape_action
249
277
 
250
278
  event = _FakeEvent()
251
279
  sel = {"i": 1}
252
- _escape_action(sel, lambda: True, None, event)
280
+ task = _FakeTask()
281
+ turn = {"task": task}
282
+ _escape_action(sel, turn, lambda: True, None, event)
253
283
 
254
- assert sel["i"] is None
255
- assert event.app.invalidated is True
256
- assert event.app.background_tasks == []
284
+ assert task.cancelled == [1] # local teardown happens regardless
285
+ assert event.app.background_tasks == [] # no stop_turn -> nothing scheduled
286
+ assert sel["i"] == 1 # busy-path leaves the selection
257
287
 
258
288
 
259
289
  def test_interrupt_while_busy_cancels_local_task_and_schedules_server_stop():
@@ -1 +0,0 @@
1
- __version__ = "0.2.1"
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