Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ STRIPE_SUBSCRIPTION_500_PRICE_ID=price_dummy_subscription_500_id
# External Services
LINKUP_API_KEY=dummy_linkup_key
LOOPS_API_KEY=dummy_loops_key
ZEROCLICK_API_KEY=dummy_zeroclick_key

# Discord Integration
DISCORD_PUBLIC_KEY=dummy_discord_public_key
Expand Down
2 changes: 2 additions & 0 deletions agents/__tests__/base2.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from 'bun:test'

import {
FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
FREEBUFF_KIMI_MODEL_ID,
FREEBUFF_MINIMAX_MODEL_ID,
Expand All @@ -13,6 +14,7 @@ describe('base2 reviewer selection', () => {
[FREEBUFF_MINIMAX_MODEL_ID, 'code-reviewer-minimax'],
[FREEBUFF_KIMI_MODEL_ID, 'code-reviewer-kimi'],
[FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID, 'code-reviewer-deepseek'],
[FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID, 'code-reviewer-deepseek-flash'],
])('uses matching reviewer for model %p', (model, expectedReviewer) => {
const base2 = createBase2('free', { model })

Expand Down
13 changes: 13 additions & 0 deletions agents/base2/base2-free-deepseek-flash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID } from '@codebuff/common/constants/freebuff-models'

import { createBase2 } from './base2'

const definition = {
...createBase2('free', {
model: FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID,
}),
id: 'base2-free-deepseek-flash',
displayName: 'Buffy the DeepSeek Flash Free Orchestrator',
}

export default definition
1 change: 0 additions & 1 deletion agents/base2/base2-free-deepseek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { createBase2 } from './base2'

const definition = {
...createBase2('free', {
noAskUser: true,
model: FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID,
}),
id: 'base2-free-deepseek',
Expand Down
13 changes: 13 additions & 0 deletions agents/reviewer/code-reviewer-deepseek-flash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID } from '@codebuff/common/constants/freebuff-models'

import { publisher } from '../constants'
import type { SecretAgentDefinition } from '../types/secret-agent-definition'
import { createReviewer } from './code-reviewer'

const definition: SecretAgentDefinition = {
id: 'code-reviewer-deepseek-flash',
publisher,
...createReviewer(FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID),
}

export default definition
2 changes: 2 additions & 0 deletions agents/types/agent-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,8 @@ export type ModelName =
// DeepSeek
| 'deepseek/deepseek-v4-pro'
| 'deepseek-v4-pro'
| 'deepseek/deepseek-v4-flash'
| 'deepseek-v4-flash'
| 'deepseek/deepseek-chat-v3-0324'
| 'deepseek/deepseek-chat-v3-0324:nitro'
| 'deepseek/deepseek-r1-0528'
Expand Down
2 changes: 1 addition & 1 deletion cli/src/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export const Chat = ({
const { ads, recordImpression } = useGravityAd({
enabled: IS_FREEBUFF || !hasSubscription,
provider: 'gravity',
fallbackProvider: 'carbon',
fallbackProvider: 'zeroclick',
})

// Set initial mode from CLI flag on mount
Expand Down
11 changes: 7 additions & 4 deletions cli/src/components/blocks/agent-branch-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ToolBlockGroup } from './tool-block-group'
import { useTheme } from '../../hooks/use-theme'
import { useChatStore } from '../../state/chat-store'
import { isTextBlock } from '../../types/chat'
import { getAgentDisplayPrompt } from '../../utils/agent-display'
import { getAgentStatusInfo } from '../../utils/agent-helpers'
import {
processBlocks,
Expand Down Expand Up @@ -64,9 +65,10 @@ function getCollapsedPreview(
}
}

// Default preview: use initialPrompt or first line of text content
if (agentBlock.initialPrompt) {
return sanitizePreview(agentBlock.initialPrompt)
// Default preview: use the displayed prompt or first line of text content.
const displayPrompt = getAgentDisplayPrompt(agentBlock)
if (displayPrompt) {
return sanitizePreview(displayPrompt)
}

const textContent =
Expand Down Expand Up @@ -413,6 +415,7 @@ export const AgentBranchWrapper = memo(

// Compute collapsed preview text
const preview = getCollapsedPreview(agentBlock, isStreaming, isCollapsed)
const displayPrompt = getAgentDisplayPrompt(agentBlock)

const effectiveStatus = isStreaming ? 'running' : agentBlock.status
const {
Expand All @@ -429,7 +432,7 @@ export const AgentBranchWrapper = memo(
<box key={keyPrefix} style={{ flexDirection: 'column', gap: 0 }}>
<AgentBranchItem
name={agentBlock.agentName}
prompt={agentBlock.initialPrompt}
prompt={displayPrompt}
agentId={agentBlock.agentId}
isCollapsed={isCollapsed}
isStreaming={isStreaming}
Expand Down
4 changes: 2 additions & 2 deletions cli/src/components/choice-ad-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { AdResponse } from '../hooks/use-gravity-ad'

interface ChoiceAdBannerProps {
ads: AdResponse[]
onImpression?: (impUrl: string) => void
onImpression?: (ad: AdResponse) => void
}

export const CHOICE_AD_BANNER_HEIGHT = 5 // border-top + 2 lines description + spacer + cta row + border-bottom
Expand Down Expand Up @@ -82,7 +82,7 @@ export const ChoiceAdBanner: React.FC<ChoiceAdBannerProps> = ({ ads, onImpressio
useEffect(() => {
if (onImpression) {
for (const ad of visibleAds) {
onImpression(ad.impUrl)
onImpression(ad)
}
}
}, [visibleAds, onImpression])
Expand Down
4 changes: 2 additions & 2 deletions cli/src/components/waiting-room-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,12 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
// Always enable ads in the waiting room — this is where monetization lives.
// forceStart bypasses the "wait for first user message" gate inside the hook,
// which would otherwise block ads here since no conversation exists yet.
// Try Gravity first, then fall back to Carbon when Gravity doesn't fill.
// Try Gravity first, then fall back to ZeroClick when Gravity doesn't fill.
const { ads, recordImpression } = useGravityAd({
enabled: true,
forceStart: true,
provider: 'gravity',
fallbackProvider: 'carbon',
fallbackProvider: 'zeroclick',
surface: 'waiting_room',
})

Expand Down
123 changes: 88 additions & 35 deletions cli/src/hooks/use-gravity-ad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const AD_ROTATION_INTERVAL_MS = 60 * 1000 // 60 seconds per ad
const MAX_ADS_AFTER_ACTIVITY = 3 // Show up to 3 ads after last activity, then pause fetching new ads
const ACTIVITY_THRESHOLD_MS = 30_000 // 30 seconds idle threshold for fetching new ads
const MAX_AD_CACHE_SIZE = 50 // Maximum number of ads to keep in cache
const ZEROCLICK_IMPRESSIONS_URL = 'https://zeroclick.dev/api/v2/impressions'

// Ad response type (normalized shape across providers; credits added after impression)
export type AdResponse = {
Expand All @@ -25,20 +26,22 @@ export type AdResponse = {
favicon: string
clickUrl: string
impUrl: string
provider?: AdProvider
impressionIds?: string[]
credits?: number // Set after impression is recorded (in cents)
}

/**
* Which upstream ad network to query. The server maps each provider onto the
* same normalized response shape, so the rest of the hook is provider-agnostic.
*/
export type AdProvider = 'gravity' | 'carbon'
export type AdProvider = 'gravity' | 'carbon' | 'zeroclick'
export type AdSurface = 'waiting_room'

export type GravityAdState = {
ads: AdResponse[] | null
isLoading: boolean
recordImpression: (impUrl: string) => void
recordImpression: (ad: AdResponse) => void
}

// Consolidated controller state for the ad rotation logic
Expand All @@ -52,6 +55,10 @@ type GravityController = {

// Pure helper: add a choice ad set to the choice cache
function addToChoiceCache(ctrl: GravityController, ads: AdResponse[]): void {
// ZeroClick offer responses must not be stored for later display. Keep them
// out of the rotation cache and only render them for the live request.
if (ads.some((ad) => ad.provider === 'zeroclick')) return

// Deduplicate by checking if any set has the same first impUrl
const key = ads[0]?.impUrl
if (key && ctrl.choiceCache.some((set) => set[0]?.impUrl === key)) return
Expand Down Expand Up @@ -134,50 +141,89 @@ export const useGravityAd = (options?: {
shouldHideAdsRef.current = shouldHideAds

// Fire impression and update credits (called when showing an ad)
const recordImpressionOnce = (impUrl: string): void => {
const recordImpressionOnce = (ad: AdResponse): void => {
// Don't record impressions when ads should be hidden
if (shouldHideAdsRef.current) return

const ctrl = ctrlRef.current
const { impUrl } = ad
if (ctrl.impressionsFired.has(impUrl)) return
ctrl.impressionsFired.add(impUrl)

const authToken = getAuthToken()
if (!authToken) {
logger.warn('[ads] No auth token, skipping impression recording')
return
}
const recordLocalImpression = async (): Promise<void> => {
const authToken = getAuthToken()
if (!authToken) {
logger.warn('[ads] No auth token, skipping local impression recording')
return
}

// Include mode in request - Freebuff should not grant credits (no balance concept).
const agentMode = useChatStore.getState().agentMode
// Include mode in request - Freebuff should not grant credits (no balance concept).
const agentMode = useChatStore.getState().agentMode

fetch(`${WEBSITE_URL}/api/v1/ads/impression`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({ impUrl, mode: agentMode }),
})
.then((res) => res.json())
.then((data) => {
if (data.creditsGranted > 0) {
logger.info(
{ creditsGranted: data.creditsGranted },
'[ads] Ad impression credits granted',
const res = await fetch(`${WEBSITE_URL}/api/v1/ads/impression`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({ impUrl, mode: agentMode }),
})

if (!res.ok) {
logger.debug(
{ status: res.status },
'[ads] Failed to record local ad impression',
)
return
}

const data = await res.json()
if (data.creditsGranted > 0) {
logger.info(
{ creditsGranted: data.creditsGranted },
'[ads] Ad impression credits granted',
)
// Also update credits in visible ads
setAds((cur) => {
if (!cur) return cur
return cur.map((a) =>
a.impUrl === impUrl ? { ...a, credits: data.creditsGranted } : a,
)
// Also update credits in visible ads
setAds((cur) => {
if (!cur) return cur
return cur.map((a) =>
a.impUrl === impUrl ? { ...a, credits: data.creditsGranted } : a,
)
})
}
}

if (ad.provider === 'zeroclick' && ad.impressionIds?.length) {
void (async () => {
try {
const res = await fetch(ZEROCLICK_IMPRESSIONS_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: ad.impressionIds }),
})

if (!res.ok) {
logger.debug(
{ status: res.status },
'[ads] Failed to record ZeroClick impression',
)
return
}
} catch (err) {
logger.debug({ err }, '[ads] Failed to record ZeroClick impression')
return
}
})
.catch((err) => {
logger.debug({ err }, '[ads] Failed to record ad impression')
})

recordLocalImpression().catch((err) => {
logger.debug({ err }, '[ads] Failed to record local ad impression')
})
})()
return
}

recordLocalImpression().catch((err) => {
logger.debug({ err }, '[ads] Failed to record ad impression')
})
}

type FetchAdResult = { ads: AdResponse[] } | null
Expand Down Expand Up @@ -265,7 +311,12 @@ export const useGravityAd = (options?: {
const data = await response.json()

if (Array.isArray(data.ads) && data.ads.length > 0) {
return { ads: data.ads as AdResponse[] }
return {
ads: (data.ads as AdResponse[]).map((ad) => ({
...ad,
provider: data.provider ?? providerToTry,
})),
}
}
} catch (err) {
logger.error(
Expand Down Expand Up @@ -305,6 +356,8 @@ export const useGravityAd = (options?: {
if (cachedSet) {
ctrl.adsShownSinceActivity += 1
setAds(cachedSet)
} else {
setAds((cur) => (cur?.[0]?.provider === 'zeroclick' ? null : cur))
}
}
} finally {
Expand Down
Loading
Loading