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
8 changes: 8 additions & 0 deletions agents/base2/base2-free.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createBase2 } from './base2'

const definition = {
...createBase2('free'),
id: 'base2-free',
displayName: 'Buffy the Free Orchestrator',
}
export default definition
8 changes: 0 additions & 8 deletions agents/base2/base2-lite.ts

This file was deleted.

32 changes: 16 additions & 16 deletions agents/base2/base2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '../types/secret-agent-definition'

export function createBase2(
mode: 'default' | 'lite' | 'max' | 'fast',
mode: 'default' | 'free' | 'max' | 'fast',
options?: {
hasNoValidation?: boolean
planOnly?: boolean
Expand All @@ -22,15 +22,15 @@ export function createBase2(
const isDefault = mode === 'default'
const isFast = mode === 'fast'
const isMax = mode === 'max'
const isLite = mode === 'lite'
const isFree = mode === 'free'

const isOpus = !isLite
const isOpus = !isFree
const isSonnet = false
const isGemini = false

return {
publisher,
model: isLite ? 'x-ai/grok-4.1-fast' : 'anthropic/claude-opus-4.5',
model: isFree ? 'x-ai/grok-4.1-fast' : 'anthropic/claude-opus-4.5',
displayName: 'Buffy the Orchestrator',
spawnerPrompt:
'Advanced base agent that orchestrates planning, editing, and reviewing for complex coding tasks',
Expand All @@ -55,7 +55,7 @@ export function createBase2(
'spawn_agents',
'read_files',
'read_subtree',
!isFast && !isLite && 'write_todos',
!isFast && !isFree && 'write_todos',
!isFast && !noAskUser && 'suggest_followups',
'str_replace',
'write_file',
Expand All @@ -72,11 +72,11 @@ export function createBase2(
'glob-matcher',
'researcher-web',
'researcher-docs',
isLite ? 'commander-lite' : 'commander',
isFree ? 'commander-lite' : 'commander',
isDefault && 'thinker',
(isDefault || isMax) && ['opus-agent', 'gpt-5-agent'],
isMax && 'thinker-best-of-n-opus',
isLite && 'editor-glm',
isFree && 'editor-glm',
isDefault && 'editor',
isMax && 'editor-multi-prompt',
isDefault && 'code-reviewer',
Expand Down Expand Up @@ -133,7 +133,7 @@ Use the spawn_agents tool to spawn specialized agents to help you complete the u
- **Sequence agents properly:** Keep in mind dependencies when spawning different agents. Don't spawn agents in parallel that depend on each other.
${buildArray(
'- Spawn context-gathering agents (file pickers, code-searcher, directory-lister, glob-matcher, and web/docs researchers) before making edits.',
isLite &&
isFree &&
'- Spawn the editor-glm agent to implement the changes after you have gathered all the context you need.',
isDefault &&
'- Spawn the editor agent to implement the changes after you have gathered all the context you need.',
Expand Down Expand Up @@ -198,7 +198,7 @@ ${isDefault
? `[ You implement the changes using the editor agent ]`
: isFast
? '[ You implement the changes using the str_replace or write_file tools ]'
: isLite
: isFree
? '[ You implement the changes using the editor-glm agent ]'
: '[ You implement the changes using the editor-multi-prompt agent ]'
}
Expand Down Expand Up @@ -248,7 +248,7 @@ ${PLACEHOLDER.GIT_CHANGES_PROMPT}
isFast,
isDefault,
isMax,
isLite,
isFree,
hasNoValidation,
noAskUser,
}),
Expand All @@ -260,7 +260,7 @@ ${PLACEHOLDER.GIT_CHANGES_PROMPT}
isMax,
hasNoValidation,
isSonnet,
isLite,
isFree,
noAskUser,
}),

Expand Down Expand Up @@ -292,15 +292,15 @@ function buildImplementationInstructionsPrompt({
isFast,
isDefault,
isMax,
isLite,
isFree,
hasNoValidation,
noAskUser,
}: {
isSonnet: boolean
isFast: boolean
isDefault: boolean
isMax: boolean
isLite: boolean
isFree: boolean
hasNoValidation: boolean
noAskUser: boolean
}) {
Expand All @@ -320,7 +320,7 @@ ${buildArray(
`- For any task requiring 3+ steps, use the write_todos tool to write out your step-by-step implementation plan. Include ALL of the applicable tasks in the list.${isFast ? '' : ' You should include a step to review the changes after you have implemented the changes.'}:${hasNoValidation ? '' : ' You should include at least one step to validate/test your changes: be specific about whether to typecheck, run tests, run lints, etc.'} You may be able to do reviewing and validation in parallel in the same step. Skip write_todos for simple tasks like quick edits or answering questions.`,
(isDefault || isMax) &&
`- For quick problems, briefly explain your reasoning to the user. If you need to think longer, write your thoughts within the <think> tags. Finally, for complex problems, spawn the thinker agent to help find the best solution. (gpt-5-agent is a last resort for complex problems)`,
isLite &&
isFree &&
'- IMPORTANT: You must spawn the editor-glm agent to implement the changes after you have gathered all the context you need. This agent will do the best job of implementing the changes so you must spawn it for all changes. Do not pass any prompt or params to the editor agent when spawning it. It will make its own best choices of what to do.',
isDefault &&
'- IMPORTANT: You must spawn the editor agent to implement the changes after you have gathered all the context you need. This agent will do the best job of implementing the changes so you must spawn it for all non-trivial changes. Do not pass any prompt or params to the editor agent when spawning it. It will make its own best choices of what to do.',
Expand All @@ -347,15 +347,15 @@ function buildImplementationStepPrompt({
isMax,
hasNoValidation,
isSonnet,
isLite,
isFree,
noAskUser,
}: {
isDefault: boolean
isFast: boolean
isMax: boolean
hasNoValidation: boolean
isSonnet: boolean
isLite: boolean
isFree: boolean
noAskUser: boolean
}) {
return buildArray(
Expand Down
2 changes: 1 addition & 1 deletion cli/src/__tests__/unit/agent-mode-toggle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('AgentModeToggle - buildExpandedSegments', () => {
for (const mode of modes) {
test(`returns segments with active indicator for ${mode}`, () => {
const segs = buildExpandedSegments(mode)
// 4 mode options (DEFAULT, LITE, MAX, PLAN) + 1 active indicator
// 4 mode options (DEFAULT, FREE, MAX, PLAN) + 1 active indicator
expect(segs.length).toBe(5)

// Current mode is disabled among the choices
Expand Down
12 changes: 11 additions & 1 deletion cli/src/commands/ads.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { saveSettings, loadSettings } from '../utils/settings'
import { getSystemMessage } from '../utils/message-history'
import { useChatStore } from '../state/chat-store'
import { logger } from '../utils/logger'

import type { ChatMessage } from '../types/chat'
Expand All @@ -8,7 +9,7 @@ export const handleAdsEnable = (): {
postUserMessage: (messages: ChatMessage[]) => ChatMessage[]
} => {
logger.info('[gravity] Enabling ads')

saveSettings({ adsEnabled: true })

return {
Expand All @@ -34,6 +35,15 @@ export const handleAdsDisable = (): {
}

export const getAdsEnabled = (): boolean => {
// If no mode provided, get it from the store
const mode = useChatStore.getState().agentMode

// In FREE mode, ads are always enabled regardless of saved setting
if (mode === 'FREE') {
return true
}

// Otherwise, use the saved setting
const settings = loadSettings()
return settings.adsEnabled ?? false
}
8 changes: 3 additions & 5 deletions cli/src/components/blocks/thinking-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,10 @@ export const ThinkingBlock = memo(
}
}, [onToggleCollapsed, thinkingId])

// thinkingOpen === true means still streaming
// thinkingOpen === false means explicitly closed with </think> tag
// thinkingOpen === undefined means native reasoning block - complete when message is complete
// thinkingOpen === false means explicitly closed (with </think> tag or message completion)
// Otherwise (true or undefined), completion is determined by message completion
const isThinkingComplete =
firstBlock?.thinkingOpen === false ||
(firstBlock?.thinkingOpen === undefined && isMessageComplete)
firstBlock?.thinkingOpen === false || isMessageComplete

// Hide if no content or no thinkingId (but NOT when thinking is complete)
if (!combinedContent || !thinkingId) {
Expand Down
5 changes: 4 additions & 1 deletion cli/src/hooks/use-gravity-ad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,16 @@ export const useGravityAd = (): GravityAdState => {
return
}

// Include mode in request - FREE mode should not grant credits
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 }),
body: JSON.stringify({ impUrl, mode: agentMode }),
})
.then((res) => res.json())
.then((data) => {
Expand Down
3 changes: 2 additions & 1 deletion cli/src/hooks/use-send-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { setCurrentChatId } from '../project-files'
import { createStreamController } from './stream-state'
import { useChatStore } from '../state/chat-store'
import { getCodebuffClient } from '../utils/codebuff-client'
import { AGENT_MODE_TO_ID } from '../utils/constants'
import { AGENT_MODE_TO_ID, AGENT_MODE_TO_COST_MODE } from '../utils/constants'
import { createEventHandlerState } from '../utils/create-event-handler-state'
import { createRunConfig } from '../utils/create-run-config'
import { loadAgentDefinitions } from '../utils/local-agent-registry'
Expand Down Expand Up @@ -443,6 +443,7 @@ export const useSendMessage = ({
agentDefinitions,
eventHandlerState,
signal: abortController.signal,
costMode: AGENT_MODE_TO_COST_MODE[agentMode],
})

logger.info({ runConfig }, '[send-message] Sending message with sdk run config')
Expand Down
5 changes: 3 additions & 2 deletions cli/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ function parseArgs(): ParsedArgs {
'--cwd <directory>',
'Set the working directory (default: current directory)',
)
.option('--lite', 'Start in LITE mode')
.option('--free', 'Start in FREE mode')
.option('--lite', 'Start in FREE mode (deprecated, use --free)')
.option('--max', 'Start in MAX mode')
.option('--plan', 'Start in PLAN mode')
.helpOption('-h, --help', 'Show this help message')
Expand All @@ -129,7 +130,7 @@ function parseArgs(): ParsedArgs {

// Determine initial mode from flags (last flag wins if multiple specified)
let initialMode: AgentMode | undefined
if (options.lite) initialMode = 'LITE'
if (options.free || options.lite) initialMode = 'FREE'
if (options.max) initialMode = 'MAX'
if (options.plan) initialMode = 'PLAN'

Expand Down
13 changes: 12 additions & 1 deletion cli/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,21 @@ export const MAIN_AGENT_ID = 'main-agent'
*/
export const AGENT_MODE_TO_ID = {
DEFAULT: 'base2',
LITE: 'base2-lite',
FREE: 'base2-free',
MAX: 'base2-max',
PLAN: 'base2-plan',
} as const

export type AgentMode = keyof typeof AGENT_MODE_TO_ID
export const AGENT_MODES = Object.keys(AGENT_MODE_TO_ID) as AgentMode[]

/**
* Maps CLI agent mode to cost mode for billing.
* FREE mode maps to 'free' cost mode where allowlisted agent+model combos cost 0 credits.
*/
export const AGENT_MODE_TO_COST_MODE = {
DEFAULT: 'normal',
FREE: 'free',
MAX: 'max',
PLAN: 'normal',
} as const satisfies Record<AgentMode, 'free' | 'normal' | 'max' | 'experimental' | 'ask'>
3 changes: 3 additions & 0 deletions cli/src/utils/create-run-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type CreateRunConfigParams = {
agentDefinitions: AgentDefinition[]
eventHandlerState: EventHandlerState
signal: AbortSignal
costMode?: 'free' | 'normal' | 'max' | 'experimental' | 'ask'
}

const SENSITIVE_EXTENSIONS = new Set([
Expand Down Expand Up @@ -98,6 +99,7 @@ export const createRunConfig = (params: CreateRunConfigParams) => {
previousRunState,
agentDefinitions,
eventHandlerState,
costMode,
} = params

return {
Expand All @@ -111,6 +113,7 @@ export const createRunConfig = (params: CreateRunConfigParams) => {
handleStreamChunk: createStreamChunkHandler(eventHandlerState),
handleEvent: createEventHandler(eventHandlerState),
signal: params.signal,
costMode,
fileFilter: ((filePath: string) => {
if (isSensitiveFile(filePath)) return { status: 'blocked' }
if (isEnvTemplateFile(filePath)) return { status: 'allow-example' }
Expand Down
34 changes: 33 additions & 1 deletion cli/src/utils/message-updater.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ChatMessage, ContentBlock } from '../types/chat'
import type { ChatMessage, ContentBlock, TextContentBlock } from '../types/chat'

// Small wrapper to avoid repeating the ai-message map/update pattern.
export type SetMessagesFn = (
Expand Down Expand Up @@ -57,9 +57,25 @@ export const createMessageUpdater = (
const markComplete = (metadata?: Partial<ChatMessage>) => {
updateAiMessage((msg) => {
const { metadata: messageMetadata, ...rest } = metadata ?? {}

// Mark native reasoning blocks as complete by setting thinkingOpen = false
// This ensures thinking blocks auto-collapse when the message finishes
// Check for thinkingOpen !== false to handle both true (native) and undefined (legacy)
const updatedBlocks = msg.blocks?.map((block) => {
if (
block.type === 'text' &&
(block as TextContentBlock).textType === 'reasoning' &&
(block as TextContentBlock).thinkingOpen !== false
) {
return { ...block, thinkingOpen: false } as ContentBlock
}
return block
})

const nextMessage: ChatMessage = {
...msg,
isComplete: true,
...(updatedBlocks && { blocks: updatedBlocks }),
...rest,
}

Expand Down Expand Up @@ -184,9 +200,25 @@ export const createBatchedMessageUpdater = (
prev.map((msg) => {
if (msg.id !== aiMessageId) return msg
const { metadata: messageMetadata, ...rest } = metadata ?? {}

// Mark native reasoning blocks as complete by setting thinkingOpen = false
// This ensures thinking blocks auto-collapse when the message finishes
// Check for thinkingOpen !== false to handle both true (native) and undefined (legacy)
const updatedBlocks = msg.blocks?.map((block) => {
if (
block.type === 'text' &&
(block as TextContentBlock).textType === 'reasoning' &&
(block as TextContentBlock).thinkingOpen !== false
) {
return { ...block, thinkingOpen: false } as ContentBlock
}
return block
})

const nextMessage: ChatMessage = {
...msg,
isComplete: true,
...(updatedBlocks && { blocks: updatedBlocks }),
...rest,
}
if (messageMetadata) {
Expand Down
2 changes: 2 additions & 0 deletions cli/src/utils/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const DEFAULT_SETTINGS: Settings = {
adsEnabled: true,
}

// Note: FREE mode is now a valid AgentMode (was previously LITE)

/**
* Settings schema - add new settings here as the product evolves
*/
Expand Down
5 changes: 2 additions & 3 deletions common/src/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { z } from 'zod/v4'

import type { CostMode } from './old-constants'
import type { GrantType } from './types/grant'
import type { MCPConfig } from './types/mcp'
import type { ToolMessage } from './types/messages/codebuff-message'
Expand Down Expand Up @@ -30,7 +29,7 @@ type ClientActionPrompt = {
promptParams?: Record<string, any> // Additional json params.
fingerprintId: string
authToken?: string
costMode?: CostMode
costMode?: string
sessionState: SessionState
toolResults: ToolMessage[]
model?: string
Expand Down Expand Up @@ -70,7 +69,7 @@ type ClientActionMcpToolData = {
tools: {
name: string
description?: string
inputSchema: { type: 'object'; [k: string]: unknown }
inputSchema: { type: 'object';[k: string]: unknown }
}[]
}

Expand Down
Loading