diff --git a/apps/backend/internal/agent/settings/controller/controller.go b/apps/backend/internal/agent/settings/controller/controller.go index 1786d17e07..5d953e5044 100644 --- a/apps/backend/internal/agent/settings/controller/controller.go +++ b/apps/backend/internal/agent/settings/controller/controller.go @@ -56,7 +56,6 @@ type Controller struct { mcpService *mcpconfig.Service modelCache *modelfetcher.Cache hostUtility *hostutility.Manager - hostUsage HostUsageLister jobStore *JobStore hub JobBroadcaster logger *logger.Logger diff --git a/apps/backend/internal/agent/settings/controller/subscription_usage.go b/apps/backend/internal/agent/settings/controller/subscription_usage.go deleted file mode 100644 index feafa324c7..0000000000 --- a/apps/backend/internal/agent/settings/controller/subscription_usage.go +++ /dev/null @@ -1,43 +0,0 @@ -package controller - -import ( - "context" - - "github.com/kandev/kandev/internal/agent/settings/dto" - agentusage "github.com/kandev/kandev/internal/agent/usage" -) - -// HostUsageLister lists subscription utilization for host-installed agent CLIs. -// Implemented by usage.HostService. fresh bypasses the 5-minute cache (still -// bounded by a short server-side clamp). -type HostUsageLister interface { - List(ctx context.Context, fresh bool) []agentusage.HostAgentUsage -} - -// SetHostUsageLister wires in the host subscription-usage service. Optional — -// when unset SubscriptionUsage returns an empty listing. -func (c *Controller) SetHostUsageLister(l HostUsageLister) { - c.hostUsage = l -} - -// SubscriptionUsage returns utilization for host agents authenticated with -// subscription (OAuth) credentials, enriched with registry display names. -func (c *Controller) SubscriptionUsage(ctx context.Context, fresh bool) *dto.AgentSubscriptionUsageResponse { - resp := &dto.AgentSubscriptionUsageResponse{Agents: []dto.AgentSubscriptionUsage{}} - if c.hostUsage == nil { - return resp - } - for _, entry := range c.hostUsage.List(ctx, fresh) { - displayName := entry.AgentID - if ag, ok := c.agentRegistry.Get(entry.AgentID); ok { - displayName = ag.DisplayName() - } - resp.Agents = append(resp.Agents, dto.AgentSubscriptionUsage{ - AgentID: entry.AgentID, - DisplayName: displayName, - Usage: entry.Usage, - Error: entry.Error, - }) - } - return resp -} diff --git a/apps/backend/internal/agent/settings/controller/subscription_usage_test.go b/apps/backend/internal/agent/settings/controller/subscription_usage_test.go deleted file mode 100644 index 2c2cf20cd4..0000000000 --- a/apps/backend/internal/agent/settings/controller/subscription_usage_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package controller - -import ( - "context" - "testing" - "time" - - "github.com/kandev/kandev/internal/agent/agents" - agentusage "github.com/kandev/kandev/internal/agent/usage" -) - -type fakeHostUsageLister struct { - entries []agentusage.HostAgentUsage - lastFresh bool - calls int -} - -func (f *fakeHostUsageLister) List(_ context.Context, fresh bool) []agentusage.HostAgentUsage { - f.calls++ - f.lastFresh = fresh - return f.entries -} - -func TestControllerSubscriptionUsage_NoLister(t *testing.T) { - ctrl := newTestController(map[string]agents.Agent{}) - - resp := ctrl.SubscriptionUsage(context.Background(), false) - - if resp == nil || resp.Agents == nil { - t.Fatalf("expected non-nil response with empty agents, got %+v", resp) - } - if len(resp.Agents) != 0 { - t.Fatalf("expected empty listing without a lister, got %+v", resp.Agents) - } -} - -func TestControllerSubscriptionUsage_MapsEntries(t *testing.T) { - ctrl := newTestController(map[string]agents.Agent{ - "claude-acp": &testAgent{id: "claude-acp", name: "claude-acp", displayName: "Claude", enabled: true}, - }) - now := time.Now() - usage := &agentusage.ProviderUsage{ - Provider: "anthropic", - Plan: "max", - Windows: []agentusage.UtilizationWindow{{Label: "5-hour", UtilizationPct: 42, ResetAt: now}}, - FetchedAt: now, - } - lister := &fakeHostUsageLister{entries: []agentusage.HostAgentUsage{ - {AgentID: "claude-acp", Usage: usage}, - {AgentID: "unknown-acp", Error: "failed to fetch usage from provider"}, - }} - ctrl.SetHostUsageLister(lister) - - resp := ctrl.SubscriptionUsage(context.Background(), false) - - if len(resp.Agents) != 2 { - t.Fatalf("agents = %+v, want 2 entries", resp.Agents) - } - // Registry display-name lookup for known agents. - if resp.Agents[0].AgentID != "claude-acp" || resp.Agents[0].DisplayName != "Claude" { - t.Errorf("entry[0] = %+v", resp.Agents[0]) - } - if resp.Agents[0].Usage != usage || resp.Agents[0].Error != "" { - t.Errorf("entry[0] usage/error = %+v", resp.Agents[0]) - } - // Unknown agents fall back to the agent ID and forward the error. - if resp.Agents[1].DisplayName != "unknown-acp" || resp.Agents[1].Error == "" { - t.Errorf("entry[1] = %+v", resp.Agents[1]) - } - if resp.Agents[1].Usage != nil { - t.Errorf("entry[1] usage = %+v, want nil", resp.Agents[1].Usage) - } -} - -func TestControllerSubscriptionUsage_PropagatesFresh(t *testing.T) { - ctrl := newTestController(map[string]agents.Agent{}) - lister := &fakeHostUsageLister{} - ctrl.SetHostUsageLister(lister) - - _ = ctrl.SubscriptionUsage(context.Background(), true) - if !lister.lastFresh { - t.Error("fresh=true was not propagated to the lister") - } - _ = ctrl.SubscriptionUsage(context.Background(), false) - if lister.lastFresh { - t.Error("fresh=false was not propagated to the lister") - } - if lister.calls != 2 { - t.Errorf("lister calls = %d, want 2", lister.calls) - } -} diff --git a/apps/backend/internal/agent/settings/dto/dto.go b/apps/backend/internal/agent/settings/dto/dto.go index 2eab5480db..a2da14a500 100644 --- a/apps/backend/internal/agent/settings/dto/dto.go +++ b/apps/backend/internal/agent/settings/dto/dto.go @@ -4,23 +4,8 @@ import ( "time" "github.com/kandev/kandev/internal/agent/mcpconfig" - agentusage "github.com/kandev/kandev/internal/agent/usage" ) -// AgentSubscriptionUsage is one subscription-billed host agent's utilization -// entry returned by GET /api/v1/agents/usage. -type AgentSubscriptionUsage struct { - AgentID string `json:"agent_id"` - DisplayName string `json:"display_name"` - Usage *agentusage.ProviderUsage `json:"usage,omitempty"` - Error string `json:"error,omitempty"` -} - -// AgentSubscriptionUsageResponse is the GET /api/v1/agents/usage payload. -type AgentSubscriptionUsageResponse struct { - Agents []AgentSubscriptionUsage `json:"agents"` -} - type AgentProfileDTO struct { ID string `json:"id"` AgentID string `json:"agent_id"` diff --git a/apps/backend/internal/agent/settings/handlers/handlers.go b/apps/backend/internal/agent/settings/handlers/handlers.go index c6c1e8322e..17fa8ef729 100644 --- a/apps/backend/internal/agent/settings/handlers/handlers.go +++ b/apps/backend/internal/agent/settings/handlers/handlers.go @@ -19,8 +19,6 @@ import ( "go.uber.org/zap" ) -const queryValueTrue = "true" - var availableAgentsBroadcastTimeout = 10 * time.Second type Handlers struct { @@ -55,7 +53,6 @@ func (h *Handlers) registerHTTP(router *gin.Engine) { api := router.Group("/api/v1") api.GET("/agents/discovery", h.httpDiscoverAgents) api.GET("/agents/available", h.httpListAvailableAgents) - api.GET("/agents/usage", h.httpAgentSubscriptionUsage) api.GET("/agents", h.httpListAgents) api.POST("/agents", h.interlock, h.httpCreateAgent) api.POST("/agents/tui", h.interlock, h.httpCreateCustomTUIAgent) @@ -87,11 +84,6 @@ func (h *Handlers) httpDiscoverAgents(c *gin.Context) { h.broadcastAvailableAgentsAsync() } -func (h *Handlers) httpAgentSubscriptionUsage(c *gin.Context) { - fresh := c.Query("fresh") == queryValueTrue || c.Query("fresh") == "1" - c.JSON(http.StatusOK, h.controller.SubscriptionUsage(c.Request.Context(), fresh)) -} - func (h *Handlers) httpListAvailableAgents(c *gin.Context) { resp, err := h.controller.ListAvailableAgents(c.Request.Context()) if err != nil { @@ -485,7 +477,7 @@ func (h *Handlers) httpUpdateProfile(c *gin.Context) { } func (h *Handlers) httpDeleteProfile(c *gin.Context) { - force := c.Query("force") == queryValueTrue + force := c.Query("force") == "true" profile, err := h.controller.DeleteProfile(c.Request.Context(), c.Param("id"), force) if err != nil { if err == controller.ErrAgentProfileNotFound { @@ -589,7 +581,7 @@ func (h *Handlers) httpGetAgentModels(c *gin.Context) { } // Check for refresh query parameter - refresh := c.Query("refresh") == queryValueTrue + refresh := c.Query("refresh") == "true" resp, err := h.controller.FetchDynamicModels(c.Request.Context(), agentName, refresh) if err != nil { diff --git a/apps/backend/internal/agent/usage/host.go b/apps/backend/internal/agent/usage/host.go deleted file mode 100644 index 43349d40ca..0000000000 --- a/apps/backend/internal/agent/usage/host.go +++ /dev/null @@ -1,106 +0,0 @@ -package usage - -import ( - "context" - "os" - "path/filepath" - "time" - - "go.uber.org/zap" - - "github.com/kandev/kandev/internal/common/logger" -) - -// freshMaxAge clamps fresh (hover-triggered) fetches: a provider is queried -// again at most every 15 s even when callers keep requesting fresh data. -const freshMaxAge = 15 * time.Second - -// hostUsageFetchError is the sanitized per-agent error surfaced through the -// API. Raw provider errors can contain credential paths and response bodies, -// so those are only logged server-side. -const hostUsageFetchError = "failed to fetch usage from provider" - -// HostAgentUsage is one host-installed subscription agent's usage entry. -type HostAgentUsage struct { - AgentID string `json:"agent_id"` - Usage *ProviderUsage `json:"usage,omitempty"` - Error string `json:"error,omitempty"` -} - -// hostUsageClient is a ProviderUsageClient that can also report whether -// subscription credentials are present on disk. -type hostUsageClient interface { - ProviderUsageClient - HasSubscriptionCredentials() bool -} - -type hostEntry struct { - agentID string - cacheKey string - client hostUsageClient -} - -// HostService lists subscription utilization for agent CLIs installed on the -// kandev host (Claude Code, Codex). Agents without subscription credentials -// are omitted from List results. -type HostService struct { - cache *UsageCache - entries []hostEntry - logger *logger.Logger -} - -// NewHostService builds a HostService reading credentials from the current -// user's home directory. The service is empty when the home dir is unavailable. -func NewHostService(log *logger.Logger) *HostService { - s := &HostService{cache: NewUsageCache(), logger: log} - home, err := os.UserHomeDir() - if err != nil { - return s - } - claudePath := filepath.Join(home, ".claude", ".credentials.json") - codexPath := filepath.Join(home, ".codex", "auth.json") - s.entries = []hostEntry{ - { - agentID: "claude-acp", - cacheKey: CacheKey("anthropic", claudePath), - client: NewClaudeUsageClientWithPath(claudePath), - }, - { - agentID: "codex-acp", - cacheKey: CacheKey("openai", codexPath), - client: NewCodexUsageClientWithPath(codexPath), - }, - } - return s -} - -// List returns usage for every host agent that has subscription credentials. -// With fresh=false cached results up to 5 minutes old are served; fresh=true -// re-queries the providers, bounded by freshMaxAge. Fetch failures are -// reported per-agent via the Error field rather than failing the whole listing. -func (s *HostService) List(ctx context.Context, fresh bool) []HostAgentUsage { - maxAge := cacheTTL - if fresh { - maxAge = freshMaxAge - } - out := make([]HostAgentUsage, 0, len(s.entries)) - for _, e := range s.entries { - if !e.client.HasSubscriptionCredentials() { - continue - } - entry := HostAgentUsage{AgentID: e.agentID} - u, err := s.cache.GetOrFetchWithin(ctx, e.cacheKey, maxAge, e.client.FetchUsage) - switch { - case err != nil: - if s.logger != nil { - s.logger.Warn("subscription usage fetch failed", - zap.String("agent", e.agentID), zap.Error(err)) - } - entry.Error = hostUsageFetchError - default: - entry.Usage = u - } - out = append(out, entry) - } - return out -} diff --git a/apps/backend/internal/agent/usage/host_internal_test.go b/apps/backend/internal/agent/usage/host_internal_test.go deleted file mode 100644 index ee39885121..0000000000 --- a/apps/backend/internal/agent/usage/host_internal_test.go +++ /dev/null @@ -1,240 +0,0 @@ -package usage - -import ( - "context" - "errors" - "sync" - "sync/atomic" - "testing" - "time" -) - -type fakeHostClient struct { - hasCreds bool - usage *ProviderUsage - err error - calls int -} - -func (f *fakeHostClient) FetchUsage(_ context.Context) (*ProviderUsage, error) { - f.calls++ - return f.usage, f.err -} - -func (f *fakeHostClient) HasSubscriptionCredentials() bool { return f.hasCreds } - -func TestHostServiceList(t *testing.T) { - now := time.Now() - okUsage := &ProviderUsage{ - Provider: "anthropic", - Plan: "max", - Windows: []UtilizationWindow{{Label: "5-hour", UtilizationPct: 42, ResetAt: now}}, - FetchedAt: now, - } - noCreds := &fakeHostClient{hasCreds: false} - ok := &fakeHostClient{hasCreds: true, usage: okUsage} - failing := &fakeHostClient{hasCreds: true, err: errors.New("boom")} - - svc := &HostService{ - cache: NewUsageCache(), - entries: []hostEntry{ - {agentID: "claude-acp", cacheKey: "k1", client: noCreds}, - {agentID: "codex-acp", cacheKey: "k2", client: ok}, - {agentID: "other-acp", cacheKey: "k3", client: failing}, - }, - } - - got := svc.List(context.Background(), false) - if len(got) != 2 { - t.Fatalf("List = %+v, want 2 entries", got) - } - if got[0].AgentID != "codex-acp" || got[0].Usage != okUsage || got[0].Error != "" { - t.Errorf("entry[0] = %+v", got[0]) - } - if got[1].AgentID != "other-acp" || got[1].Usage != nil || got[1].Error != hostUsageFetchError { - t.Errorf("entry[1] = %+v", got[1]) - } - if noCreds.calls != 0 { - t.Errorf("client without creds was fetched %d times", noCreds.calls) - } - - // Second List hits the cache for the successful entry. - _ = svc.List(context.Background(), false) - if ok.calls != 1 { - t.Errorf("expected cached fetch, got %d calls", ok.calls) - } -} - -func TestHostServiceList_FreshBypassesStaleCache(t *testing.T) { - now := time.Now() - okUsage := &ProviderUsage{Provider: "anthropic", FetchedAt: now} - ok := &fakeHostClient{hasCreds: true, usage: okUsage} - svc := &HostService{ - cache: NewUsageCache(), - entries: []hostEntry{{agentID: "claude-acp", cacheKey: "k1", client: ok}}, - } - - _ = svc.List(context.Background(), false) - if ok.calls != 1 { - t.Fatalf("initial fetch calls = %d", ok.calls) - } - - // Entry is younger than freshMaxAge: fresh serves the cached value. - _ = svc.List(context.Background(), true) - if ok.calls != 1 { - t.Errorf("fresh within clamp should hit cache, calls = %d", ok.calls) - } - - // Age the entry past freshMaxAge but below the 5-minute TTL: a normal List - // still serves the cache, a fresh List re-queries the provider. - svc.cache.mu.Lock() - svc.cache.entries["k1"].successAt = now.Add(-freshMaxAge - time.Second) - svc.cache.mu.Unlock() - - _ = svc.List(context.Background(), false) - if ok.calls != 1 { - t.Errorf("stale-but-within-TTL cached List should not refetch, calls = %d", ok.calls) - } - _ = svc.List(context.Background(), true) - if ok.calls != 2 { - t.Errorf("fresh List should refetch, calls = %d", ok.calls) - } -} - -func TestNewHostServiceRegistersHostAgents(t *testing.T) { - svc := NewHostService(nil) - if len(svc.entries) != 2 { - t.Fatalf("entries = %d, want 2", len(svc.entries)) - } - if svc.entries[0].agentID != "claude-acp" || svc.entries[1].agentID != "codex-acp" { - t.Errorf("agent IDs = %q, %q", svc.entries[0].agentID, svc.entries[1].agentID) - } -} - -// runCoalescedFetch launches n concurrent GetOrFetchWithin callers whose first -// fetch blocks until every goroutine has been started, maximizing contention -// without sleep-based synchronization. It returns the per-caller results. -func runCoalescedFetch( - t *testing.T, - cache *UsageCache, - n int, - result *ProviderUsage, - fetchErr error, - calls *atomic.Int32, -) { - t.Helper() - release := make(chan struct{}) - fetch := func(_ context.Context) (*ProviderUsage, error) { - calls.Add(1) - <-release - return result, fetchErr - } - - var started, wg sync.WaitGroup - for range n { - started.Add(1) - wg.Add(1) - go func() { - defer wg.Done() - started.Done() - _, err := cache.GetOrFetchWithin(context.Background(), "k", freshMaxAge, fetch) - if fetchErr == nil && err != nil { - t.Errorf("GetOrFetchWithin: %v", err) - } - if fetchErr != nil && err == nil { - t.Error("expected error from coalesced failing fetch") - } - }() - } - started.Wait() - close(release) - wg.Wait() -} - -func TestUsageCacheCoalescesConcurrentFetches(t *testing.T) { - cache := NewUsageCache() - var calls atomic.Int32 - - runCoalescedFetch(t, cache, 8, &ProviderUsage{Provider: "anthropic", FetchedAt: time.Now()}, nil, &calls) - - if got := calls.Load(); got != 1 { - t.Errorf("concurrent misses fetched %d times, want 1", got) - } -} - -func TestUsageCacheCoalescesFailures(t *testing.T) { - cache := NewUsageCache() - var calls atomic.Int32 - - runCoalescedFetch(t, cache, 8, nil, errors.New("boom"), &calls) - - if got := calls.Load(); got != 1 { - t.Errorf("concurrent failing fetches called provider %d times, want 1", got) - } - fetch := func(_ context.Context) (*ProviderUsage, error) { - calls.Add(1) - return nil, errors.New("boom") - } - - // Once the negative entry ages past failureCacheTTL, the provider is retried. - cache.mu.Lock() - cache.entries["k"].errAt = time.Now().Add(-failureCacheTTL - time.Second) - cache.mu.Unlock() - if _, err := cache.GetOrFetchWithin(context.Background(), "k", freshMaxAge, fetch); err == nil { - t.Error("expected error on retry after TTL") - } - if got := calls.Load(); got != 2 { - t.Errorf("expired failure entry should refetch, calls = %d, want 2", got) - } -} - -func TestUsageCacheFailureKeepsStaleSuccess(t *testing.T) { - cache := NewUsageCache() - okUsage := &ProviderUsage{Provider: "anthropic", FetchedAt: time.Now()} - okFetch := func(_ context.Context) (*ProviderUsage, error) { return okUsage, nil } - failFetch := func(_ context.Context) (*ProviderUsage, error) { return nil, errors.New("boom") } - - if _, err := cache.GetOrFetchWithin(context.Background(), "k", freshMaxAge, okFetch); err != nil { - t.Fatalf("seed fetch: %v", err) - } - // Age the success past the fresh clamp but within the regular TTL. - cache.mu.Lock() - cache.entries["k"].successAt = time.Now().Add(-freshMaxAge - time.Second) - cache.mu.Unlock() - - // A failed fresh refresh returns the error to the fresh caller... - if _, err := cache.GetOrFetchWithin(context.Background(), "k", freshMaxAge, failFetch); err == nil { - t.Fatal("expected error from failed fresh fetch") - } - // ...but must not evict the stale-but-valid success for non-fresh callers. - got, err := cache.GetOrFetchWithin(context.Background(), "k", cacheTTL, failFetch) - if err != nil || got != okUsage { - t.Errorf("stale success was evicted: usage=%v err=%v", got, err) - } -} - -func TestUsageCacheDoesNotCacheCancellation(t *testing.T) { - cache := NewUsageCache() - var calls atomic.Int32 - fetch := func(ctx context.Context) (*ProviderUsage, error) { - calls.Add(1) - return nil, ctx.Err() - } - - cancelled, cancel := context.WithCancel(context.Background()) - cancel() - if _, err := cache.GetOrFetchWithin(cancelled, "k", freshMaxAge, fetch); err == nil { - t.Fatal("expected cancellation error") - } - - // The cancellation must not have been recorded as a provider failure. - okUsage := &ProviderUsage{Provider: "anthropic", FetchedAt: time.Now()} - got, err := cache.GetOrFetchWithin(context.Background(), "k", freshMaxAge, - func(_ context.Context) (*ProviderUsage, error) { return okUsage, nil }) - if err != nil || got != okUsage { - t.Errorf("cancelled fetch poisoned the cache: usage=%v err=%v", got, err) - } - if calls.Load() != 1 { - t.Errorf("fetch calls = %d, want 1", calls.Load()) - } -} diff --git a/apps/backend/internal/backendapp/services.go b/apps/backend/internal/backendapp/services.go index 9059702326..05cb19244b 100644 --- a/apps/backend/internal/backendapp/services.go +++ b/apps/backend/internal/backendapp/services.go @@ -13,7 +13,6 @@ import ( "github.com/kandev/kandev/internal/agent/hostutility" "github.com/kandev/kandev/internal/agent/registry" agentsettingscontroller "github.com/kandev/kandev/internal/agent/settings/controller" - agentusage "github.com/kandev/kandev/internal/agent/usage" agentctlutil "github.com/kandev/kandev/internal/agentctl/server/utility" analyticsservice "github.com/kandev/kandev/internal/analytics/service" "github.com/kandev/kandev/internal/automation" @@ -53,7 +52,6 @@ func provideServices(cfg *config.Config, log *logger.Logger, repos *Repositories return nil, nil, err } agentSettingsController := agentsettingscontroller.NewController(repos.AgentSettings, discoveryRegistry, agentRegistry, repos.Task, log) - agentSettingsController.SetHostUsageLister(agentusage.NewHostService(log)) userSvc := userservice.NewService(repos.User, eventBus, log) editorSvc := editorservice.NewService(repos.Editor, repos.Task, userSvc) diff --git a/apps/web/app/office/agents/[id]/components/agent-overview-tab.tsx b/apps/web/app/office/agents/[id]/components/agent-overview-tab.tsx index b0e57880c8..c28e755381 100644 --- a/apps/web/app/office/agents/[id]/components/agent-overview-tab.tsx +++ b/apps/web/app/office/agents/[id]/components/agent-overview-tab.tsx @@ -11,7 +11,7 @@ import { toast } from "sonner"; import { useAppStore } from "@/components/state-provider"; import { updateAgentProfile, getAgentUtilization } from "@/lib/api/domains/office-api"; import type { AgentProfile, AgentRole, ProviderUsage } from "@/lib/state/slices/office/types"; -import { UtilizationBars } from "@/components/usage/utilization-bars"; +import { UtilizationBars } from "@/app/office/components/utilization-bars"; type AgentOverviewTabProps = { agent: AgentProfile; diff --git a/apps/web/components/usage/utilization-bars.tsx b/apps/web/app/office/components/utilization-bars.tsx similarity index 89% rename from apps/web/components/usage/utilization-bars.tsx rename to apps/web/app/office/components/utilization-bars.tsx index a48b3c6a4c..7a814ae85f 100644 --- a/apps/web/components/usage/utilization-bars.tsx +++ b/apps/web/app/office/components/utilization-bars.tsx @@ -1,22 +1,22 @@ "use client"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; -import type { ProviderUsage } from "@/lib/types/agent-profile"; +import type { ProviderUsage } from "@/lib/state/slices/office/types"; type Props = { usage: ProviderUsage; }; -export function getBarColor(pct: number): string { +function getBarColor(pct: number): string { if (pct >= 90) return "bg-red-500"; if (pct >= 80) return "bg-amber-500"; - return "bg-emerald-500"; + return "bg-blue-500"; } -export function getTextColor(pct: number): string { +function getTextColor(pct: number): string { if (pct >= 90) return "text-red-600 dark:text-red-400"; if (pct >= 80) return "text-amber-600 dark:text-amber-400"; - return "text-emerald-600 dark:text-emerald-400"; + return "text-muted-foreground"; } function formatResetTime(resetAt: string): string { diff --git a/apps/web/app/office/page-client.tsx b/apps/web/app/office/page-client.tsx index 8faa79114a..e7f1b4b617 100644 --- a/apps/web/app/office/page-client.tsx +++ b/apps/web/app/office/page-client.tsx @@ -23,7 +23,7 @@ import { AgentCardsPanel } from "./components/agent-cards-panel"; import { ProviderHealthCard } from "./components/routing/provider-health-card"; import { timeAgo } from "@/lib/utils/time"; -import { UtilizationBars } from "@/components/usage/utilization-bars"; +import { UtilizationBars } from "./components/utilization-bars"; import { formatDollars } from "@/lib/utils"; // formatMonthSpend renders the subcents value from /office dashboard diff --git a/apps/web/app/settings/agents/page.tsx b/apps/web/app/settings/agents/page.tsx index 7e5462e596..1b4669703e 100644 --- a/apps/web/app/settings/agents/page.tsx +++ b/apps/web/app/settings/agents/page.tsx @@ -31,7 +31,6 @@ import { useAgentDiscovery } from "@/hooks/domains/settings/use-agent-discovery" import { useAvailableAgents } from "@/hooks/domains/settings/use-available-agents"; import { AgentLogo } from "@/components/agent-logo"; import { AddTUIAgentDialog } from "@/components/settings/add-tui-agent-dialog"; -import { AgentUsageSection } from "@/components/settings/agent-usage-section"; import { HostShellDialog } from "@/components/settings/host-shell-dialog"; import { InstallAgentCard } from "@/components/settings/install-agent-card"; import { InstalledAgentCard } from "@/components/settings/installed-agent-card"; @@ -563,8 +562,6 @@ export default function AgentsSettingsPage() {

- - - vi.fn(), -); - -vi.mock("@/lib/api", () => ({ - listAgentSubscriptionUsage, -})); - -vi.mock("@/components/agent-logo", () => ({ - AgentLogo: ({ agentName, className }: { agentName: string; className?: string }) => ( - - ), -})); - -afterEach(() => { - cleanup(); - listAgentSubscriptionUsage.mockReset(); -}); - -describe("AgentUsageSection", () => { - it("renders nothing when no subscription agents are present", async () => { - listAgentSubscriptionUsage.mockResolvedValue({ agents: [] }); - - const { container } = render(); - - await waitFor(() => expect(listAgentSubscriptionUsage).toHaveBeenCalled()); - expect(container.querySelector('[data-testid="agent-usage-section"]')).toBeNull(); - }); - - it("renders utilization windows, plan badge, and per-agent errors", async () => { - listAgentSubscriptionUsage.mockResolvedValue({ - agents: [ - { - agent_id: "claude-acp", - display_name: "Claude Code", - usage: { - provider: "anthropic", - plan: "max", - windows: [ - { - label: "5-hour", - utilization_pct: 62, - reset_at: new Date(Date.now() + 3600_000).toISOString(), - }, - { - label: "7-day", - utilization_pct: 15, - reset_at: new Date(Date.now() + 86400_000).toISOString(), - }, - ], - fetched_at: new Date().toISOString(), - }, - }, - { - agent_id: "codex-acp", - display_name: "Codex", - error: "codex usage: unexpected status 500", - }, - ], - }); - - render( - - - , - ); - - await screen.findByTestId("agent-usage-section"); - expect(screen.getByText("Claude Code")).toBeDefined(); - expect(screen.getByText("max")).toBeDefined(); - expect(screen.getByText("Good")).toBeDefined(); // worst window 62% → Good - expect(screen.getByText("5h")).toBeDefined(); - expect(screen.getByText("62%")).toBeDefined(); - expect(screen.getByText("7d")).toBeDefined(); - expect(screen.getByText("Codex")).toBeDefined(); - expect(screen.getByTestId("logo-codex-acp").className).toContain("!opacity-100"); - expect(screen.getByTestId("logo-codex-acp").className).toContain("brightness-100"); - expect(screen.getByText("Could not fetch usage data.")).toBeDefined(); - }); -}); diff --git a/apps/web/components/settings/agent-usage-section.tsx b/apps/web/components/settings/agent-usage-section.tsx deleted file mode 100644 index 738b9f129a..0000000000 --- a/apps/web/components/settings/agent-usage-section.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; - -import { IconAlertTriangle, IconLoader2, IconRefresh } from "@tabler/icons-react"; -import { Badge } from "@kandev/ui/badge"; -import { Button } from "@kandev/ui/button"; -import { Card, CardContent } from "@kandev/ui/card"; -import { Separator } from "@kandev/ui/separator"; -import { cn } from "@/lib/utils"; -import { AgentLogo } from "@/components/agent-logo"; -import { UsageWindowRows, usageStatus } from "@/components/usage/usage-window-rows"; -import { useAgentSubscriptionUsage } from "@/hooks/domains/settings/use-agent-subscription-usage"; -import type { AgentSubscriptionUsage } from "@/lib/types/http"; - -function AgentUsageCard({ item }: { item: AgentSubscriptionUsage }) { - const usage = item.usage; - const status = usage && usage.windows.length > 0 ? usageStatus(usage) : null; - return ( - - -
- -

{item.display_name}

- {usage?.plan && ( - - {usage.plan} - - )} - {status && ( - - {status.label} - - )} -
- {item.error ? ( -

- - Could not fetch usage data. -

- ) : ( - usage && - )} -
-
- ); -} - -/** - * "Subscription Usage" section on Settings > Agents. Shows rate-limit window - * utilization for host agents authenticated with a subscription (OAuth) — - * Claude Code and Codex. Hidden when no subscription-billed agent is present. - */ -export function AgentUsageSection() { - const { items, loading, refresh } = useAgentSubscriptionUsage(); - if (items.length === 0) return null; - - return ( -
- -
-
-

Subscription Usage

-

- Rate-limit utilization for agents signed in with a subscription plan. -

-
- -
- -
- {items.map((item) => ( - - ))} -
-
- ); -} diff --git a/apps/web/components/task/chat/token-usage-display.test.tsx b/apps/web/components/task/chat/token-usage-display.test.tsx index 1a46376bfb..5115d881b8 100644 --- a/apps/web/components/task/chat/token-usage-display.test.tsx +++ b/apps/web/components/task/chat/token-usage-display.test.tsx @@ -1,17 +1,12 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render } from "@testing-library/react"; import { useSessionContextWindow } from "@/hooks/domains/session/use-session-context-window"; -import { useSessionAgentUsage } from "@/hooks/domains/session/use-session-agent-usage"; import { isContextWindowReliable, TokenUsageDisplay } from "./token-usage-display"; vi.mock("@/hooks/domains/session/use-session-context-window", () => ({ useSessionContextWindow: vi.fn(), })); -vi.mock("@/hooks/domains/session/use-session-agent-usage", () => ({ - useSessionAgentUsage: vi.fn(() => null), -})); - vi.mock("@kandev/ui/tooltip", () => ({ TooltipProvider: ({ children }: { children: React.ReactNode }) => <>{children}, Tooltip: ({ children, open }: { children: React.ReactNode; open?: boolean }) => ( @@ -94,74 +89,6 @@ describe("TokenUsageDisplay", () => { expect(getByTestId("tooltip-root").getAttribute("data-open")).toBe("false"); }); - - it("shows subscription usage rows in the tooltip when the agent has them", () => { - vi.mocked(useSessionContextWindow).mockReturnValue({ - size: 200_000, - used: 56_047, - remaining: 143_953, - efficiency: 28, - }); - vi.mocked(useSessionAgentUsage).mockReturnValue({ - agent_id: "claude-acp", - display_name: "Claude", - usage: { - provider: "anthropic", - plan: "max", - windows: [ - { - label: "5-hour", - utilization_pct: 86, - reset_at: new Date(Date.now() + 3 * 3_600_000).toISOString(), - }, - { - label: "7-day", - utilization_pct: 19, - reset_at: new Date(Date.now() + 30 * 3_600_000).toISOString(), - }, - ], - fetched_at: new Date().toISOString(), - }, - }); - - const { container, getByRole, getByTestId, getByText } = render( - , - ); - - expect(container.querySelector('[data-testid="doughnut-subscription-usage"]')).not.toBeNull(); - expect(getByRole("button", { name: "Context window: 28% used" })).toBeDefined(); - expect(getByText("Context window")).toBeDefined(); - expect(getByText("56.0K of 200.0K tokens")).toBeDefined(); - expect(getByText(/Subscription · max/i)).toBeDefined(); - expect( - getByTestId("context-window-usage").compareDocumentPosition( - getByTestId("doughnut-subscription-usage"), - ) & Node.DOCUMENT_POSITION_FOLLOWING, - ).toBeTruthy(); - // Worst window is 86% → provider status "High". - expect(getByText("High")).toBeDefined(); - expect(getByText("5h")).toBeDefined(); - expect(getByText("86%")).toBeDefined(); - expect(getByText("7d")).toBeDefined(); - // Reset countdown column (≈3h out) and colored bars per window. - expect(getByText(/^(3h 0m|2h 59m)$/)).toBeDefined(); - expect(container.querySelector(".bg-amber-500")).not.toBeNull(); // 86% bar - expect(container.querySelector(".bg-emerald-500")).not.toBeNull(); // 19% bar - }); - - it("omits the subscription block when the agent has no usage", () => { - vi.mocked(useSessionContextWindow).mockReturnValue({ - size: 200_000, - used: 56_047, - remaining: 143_953, - efficiency: 28, - }); - vi.mocked(useSessionAgentUsage).mockReturnValue(null); - - const { container } = render(); - - expect(container.querySelector('[data-testid="doughnut-subscription-usage"]')).toBeNull(); - }); }); describe("TokenUsageDisplay context source", () => { diff --git a/apps/web/components/task/chat/token-usage-display.tsx b/apps/web/components/task/chat/token-usage-display.tsx index 68af7a2b33..f81646c5ca 100644 --- a/apps/web/components/task/chat/token-usage-display.tsx +++ b/apps/web/components/task/chat/token-usage-display.tsx @@ -4,9 +4,7 @@ import { memo, useEffect, useId, useRef, useState } from "react"; import { IconInfoCircle } from "@tabler/icons-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kandev/ui/tooltip"; import { cn } from "@/lib/utils"; -import { UsageWindowRows, usageStatus } from "@/components/usage/usage-window-rows"; import { useSessionContextWindow } from "@/hooks/domains/session/use-session-context-window"; -import { useSessionAgentUsage } from "@/hooks/domains/session/use-session-agent-usage"; type TokenUsageDisplayProps = { sessionId: string | null; @@ -153,33 +151,6 @@ function ContextWindowSource({ source }: { source: "acp" | "api" | undefined }) ); } -/** - * Subscription utilization rows for the session's agent, rendered inside the - * doughnut tooltip. Mounted only while the tooltip is open, so each hover - * triggers a fresh provider fetch (server-clamped to one per 15 s). - */ -function SessionUsageRows({ sessionId }: { sessionId: string | null }) { - const agentUsage = useSessionAgentUsage(sessionId); - const usage = agentUsage?.usage; - if (!usage || usage.windows.length === 0) return null; - const status = usageStatus(usage); - - return ( -
-
- - Subscription{usage.plan ? ` · ${usage.plan}` : ""} - - {status.label} -
- -
- ); -} - export const TokenUsageDisplay = memo(function TokenUsageDisplay({ sessionId, className, @@ -248,7 +219,6 @@ export const TokenUsageDisplay = memo(function TokenUsageDisplay({ - diff --git a/apps/web/components/usage/usage-window-rows.test.tsx b/apps/web/components/usage/usage-window-rows.test.tsx deleted file mode 100644 index 4764b59a7a..0000000000 --- a/apps/web/components/usage/usage-window-rows.test.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { cleanup, render } from "@testing-library/react"; -import { - UsageWindowRows, - formatResetShort, - shortWindowLabel, - usageStatus, -} from "./usage-window-rows"; -import type { ProviderUsage } from "@/lib/types/agent-profile"; - -afterEach(cleanup); - -describe("shortWindowLabel", () => { - it("shortens hour and day windows", () => { - expect(shortWindowLabel("5-hour")).toBe("5h"); - expect(shortWindowLabel("7-day")).toBe("7d"); - expect(shortWindowLabel("30-day")).toBe("30d"); - expect(shortWindowLabel("7-day (Opus)")).toBe("7d (Opus)"); - }); - - it("leaves unknown labels untouched", () => { - expect(shortWindowLabel("primary")).toBe("primary"); - }); -}); - -describe("formatResetShort", () => { - const now = Date.UTC(2026, 6, 14, 12, 0, 0); - - it("formats minutes", () => { - expect(formatResetShort(new Date(now + 42 * 60_000).toISOString(), now)).toBe("42m"); - }); - - it("formats hours and minutes", () => { - expect(formatResetShort(new Date(now + 3 * 3_600_000 + 17 * 60_000).toISOString(), now)).toBe( - "3h 17m", - ); - }); - - it("formats days and hours", () => { - expect(formatResetShort(new Date(now + 29 * 3_600_000).toISOString(), now)).toBe("1d 5h"); - }); - - it("handles past reset times", () => { - expect(formatResetShort(new Date(now - 60_000).toISOString(), now)).toBe("soon"); - }); - - it("handles invalid timestamps", () => { - expect(formatResetShort("not-a-date", now)).toBe("soon"); - }); -}); - -function makeUsage(pcts: number[]): ProviderUsage { - return { - provider: "anthropic", - plan: "max", - windows: pcts.map((pct, i) => ({ - label: `${i + 1}-day`, - utilization_pct: pct, - reset_at: new Date(Date.now() + 3_600_000).toISOString(), - })), - fetched_at: new Date().toISOString(), - }; -} - -describe("usageStatus", () => { - it("is Good below 80%", () => { - expect(usageStatus(makeUsage([10, 79])).label).toBe("Good"); - }); - - it("is High at 80–89%", () => { - expect(usageStatus(makeUsage([10, 85])).label).toBe("High"); - }); - - it("is Critical at 90%+", () => { - expect(usageStatus(makeUsage([10, 94])).label).toBe("Critical"); - }); -}); - -describe("UsageWindowRows", () => { - it("renders a colored bar, percentage, and reset time per window", () => { - const usage: ProviderUsage = { - provider: "anthropic", - plan: "max", - windows: [ - { - label: "5-hour", - utilization_pct: 94, - reset_at: new Date(Date.now() + 2 * 3_600_000).toISOString(), - }, - { - label: "7-day", - utilization_pct: 39, - // 30s under 5 days so the elapsed render time cannot flip the - // formatted value between "5d 0h" and "4d 23h". - reset_at: new Date(Date.now() + 5 * 24 * 3_600_000 - 30_000).toISOString(), - }, - ], - fetched_at: new Date().toISOString(), - }; - - const { container, getByText } = render(); - - expect(getByText("5h")).toBeDefined(); - expect(getByText("94%")).toBeDefined(); - expect(getByText("7d")).toBeDefined(); - expect(getByText("39%")).toBeDefined(); - expect(getByText("4d 23h")).toBeDefined(); - expect(container.querySelector(".bg-red-500")).not.toBeNull(); - expect(container.querySelector(".bg-emerald-500")).not.toBeNull(); - }); -}); diff --git a/apps/web/components/usage/usage-window-rows.tsx b/apps/web/components/usage/usage-window-rows.tsx deleted file mode 100644 index a1513df93a..0000000000 --- a/apps/web/components/usage/usage-window-rows.tsx +++ /dev/null @@ -1,86 +0,0 @@ -"use client"; - -import { Fragment } from "react"; -import { cn } from "@/lib/utils"; -import type { ProviderUsage, UtilizationWindow } from "@/lib/types/agent-profile"; -import { getBarColor, getTextColor } from "./utilization-bars"; - -/** "5-hour" → "5h", "7-day (Opus)" → "7d (Opus)". */ -export function shortWindowLabel(label: string): string { - return label.replace(/(\d+)-hour/i, "$1h").replace(/(\d+)-day/i, "$1d"); -} - -/** Compact time-until-reset: "3h 3m", "5d 12h", "42m", "soon". */ -export function formatResetShort(resetAt: string, now = Date.now()): string { - const diffMs = new Date(resetAt).getTime() - now; - if (!Number.isFinite(diffMs) || diffMs <= 0) return "soon"; - const totalHours = Math.floor(diffMs / 3_600_000); - const minutes = Math.floor((diffMs % 3_600_000) / 60_000); - if (totalHours >= 24) return `${Math.floor(totalHours / 24)}d ${totalHours % 24}h`; - if (totalHours > 0) return `${totalHours}h ${minutes}m`; - return `${minutes}m`; -} - -export type UsageStatus = { - label: "Good" | "High" | "Critical"; - className: string; -}; - -/** Worst-window health for a provider, rendered like "Good" / "Critical". */ -export function usageStatus(usage: ProviderUsage): UsageStatus { - const worst = Math.max(0, ...usage.windows.map((w) => w.utilization_pct)); - if (worst >= 90) return { label: "Critical", className: "text-red-600 dark:text-red-400" }; - if (worst >= 80) return { label: "High", className: "text-amber-600 dark:text-amber-400" }; - return { label: "Good", className: "text-emerald-600 dark:text-emerald-400" }; -} - -function UsageWindowCells({ window: w }: { window: UtilizationWindow }) { - const pct = Math.min(100, Math.max(0, w.utilization_pct)); - return ( - <> - {shortWindowLabel(w.label)} -
-
-
- - {Math.round(pct)}% - - - {formatResetShort(w.reset_at)} - - - ); -} - -/** - * Compact per-window utilization rows: `5h [==== ] 39% 3h 3m`. - * Shared by the settings usage cards and the chat doughnut tooltip. - */ -export function UsageWindowRows({ - usage, - className, -}: { - usage: ProviderUsage; - className?: string; -}) { - return ( -
- {usage.windows.map((w) => ( - - - - ))} -
- ); -} diff --git a/apps/web/hooks/domains/session/use-session-agent-usage.ts b/apps/web/hooks/domains/session/use-session-agent-usage.ts deleted file mode 100644 index 5709d0ece3..0000000000 --- a/apps/web/hooks/domains/session/use-session-agent-usage.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { useEffect, useState } from "react"; -import { useAppStore } from "@/components/state-provider"; -import { listAgentSubscriptionUsage } from "@/lib/api"; -import type { AgentSubscriptionUsage } from "@/lib/types/http"; - -// Module-level memo: the last usage listing, served instantly on the next -// tooltip open while a fresh fetch is in flight. One in-flight request is -// shared between concurrent consumers. -let lastAgents: AgentSubscriptionUsage[] | null = null; -let inflight: Promise | null = null; - -function fetchFreshAgentUsage(): Promise { - if (!inflight) { - inflight = listAgentSubscriptionUsage({ cache: "no-store", fresh: true }) - .then((response) => { - lastAgents = response.agents ?? []; - return lastAgents; - }) - .finally(() => { - inflight = null; - }); - } - return inflight; -} - -/** Test-only: clear the module-level memo between tests. */ -export function resetSessionAgentUsageCacheForTest() { - lastAgents = null; - inflight = null; -} - -/** Resolves the session's agent name (e.g. "claude-acp") from the store. */ -export function useSessionAgentName(sessionId: string | null): string | null { - const profileId = useAppStore((state) => - sessionId ? state.taskSessions.items[sessionId]?.agent_profile_id : undefined, - ); - const agentName = useAppStore((state) => - profileId - ? state.agentProfiles.items.find((profile) => profile.id === profileId)?.agent_name - : undefined, - ); - return agentName ?? null; -} - -/** - * Live subscription usage for the session's agent. Fetches fresh provider - * data on mount — mount the consuming component lazily (e.g. inside a tooltip - * content) so hovering triggers the fetch. The previous listing renders - * immediately while the fresh one is in flight. Returns null while the agent - * is unknown or has no subscription usage. - */ -export function useSessionAgentUsage(sessionId: string | null): AgentSubscriptionUsage | null { - const agentName = useSessionAgentName(sessionId); - const [agents, setAgents] = useState(lastAgents ?? []); - - useEffect(() => { - if (!agentName) return; - let active = true; - fetchFreshAgentUsage() - .then((list) => { - if (active) setAgents(list); - }) - .catch(() => { - // Don't keep presenting stale usage as live: drop the memo so the - // next open refetches, and hide the rows for this open. - lastAgents = null; - if (active) setAgents([]); - }); - return () => { - active = false; - }; - }, [agentName]); - - if (!agentName) return null; - return agents.find((agent) => agent.agent_id === agentName) ?? null; -} diff --git a/apps/web/hooks/domains/settings/use-agent-subscription-usage.ts b/apps/web/hooks/domains/settings/use-agent-subscription-usage.ts deleted file mode 100644 index 4a3a97897b..0000000000 --- a/apps/web/hooks/domains/settings/use-agent-subscription-usage.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { listAgentSubscriptionUsage } from "@/lib/api"; -import type { AgentSubscriptionUsage } from "@/lib/types/http"; - -/** - * Fetches subscription utilization for host-installed agents (Claude Code, - * Codex). The initial load accepts the backend's 5-minute cache; manual - * refresh() requests fresh provider data (server-clamped to 15 s). - */ -export function useAgentSubscriptionUsage() { - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(true); - const fetchingRef = useRef(false); - - const fetchUsage = useCallback(async (fresh: boolean) => { - if (fetchingRef.current) return; - fetchingRef.current = true; - setLoading(true); - try { - const response = await listAgentSubscriptionUsage({ cache: "no-store", fresh }); - setItems(response.agents ?? []); - } catch { - // Keep the previous items: clearing them would hide the whole section - // (including the Refresh button) on a transient refresh failure. - } finally { - fetchingRef.current = false; - setLoading(false); - } - }, []); - - const refresh = useCallback(() => fetchUsage(true), [fetchUsage]); - - useEffect(() => { - void fetchUsage(false); - }, [fetchUsage]); - - return { items, loading, refresh }; -} diff --git a/apps/web/lib/api/domains/settings-api.ts b/apps/web/lib/api/domains/settings-api.ts index 582f01c65c..f1524cabed 100644 --- a/apps/web/lib/api/domains/settings-api.ts +++ b/apps/web/lib/api/domains/settings-api.ts @@ -2,7 +2,6 @@ import { fetchJson, fetchJsonWithRetry, type ApiRequestOptions } from "../client import { getBackendConfig } from "@/lib/config"; import type { Agent, - AgentSubscriptionUsageResponse, ListAgentsResponse, ListAgentDiscoveryResponse, ListAvailableAgentsResponse, @@ -216,14 +215,6 @@ export async function listAvailableAgents( return fetchJson("/api/v1/agents/available", options); } -export async function listAgentSubscriptionUsage( - options?: ApiRequestOptions & { fresh?: boolean }, -): Promise { - const { fresh, ...rest } = options ?? {}; - const path = fresh ? "/api/v1/agents/usage?fresh=true" : "/api/v1/agents/usage"; - return fetchJson(path, rest); -} - export async function getAgentProfileMcpConfig( profileId: string, options?: ApiRequestOptions, diff --git a/apps/web/lib/types/agent-profile.ts b/apps/web/lib/types/agent-profile.ts index a25f3ceb59..92d6ef63bf 100644 --- a/apps/web/lib/types/agent-profile.ts +++ b/apps/web/lib/types/agent-profile.ts @@ -36,22 +36,10 @@ export type UtilizationWindow = { export type ProviderUsage = { provider: string; - plan?: string; windows: UtilizationWindow[]; fetched_at: string; }; -export type AgentSubscriptionUsage = { - agent_id: string; - display_name: string; - usage?: ProviderUsage | null; - error?: string; -}; - -export type AgentSubscriptionUsageResponse = { - agents: AgentSubscriptionUsage[]; -}; - export type AgentRole = | "ceo" | "worker" diff --git a/apps/web/lib/types/http-agents.ts b/apps/web/lib/types/http-agents.ts index 2515c26568..36853f8a35 100644 --- a/apps/web/lib/types/http-agents.ts +++ b/apps/web/lib/types/http-agents.ts @@ -10,8 +10,6 @@ export type { AgentProfilePayload, AgentRole, AgentStatus, - AgentSubscriptionUsage, - AgentSubscriptionUsageResponse, BillingType, CLIFlag, UtilizationWindow,