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.
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/PKG-INFO +1 -1
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/__init__.py +1 -1
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/banner.py +10 -10
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/repl.py +6 -2
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/update.py +5 -1
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble_cli.egg-info/PKG-INFO +1 -1
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_update.py +48 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/LICENSE +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/README.md +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/__main__.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/agent.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/board.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/github.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/hooks.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/images.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/keystore.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/main.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/mcp.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/mcp_oauth.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/models.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/onboarding.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/permissions.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/plugins.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/prompts.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/provider.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/providers.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/sandbox.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/scanner.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/session.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/settings.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/skills.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/spinner.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/subagents.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/tools.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/ui.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/web.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble/worktree.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble_cli.egg-info/SOURCES.txt +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble_cli.egg-info/dependency_links.txt +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble_cli.egg-info/entry_points.txt +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble_cli.egg-info/requires.txt +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/hubble_cli.egg-info/top_level.txt +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/pyproject.toml +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/setup.cfg +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_agents_plugins.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_core.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_github.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_hook_events.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_hooks.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_images.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_keystore.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_mcp.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_mcp_remote.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_sandbox.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_scanner.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_skills.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_stream_json.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_task_tool.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_tools.py +0 -0
- {hubble_cli-4.1.0 → hubble_cli-4.1.2}/tests/test_worktree.py +0 -0
|
@@ -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.
|
|
448
|
-
#
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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}. "
|
|
@@ -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
|
|
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
|