Resolve stash merge conflicts: keep both inline-task and billing features

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
tusharmagar 2026-03-13 10:44:48 +05:30
parent 63a5af8f9a
commit 4f07f626b3
7 changed files with 118 additions and 1 deletions

View file

@ -40,6 +40,7 @@ import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedu
import { search } from '@x/core/dist/search/search.js';
import { versionHistory } from '@x/core';
import { classifySchedule } from '@x/core/dist/knowledge/inline_tasks.js';
import { getBillingInfo } from '@x/core/dist/billing/billing.js';
type InvokeChannels = ipc.InvokeChannels;
type IPCChannels = ipc.IPCChannels;
@ -571,5 +572,9 @@ export function setupIpcHandlers() {
const schedule = await classifySchedule(args.instruction);
return { schedule };
},
// Billing handler
'billing:getInfo': async () => {
return await getBillingInfo();
},
});
}

View file

@ -87,6 +87,7 @@ import { ConnectorsPopover } from "@/components/connectors-popover"
import { HelpPopover } from "@/components/help-popover"
import { SettingsDialog } from "@/components/settings-dialog"
import { toast } from "@/lib/toast"
import { useBilling } from "@/hooks/useBilling"
import { ServiceEvent } from "@x/shared/src/service-events.js"
import z from "zod"
@ -401,6 +402,8 @@ export function SidebarContentPanel({
const [connectorsOpen, setConnectorsOpen] = useState(false)
const [openConnectorsAfterClose, setOpenConnectorsAfterClose] = useState(false)
const connectorsButtonRef = useRef<HTMLButtonElement | null>(null)
const [isRowboatConnected, setIsRowboatConnected] = useState(false)
const { billing } = useBilling(isRowboatConnected)
useEffect(() => {
let mounted = true
@ -412,6 +415,7 @@ export function SidebarContentPanel({
const hasError = Object.values(config).some((entry) => Boolean(entry?.error))
if (mounted) {
setHasOauthError(hasError)
setIsRowboatConnected(config['rowboat']?.connected ?? false)
if (!hasError) {
setShowOauthAlert(true)
}
@ -420,6 +424,7 @@ export function SidebarContentPanel({
console.error('Failed to fetch OAuth state:', error)
if (mounted) {
setHasOauthError(false)
setIsRowboatConnected(false)
setShowOauthAlert(true)
}
}
@ -483,6 +488,24 @@ export function SidebarContentPanel({
/>
)}
</SidebarContent>
{/* Billing status */}
{isRowboatConnected && billing && (
<div className="px-3 py-2">
<div className="flex items-center justify-between rounded-md border border-sidebar-border bg-sidebar-accent/30 px-2.5 py-1.5">
<div className="flex flex-col">
<span className="text-xs font-medium capitalize text-sidebar-foreground">
{billing.subscriptionPlan ?? 'Free'}
</span>
<span className="text-[10px] text-sidebar-foreground/50">
8 days left
</span>
</div>
<button className="rounded-md bg-sidebar-accent px-2.5 py-1 text-[10px] font-medium text-sidebar-foreground hover:bg-sidebar-accent/80 transition-colors">
Upgrade
</button>
</div>
</div>
)}
{/* Bottom actions */}
<div className="border-t border-sidebar-border px-2 py-2">
<div className="flex flex-col gap-1">

View file

@ -0,0 +1,37 @@
import { useState, useEffect, useCallback } from 'react'
interface BillingInfo {
subscriptionPlan: string
subscriptionStatus: string
trialUsed: boolean
sanctionedCredits: number
availableCredits: number
}
export function useBilling(isRowboatConnected: boolean) {
const [billing, setBilling] = useState<BillingInfo | null>(null)
const [isLoading, setIsLoading] = useState(false)
const fetchBilling = useCallback(async () => {
if (!isRowboatConnected) {
setBilling(null)
return
}
try {
setIsLoading(true)
const result = await window.ipc.invoke('billing:getInfo', null)
setBilling(result)
} catch (error) {
console.error('Failed to fetch billing info:', error)
setBilling(null)
} finally {
setIsLoading(false)
}
}, [isRowboatConnected])
useEffect(() => {
fetchBilling()
}, [fetchBilling])
return { billing, isLoading, refresh: fetchBilling }
}

View file

@ -0,0 +1,38 @@
import { getAccessToken } from '../models/gateway.js';
import { ROWBOAT_BILLING_BASE_URL } from '../config/env.js';
export interface BillingInfo {
subscriptionPlan: string | null;
subscriptionStatus: string | null;
trialUsed: boolean;
sanctionedCredits: number;
availableCredits: number;
}
export async function getBillingInfo(): Promise<BillingInfo> {
const accessToken = await getAccessToken();
const response = await fetch(`${ROWBOAT_BILLING_BASE_URL}/me`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) {
throw new Error(`Billing API failed: ${response.status}`);
}
const body = await response.json() as {
customer: {
subscriptionPlan: string | null;
subscriptionStatus: string | null;
trialUsed: boolean;
};
usage: {
sanctionedCredits: number;
availableCredits: number;
};
};
return {
subscriptionPlan: body.customer.subscriptionPlan,
subscriptionStatus: body.customer.subscriptionStatus,
trialUsed: body.customer.trialUsed,
sanctionedCredits: body.usage.sanctionedCredits,
availableCredits: body.usage.availableCredits,
};
}

View file

@ -3,3 +3,6 @@ export const API_URL =
export const SUPABASE_PROJECT_URL =
process.env.SUPABASE_PROJECT_URL || 'http://127.0.0.1:54321';
export const ROWBOAT_BILLING_BASE_URL =
process.env.ROWBOAT_BILLING_BASE_URL || 'https://billing.staging.x.rowboatlabs.com';

View file

@ -7,7 +7,7 @@ import { getProviderConfig } from '../auth/providers.js';
import * as oauthClient from '../auth/oauth-client.js';
import { API_URL } from '../config/env.js';
async function getAccessToken(): Promise<string> {
export async function getAccessToken(): Promise<string> {
const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo');
const { tokens } = await oauthRepo.read('rowboat');
if (!tokens) {

View file

@ -473,6 +473,17 @@ const ipcSchemas = {
]).nullable(),
}),
},
// Billing channels
'billing:getInfo': {
req: z.null(),
res: z.object({
subscriptionPlan: z.string().nullable(),
subscriptionStatus: z.string().nullable(),
trialUsed: z.boolean(),
sanctionedCredits: z.number(),
availableCredits: z.number(),
}),
},
} as const;
// ============================================================================