hubble-cli 4.1.0__tar.gz → 4.1.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 (60) hide show
  1. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/PKG-INFO +1 -1
  2. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/__init__.py +1 -1
  3. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/banner.py +10 -10
  4. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/repl.py +6 -2
  5. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/update.py +5 -1
  6. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble_cli.egg-info/PKG-INFO +1 -1
  7. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_update.py +48 -0
  8. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/LICENSE +0 -0
  9. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/README.md +0 -0
  10. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/__main__.py +0 -0
  11. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/agent.py +0 -0
  12. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/board.py +0 -0
  13. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/github.py +0 -0
  14. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/hooks.py +0 -0
  15. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/images.py +0 -0
  16. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/keystore.py +0 -0
  17. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/main.py +0 -0
  18. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/mcp.py +0 -0
  19. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/mcp_oauth.py +0 -0
  20. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/models.py +0 -0
  21. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/onboarding.py +0 -0
  22. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/permissions.py +0 -0
  23. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/plugins.py +0 -0
  24. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/prompts.py +0 -0
  25. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/provider.py +0 -0
  26. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/providers.py +0 -0
  27. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/sandbox.py +0 -0
  28. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/scanner.py +0 -0
  29. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/session.py +0 -0
  30. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/settings.py +0 -0
  31. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/skills.py +0 -0
  32. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/spinner.py +0 -0
  33. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/subagents.py +0 -0
  34. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/tools.py +0 -0
  35. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/ui.py +0 -0
  36. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/web.py +0 -0
  37. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/worktree.py +0 -0
  38. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble_cli.egg-info/SOURCES.txt +0 -0
  39. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble_cli.egg-info/dependency_links.txt +0 -0
  40. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble_cli.egg-info/entry_points.txt +0 -0
  41. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble_cli.egg-info/requires.txt +0 -0
  42. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble_cli.egg-info/top_level.txt +0 -0
  43. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/pyproject.toml +0 -0
  44. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/setup.cfg +0 -0
  45. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_agents_plugins.py +0 -0
  46. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_core.py +0 -0
  47. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_github.py +0 -0
  48. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_hook_events.py +0 -0
  49. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_hooks.py +0 -0
  50. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_images.py +0 -0
  51. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_keystore.py +0 -0
  52. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_mcp.py +0 -0
  53. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_mcp_remote.py +0 -0
  54. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_sandbox.py +0 -0
  55. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_scanner.py +0 -0
  56. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_skills.py +0 -0
  57. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_stream_json.py +0 -0
  58. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_task_tool.py +0 -0
  59. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_tools.py +0 -0
  60. {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_worktree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hubble-cli
3
- Version: 4.1.0
3
+ Version: 4.1.2
4
4
  Summary: Hubble: agentic coding CLI for AIHub and other OpenAI-compatible APIs
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/Hamdayrabby/hubble-cli
@@ -1,3 +1,3 @@
1
1
  """Hubble: an agentic coding CLI for OpenAI-compatible gateways such as AIHub."""
2
2
 
3
- __version__ = "4.1.0"
3
+ __version__ = "4.1.2"
@@ -444,17 +444,17 @@ def compose_grid(width: int, height: int, t: float, status: List[Text], tips: Li
444
444
  logo_h = {2: 9, 1: 7, 0: 1} # letter rows + shadow row + tagline
445
445
  top = 2 if width >= BIG_LOGO_WIDTH + 4 else (1 if width >= 58 else 0)
446
446
  mid = min(top, 1)
447
- # Richest to poorest. Below a plain small-logo layout, keep shrinking the status block itself
448
- # (never just tips) so something animated always fits, all the way down to the logo line alone.
447
+ # Richest to poorest. The name is the last thing to shrink: first drop the scene, then the
448
+ # tips, then status lines, and only then fall back to a smaller logo.
449
449
  n_status = len(status)
450
- layouts = [
451
- (top, FULL, True, n_status), (top, COMPACT, True, n_status),
452
- (mid, FULL, True, n_status), (mid, COMPACT, True, n_status),
453
- (top, COMPACT, False, n_status), (mid, COMPACT, False, n_status), (0, COMPACT, False, n_status),
454
- (top, None, True, n_status), (0, None, True, n_status), (0, None, False, n_status),
455
- (0, None, False, min(2, n_status)), (0, None, False, min(1, n_status)),
456
- (0, None, False, 0),
457
- ]
450
+ layouts = []
451
+ for cand_size in dict.fromkeys([top, mid, 0]):
452
+ for cand_art in (FULL, COMPACT, None):
453
+ layouts.append((cand_size, cand_art, True, n_status))
454
+ for cand_art in (COMPACT, None):
455
+ layouts.append((cand_size, cand_art, False, n_status))
456
+ for n in (min(2, n_status), min(1, n_status), 0):
457
+ layouts.append((cand_size, None, False, n))
458
458
  size, art, with_tips, n_status = layouts[-1]
459
459
  for cand_size, cand_art, cand_tips, cand_n in layouts:
460
460
  if cand_art is not None and width < cand_art.width + 24:
@@ -98,6 +98,7 @@ class Repl:
98
98
  self._main_root: Optional[Path] = None # set once /worktree moves the tools elsewhere
99
99
  from hubble.update import UpdateChecker
100
100
  self.updates = UpdateChecker(enabled=bool(agent.settings.get("update_check", True)))
101
+ self._update_scheduled = False
101
102
  self.turn_stats = None
102
103
  self.agent = agent
103
104
  agent.fallback_resolver = self.fallback_for
@@ -263,12 +264,15 @@ class Repl:
263
264
  """Called on every prompt redraw (at least once a second): keep the home screen's model
264
265
  line in step with the running scan, and report a finished scan right away instead of
265
266
  waiting for the next Enter."""
266
- if self.updates.latest and not self.updates.announced:
267
+ # Only when there really is something to print, and only schedule it once: run_in_terminal
268
+ # redraws the prompt, which calls this again -- anything looser here loops forever (flicker).
269
+ if self.updates.has_notice() and not self._update_scheduled:
270
+ self._update_scheduled = True
267
271
  try:
268
272
  from prompt_toolkit.application import get_app, run_in_terminal
269
273
  get_app().loop.call_soon(lambda: run_in_terminal(self._announce_update))
270
274
  except Exception:
271
- pass # not inside a prompt; the main loop shows it before the next one
275
+ self._update_scheduled = False # not inside a prompt; the main loop shows it
272
276
  key = tuple((n, s.status, s.done, s.retry_done) for n, s in self.scanners.items())
273
277
  if key == self._scan_key:
274
278
  return
@@ -89,9 +89,13 @@ class UpdateChecker:
89
89
  except OSError:
90
90
  pass
91
91
 
92
+ def has_notice(self) -> bool:
93
+ """Cheap, side-effect-free: is there a notice waiting to be shown? (Called on every redraw.)"""
94
+ return bool(self.latest) and not self.announced and is_newer(self.latest)
95
+
92
96
  def pending_notice(self) -> Optional[str]:
93
97
  """The notice text once, when a newer version is known and not yet shown."""
94
- if self.announced or not self.latest or not is_newer(self.latest):
98
+ if not self.has_notice():
95
99
  return None
96
100
  self.announced = True
97
101
  return (f"Update available: hubble {__version__} → {self.latest}. "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hubble-cli
3
- Version: 4.1.0
3
+ Version: 4.1.2
4
4
  Summary: Hubble: agentic coding CLI for AIHub and other OpenAI-compatible APIs
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/Hamdayrabby/hubble-cli
@@ -63,6 +63,54 @@ def test_disabled_by_setting_or_env(monkeypatch, tmp_path):
63
63
  assert c.pending_notice() is None
64
64
 
65
65
 
66
+ def _idle_redraws(tmp_path, latest, seconds=2.0):
67
+ """Sit at a real prompt for `seconds` with `latest` known; return how many times it redrew."""
68
+ import threading
69
+
70
+ from prompt_toolkit.application import create_app_session, get_app
71
+ from prompt_toolkit.input import create_pipe_input
72
+ from prompt_toolkit.output import DummyOutput
73
+
74
+ from hubble.agent import Agent
75
+ from hubble.permissions import Permissions
76
+ from hubble.repl import Repl
77
+ from hubble.session import SessionStore
78
+ from hubble.tools import ToolContext
79
+ from hubble.ui import ReplEvents
80
+
81
+ ctx = ToolContext(root=tmp_path)
82
+ agent = Agent(None, {"model": "m", "max_turns": 2, "max_tokens": 10, "context_window": 1000,
83
+ "auto_compact_ratio": 0, "persona": "code"}, ctx, Permissions(), ReplEvents(ctx))
84
+ repl = Repl(agent, SessionStore(tmp_path), {"hubble": object()})
85
+ repl.updates.latest = latest
86
+ counts = {}
87
+ with create_pipe_input() as inp:
88
+ with create_app_session(input=inp, output=DummyOutput()):
89
+ session = repl._prompt_session()
90
+
91
+ def driver():
92
+ time.sleep(seconds)
93
+ counts["renders"] = session.app.render_counter
94
+ inp.send_text("\r")
95
+ threading.Thread(target=driver, daemon=True).start()
96
+ session.prompt("> ")
97
+ return counts["renders"], repl
98
+
99
+
100
+ def test_no_redraw_loop_when_pypi_is_not_newer(tmp_path, monkeypatch):
101
+ """Regression: a known-but-not-newer version made every redraw schedule another redraw
102
+ (run_in_terminal), so the terminal flickered nonstop."""
103
+ renders, repl = _idle_redraws(tmp_path, latest="0.0.1")
104
+ assert renders < 15, f"{renders} redraws in 2s while idle: redraw loop"
105
+ assert repl.updates.pending_notice() is None
106
+
107
+
108
+ def test_newer_version_announced_once_without_redraw_loop(tmp_path):
109
+ renders, repl = _idle_redraws(tmp_path, latest="999.0.0")
110
+ assert renders < 15, f"{renders} redraws in 2s while idle: redraw loop"
111
+ assert repl.updates.announced
112
+
113
+
66
114
  def test_network_failure_is_silent(monkeypatch, tmp_path):
67
115
  monkeypatch.setattr(update, "CACHE_FILE", tmp_path / "c.json")
68
116
  monkeypatch.setattr(update, "_fetch_latest", lambda timeout=4.0: None)
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