diff --git a/strix/interface/tui/backend/controller.py b/strix/interface/tui/backend/controller.py index d74bbba6..f7d2e6c9 100644 --- a/strix/interface/tui/backend/controller.py +++ b/strix/interface/tui/backend/controller.py @@ -24,7 +24,7 @@ from strix.interface.tui.backend.projection import ( sanitize_terminal_text, terminal_projection, ) -from strix.interface.utils import is_subscription_run +from strix.interface.utils import is_subscription_run, subscription_label if TYPE_CHECKING: @@ -162,8 +162,11 @@ class TuiController: if self.report_state is not None: usage = dict(self.report_state.get_total_llm_usage()) subscription = False + subscription_name = "" with contextlib.suppress(Exception): subscription = is_subscription_run(self.report_state) + if subscription: + subscription_name = subscription_label(self.report_state) model_warning = "" if model and not is_recommended_or_frontier_model(model): model_warning = ( @@ -200,6 +203,7 @@ class TuiController: ], "usage": terminal_projection(usage, max_string=256, max_items=20), "subscription": subscription, + "subscription_label": terminal_projection(subscription_name, max_string=64), "viewer_status": self.viewer_status, "viewer_url": terminal_projection(self.viewer_url, max_string=1024), "error": terminal_projection(self.error, max_string=2 * 1024), diff --git a/strix/interface/tui/backend/projection.py b/strix/interface/tui/backend/projection.py index 22fa957e..06d63f9f 100644 --- a/strix/interface/tui/backend/projection.py +++ b/strix/interface/tui/backend/projection.py @@ -175,6 +175,7 @@ def bounded_state_projection(state: dict[str, Any]) -> dict[str, Any]: "messages": [], "usage": {}, "subscription": state["subscription"], + "subscription_label": state["subscription_label"], "viewer_status": state["viewer_status"], "viewer_url": None, "error": terminal_projection(state["error"], max_string=256), diff --git a/strix/interface/tui/internal/app/model_test.go b/strix/interface/tui/internal/app/model_test.go index 2b9f0dfb..ce4d2042 100644 --- a/strix/interface/tui/internal/app/model_test.go +++ b/strix/interface/tui/internal/app/model_test.go @@ -1067,11 +1067,12 @@ func TestBudgetPauseShowsOneWarningToastUntilResumed(t *testing.T) { func TestStatsViewShowsSubscription(t *testing.T) { model := New(nil) - model.snapshot.Model = "gpt-5" + model.snapshot.Model = "grok/grok-4" model.snapshot.Subscription = true + model.snapshot.SubscriptionLabel = "Grok subscription" model.snapshot.Usage = map[string]any{"total_tokens": float64(1200), "cost": 3.5} stats := ansi.Strip(model.statsView()) - if !strings.Contains(stats, "ChatGPT subscription") { + if !strings.Contains(stats, "Grok subscription") { t.Fatalf("stats missing subscription line: %q", stats) } if strings.Contains(stats, "$") { @@ -1079,6 +1080,16 @@ func TestStatsViewShowsSubscription(t *testing.T) { } } +func TestStatsViewSubscriptionFallsBackWithoutLabel(t *testing.T) { + model := New(nil) + model.snapshot.Model = "gpt-5" + model.snapshot.Subscription = true + stats := ansi.Strip(model.statsView()) + if !strings.Contains(stats, "Subscription") { + t.Fatalf("stats missing generic subscription line: %q", stats) + } +} + func TestVulnerabilityMarkdownReport(t *testing.T) { report := vulnerabilityMarkdownReport(map[string]any{ "title": "SQLi in login", diff --git a/strix/interface/tui/internal/app/view.go b/strix/interface/tui/internal/app/view.go index 78a3d9f5..2f13357d 100644 --- a/strix/interface/tui/internal/app/view.go +++ b/strix/interface/tui/internal/app/view.go @@ -596,7 +596,11 @@ func (m Model) statsView() string { if b.Len() > 0 { b.WriteString("\n") } - b.WriteString(lipgloss.NewStyle().Foreground(green).Render("ChatGPT subscription")) + label := m.snapshot.SubscriptionLabel + if label == "" { + label = "Subscription" + } + b.WriteString(lipgloss.NewStyle().Foreground(green).Render(label)) } total := numberValue(m.snapshot.Usage["total_tokens"]) if total > 0 { diff --git a/strix/interface/tui/internal/protocol/protocol.go b/strix/interface/tui/internal/protocol/protocol.go index 38ba63a3..dfe2323b 100644 --- a/strix/interface/tui/internal/protocol/protocol.go +++ b/strix/interface/tui/internal/protocol/protocol.go @@ -68,6 +68,7 @@ type Snapshot struct { Vulnerabilities []map[string]any `json:"-"` Usage map[string]any `json:"usage"` Subscription bool `json:"subscription"` + SubscriptionLabel string `json:"subscription_label"` ViewerStatus string `json:"viewer_status"` ViewerURL *string `json:"viewer_url"` Error *string `json:"error"` diff --git a/strix/interface/utils.py b/strix/interface/utils.py index f73df1c2..5d872769 100644 --- a/strix/interface/utils.py +++ b/strix/interface/utils.py @@ -267,7 +267,7 @@ def is_subscription_run(report_state: Any) -> bool: return subscription.auth_mode(load_settings().llm.model) == "subscription" -def _subscription_label(report_state: Any) -> str: +def subscription_label(report_state: Any) -> str: """Human label for the active model subscription (e.g. "Grok subscription"). Prefers the persisted run record so a resumed run keeps its original provider @@ -386,7 +386,7 @@ def build_live_stats_text(report_state: Any) -> Text: stats_text.append(str(model), style="white") if is_subscription_run(report_state): stats_text.append(" ยท ", style="dim white") - stats_text.append(_subscription_label(report_state), style="#22c55e") + stats_text.append(subscription_label(report_state), style="#22c55e") stats_text.append("\n") vuln_count = len(report_state.vulnerability_reports) @@ -432,7 +432,7 @@ def build_tui_stats_text(report_state: Any) -> Text: subscription = is_subscription_run(report_state) if subscription: stats_text.append("\n") - stats_text.append(_subscription_label(report_state), style="#22c55e") + stats_text.append(subscription_label(report_state), style="#22c55e") usage = _llm_usage(report_state) if usage and _int_stat(usage, "total_tokens") > 0: diff --git a/tests/test_grok_routing.py b/tests/test_grok_routing.py index b54306c4..581bb613 100644 --- a/tests/test_grok_routing.py +++ b/tests/test_grok_routing.py @@ -66,12 +66,12 @@ def test_subscription_label_prefers_persisted_provider(monkeypatch) -> None: # resumed = mock.MagicMock( run_record={"auth_mode": "subscription", "subscription_provider": "Grok"} ) - assert utils._subscription_label(resumed) == "Grok subscription" + assert utils.subscription_label(resumed) == "Grok subscription" # With no persisted provider, it derives the label from settings (not a # hardcoded default). fresh = mock.MagicMock(run_record={}) - assert utils._subscription_label(fresh) == "ChatGPT subscription" + assert utils.subscription_label(fresh) == "ChatGPT subscription" def test_persisted_run_record_carries_provider(tmp_path, monkeypatch) -> None: # type: ignore[no-untyped-def] diff --git a/tests/test_tui_backend_controller.py b/tests/test_tui_backend_controller.py index 0a4ff15c..e7fcefcc 100644 --- a/tests/test_tui_backend_controller.py +++ b/tests/test_tui_backend_controller.py @@ -110,6 +110,28 @@ def test_state_populates_model_warning_for_non_frontier_model() -> None: assert "not a recommended frontier model" in warning +def test_snapshot_carries_the_subscription_provider_label() -> None: + os.environ["STRIX_LLM"] = "grok/grok-4" + loader._cached = None + + snapshot = TuiController(args()).snapshot() + + # The TUI renders this label, so it must name the actual provider rather + # than assuming ChatGPT. + assert snapshot["subscription"] is True + assert snapshot["subscription_label"] == "Grok subscription" + + +def test_snapshot_has_no_subscription_label_for_api_key_runs() -> None: + os.environ["STRIX_LLM"] = "openai/gpt-5.4" + loader._cached = None + + snapshot = TuiController(args()).snapshot() + + assert snapshot["subscription"] is False + assert snapshot["subscription_label"] == "" + + def test_setup_restores_prepared_cli_targets() -> None: setup_args = args() setup_args.targets_info = [