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
4 changes: 2 additions & 2 deletions .github/workflows/freebuff-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
e2e:
needs: build-freebuff
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -101,7 +101,7 @@ jobs:
run: cd sdk && bun run build

- name: Run e2e test - ${{ matrix.test }}
run: bun test freebuff/e2e/tests/${{ matrix.test }}.e2e.test.ts --timeout=120000
run: bun test freebuff/e2e/tests/${{ matrix.test }}.e2e.test.ts --timeout=${{ (matrix.test == 'code-edit' || matrix.test == 'terminal-command') && '900000' || '120000' }}

- name: Upload tmux session logs on failure
if: failure()
Expand Down
24 changes: 10 additions & 14 deletions agents/__tests__/basher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,11 @@ describe('commander agent', () => {
expect(schema?.params?.required).not.toContain('timeout_seconds')
})

test('has optional rawOutput parameter', () => {
test('has optional what_to_summarize parameter', () => {
const schema = commander.inputSchema
const rawOutputProp = schema?.params?.properties?.rawOutput
expect(rawOutputProp && typeof rawOutputProp === 'object' && 'type' in rawOutputProp && rawOutputProp.type).toBe('boolean')
expect(schema?.params?.required).not.toContain('rawOutput')
})

test('has prompt parameter', () => {
expect(commander.inputSchema?.prompt?.type).toBe('string')
const summarizeProp = schema?.params?.properties?.what_to_summarize
expect(summarizeProp && typeof summarizeProp === 'object' && 'type' in summarizeProp && summarizeProp.type).toBe('string')
expect(schema?.params?.required).not.toContain('what_to_summarize')
})
})

Expand Down Expand Up @@ -149,7 +145,7 @@ describe('commander agent', () => {
})
})

test('yields set_output with raw result when rawOutput is true', () => {
test('yields set_output with raw result when what_to_summarize is not provided', () => {
const mockAgentState = createMockAgentState()
const mockLogger = {
debug: () => {},
Expand All @@ -161,7 +157,7 @@ describe('commander agent', () => {
const generator = commander.handleSteps!({
agentState: mockAgentState,
logger: mockLogger as any,
params: { command: 'echo hello', rawOutput: true },
params: { command: 'echo hello' },
})

// First yield is the command
Expand Down Expand Up @@ -190,7 +186,7 @@ describe('commander agent', () => {
expect(final.done).toBe(true)
})

test('yields STEP for model analysis when rawOutput is false', () => {
test('yields STEP for model analysis when what_to_summarize is provided', () => {
const mockAgentState = createMockAgentState()
const mockLogger = {
debug: () => {},
Expand All @@ -202,7 +198,7 @@ describe('commander agent', () => {
const generator = commander.handleSteps!({
agentState: mockAgentState,
logger: mockLogger as any,
params: { command: 'ls -la', rawOutput: false },
params: { command: 'ls -la', what_to_summarize: 'list of files' },
})

// First yield is the command
Expand Down Expand Up @@ -233,7 +229,7 @@ describe('commander agent', () => {
const generator = commander.handleSteps!({
agentState: mockAgentState,
logger: mockLogger as any,
params: { command: 'echo test', rawOutput: true },
params: { command: 'echo test' },
})

// First yield is the command
Expand Down Expand Up @@ -266,7 +262,7 @@ describe('commander agent', () => {
const generator = commander.handleSteps!({
agentState: mockAgentState,
logger: mockLogger as any,
params: { command: 'echo test', rawOutput: true },
params: { command: 'echo test' },
})

// First yield is the command
Expand Down
8 changes: 8 additions & 0 deletions agents/base2/base2-lite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createBase2 } from './base2'

const definition = {
...createBase2('lite'),
id: 'base2-lite',
displayName: 'Buffy the Lite Orchestrator',
}
export default definition
4 changes: 2 additions & 2 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' | 'free' | 'max' | 'fast',
mode: 'default' | 'free' | 'lite' | 'max' | 'fast',
options?: {
hasNoValidation?: boolean
planOnly?: boolean
Expand All @@ -22,7 +22,7 @@ export function createBase2(
const isDefault = mode === 'default'
const isFast = mode === 'fast'
const isMax = mode === 'max'
const isFree = mode === 'free'
const isFree = mode === 'free' || mode === 'lite'

const isSonnet = false
const model = isFree ? 'z-ai/glm-5.1' : 'anthropic/claude-opus-4.7'
Expand Down
2 changes: 1 addition & 1 deletion bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ linkWorkspacePackages = true
[test]
# Exclude test repositories, integration tests, and Playwright e2e tests from test execution by default
exclude = ["evals/test-repos/**", "**/*.integration.test.*", "web/src/__tests__/e2e/**"]
preload = ["./sdk/test/setup-env.ts", "./test/setup-bigquery-mocks.ts", "./web/test/setup-globals.ts"]
preload = ["./test/setup-scm-loader.ts", "./sdk/test/setup-env.ts", "./test/setup-bigquery-mocks.ts", "./web/test/setup-globals.ts"]
2 changes: 1 addition & 1 deletion cli/release/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codebuff",
"version": "1.0.642",
"version": "1.0.643",
"description": "AI coding agent",
"license": "MIT",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion cli/src/__tests__/rerender-perf.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const RERENDER_THRESHOLDS = {
'onToggleCollapsed',
'onBuildFast',
'onBuildMax',
'onBuildFree',
'onBuildLite',
'onCloseFeedback',
],

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, FREE, MAX, PLAN) + 1 active indicator
// 4 mode options (DEFAULT, LITE, MAX, PLAN) + 1 active indicator
expect(segs.length).toBe(5)

// Current mode is disabled among the choices
Expand Down
97 changes: 95 additions & 2 deletions cli/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { useShallow } from 'zustand/react/shallow'

import { Chat } from './chat'
import { ChatHistoryScreen } from './components/chat-history-screen'
import { FreebuffSupersededScreen } from './components/freebuff-superseded-screen'
import { LoginModal } from './components/login-modal'
import { ProjectPickerScreen } from './components/project-picker-screen'
import { TerminalLink } from './components/terminal-link'
import { WaitingRoomScreen } from './components/waiting-room-screen'
import { useAuthQuery } from './hooks/use-auth-query'
import { useAuthState } from './hooks/use-auth-state'
import { useFreebuffSession } from './hooks/use-freebuff-session'
import { useLogo } from './hooks/use-logo'
import { useSheenAnimation } from './hooks/use-sheen-animation'
import { useTerminalDimensions } from './hooks/use-terminal-dimensions'
Expand Down Expand Up @@ -297,8 +300,8 @@ export const App = ({
const chatKey = resumeChatId ?? 'current'

return (
<Chat
key={chatKey}
<AuthedSurface
chatKey={chatKey}
headerContent={headerContent}
initialPrompt={initialPrompt}
agentId={agentId}
Expand All @@ -316,3 +319,93 @@ export const App = ({
/>
)
}

interface AuthedSurfaceProps {
chatKey: string
headerContent: React.ReactNode
initialPrompt: string | null
agentId?: string
fileTree: FileTreeNode[]
inputRef: React.MutableRefObject<MultilineInputHandle | null>
setIsAuthenticated: React.Dispatch<React.SetStateAction<boolean | null>>
setUser: React.Dispatch<React.SetStateAction<import('./utils/auth').User | null>>
logoutMutation: ReturnType<typeof useAuthState>['logoutMutation']
continueChat: boolean
continueChatId: string | undefined
authStatus: AuthStatus
initialMode: AgentMode | undefined
gitRoot: string | null | undefined
onSwitchToGitRoot: () => void
}

/**
* Rendered only after auth is confirmed. Owns the freebuff waiting-room gate
* so `useFreebuffSession` runs exactly once per authed session (not before
* we have a token).
*/
const AuthedSurface = ({
chatKey,
headerContent,
initialPrompt,
agentId,
fileTree,
inputRef,
setIsAuthenticated,
setUser,
logoutMutation,
continueChat,
continueChatId,
authStatus,
initialMode,
gitRoot,
onSwitchToGitRoot,
}: AuthedSurfaceProps) => {
const { session, error: sessionError } = useFreebuffSession()

// Terminal state: a 409 from the gate means another CLI rotated our
// instance id. Show a dedicated screen and stop polling — don't fall back
// into the waiting room, which would look like normal queued progress.
if (IS_FREEBUFF && session?.status === 'superseded') {
return <FreebuffSupersededScreen />
}

// Route every non-admitted state through the waiting room:
// null → initial POST in flight
// 'queued' → waiting our turn
// 'none' → server lost our row; hook is about to re-POST
// Falling through to <Chat> on 'none' would leave the user unable to send
// any free-mode request until the next poll cycle.
//
// 'ended' deliberately falls through to <Chat>: the agent may still be
// finishing work under the server-side grace period, and the chat surface
// itself swaps the input box for the session-ended banner.
if (
IS_FREEBUFF &&
(session === null ||
session.status === 'queued' ||
session.status === 'none')
) {
return <WaitingRoomScreen session={session} error={sessionError} />
}

return (
<Chat
key={chatKey}
headerContent={headerContent}
initialPrompt={initialPrompt}
agentId={agentId}
fileTree={fileTree}
inputRef={inputRef}
setIsAuthenticated={setIsAuthenticated}
setUser={setUser}
logoutMutation={logoutMutation}
continueChat={continueChat}
continueChatId={continueChatId}
authStatus={authStatus}
initialMode={initialMode}
gitRoot={gitRoot}
onSwitchToGitRoot={onSwitchToGitRoot}
freebuffSession={session}
/>
)
}
29 changes: 24 additions & 5 deletions cli/src/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ReviewScreen } from './components/review-screen'
import { MessageWithAgents } from './components/message-with-agents'
import { areCreditsRestored } from './components/out-of-credits-banner'
import { PendingBashMessage } from './components/pending-bash-message'
import { SessionEndedBanner } from './components/session-ended-banner'
import { StatusBar } from './components/status-bar'
import { TopBanner } from './components/top-banner'
import { getSlashCommandsWithSkills } from './data/slash-commands'
Expand Down Expand Up @@ -83,6 +84,7 @@ import { computeInputLayoutMetrics } from './utils/text-layout'
import type { CommandResult } from './commands/command-registry'
import type { MultilineInputHandle } from './components/multiline-input'
import type { MatchedSlashCommand } from './hooks/use-suggestion-engine'
import type { FreebuffSessionResponse } from './types/freebuff-session'
import type { User } from './utils/auth'
import type { AgentMode } from './utils/constants'
import type { FileTreeNode } from '@codebuff/common/util/file'
Expand All @@ -105,6 +107,7 @@ export const Chat = ({
initialMode,
gitRoot,
onSwitchToGitRoot,
freebuffSession,
}: {
headerContent: React.ReactNode
initialPrompt: string | null
Expand All @@ -120,6 +123,7 @@ export const Chat = ({
initialMode?: AgentMode
gitRoot?: string | null
onSwitchToGitRoot?: () => void
freebuffSession: FreebuffSessionResponse | null
}) => {
const [forceFileOnlyMentions, setForceFileOnlyMentions] = useState(false)

Expand Down Expand Up @@ -614,7 +618,7 @@ export const Chat = ({
],
)

const { inputWidth, handleBuildFast, handleBuildMax, handleBuildFree } = useChatInput({
const { inputWidth, handleBuildFast, handleBuildMax, handleBuildLite } = useChatInput({
setInputValue,
agentMode,
setAgentMode,
Expand Down Expand Up @@ -1242,15 +1246,15 @@ export const Chat = ({
onToggleCollapsed: handleCollapseToggle,
onBuildFast: handleBuildFast,
onBuildMax: handleBuildMax,
onBuildFree: handleBuildFree,
onBuildLite: handleBuildLite,
onFeedback: handleMessageFeedback,
onCloseFeedback: handleCloseFeedback,
})
}, [
handleCollapseToggle,
handleBuildFast,
handleBuildMax,
handleBuildFree,
handleBuildLite,
handleMessageFeedback,
handleCloseFeedback,
setMessageBlockCallbacks,
Expand Down Expand Up @@ -1337,9 +1341,16 @@ export const Chat = ({
return ` ${segments.join(' ')} `
}, [queuePreviewTitle, pausedQueueText])

const hasActiveFreebuffSession =
IS_FREEBUFF && freebuffSession?.status === 'active'
const isFreebuffSessionOver =
IS_FREEBUFF && freebuffSession?.status === 'ended'
const shouldShowStatusLine =
!feedbackMode &&
(hasStatusIndicatorContent || shouldShowQueuePreview || !isAtBottom)
(hasStatusIndicatorContent ||
shouldShowQueuePreview ||
!isAtBottom ||
hasActiveFreebuffSession)

// Track mouse movement for ad activity (throttled)
const lastMouseActivityRef = useRef<number>(0)
Expand Down Expand Up @@ -1442,6 +1453,7 @@ export const Chat = ({
scrollToLatest={scrollToLatest}
statusIndicatorState={statusIndicatorState}
onStop={chatKeyboardHandlers.onInterruptStream}
freebuffSession={freebuffSession}
/>
)}

Expand All @@ -1455,17 +1467,24 @@ export const Chat = ({
<AdBanner
ad={ad}
onDisableAds={handleDisableAds}
isFreeMode={IS_FREEBUFF || agentMode === 'FREE'}
isFreeMode={IS_FREEBUFF}
/>
)
)}

{reviewMode ? (
// Review takes precedence over the session-ended banner: during the
// grace window the agent may still be asking to run tools, and
// those approvals must be reachable for the run to finish.
<ReviewScreen
onSelectOption={handleReviewOptionSelect}
onCustom={handleReviewCustom}
onCancel={handleCloseReviewScreen}
/>
) : isFreebuffSessionOver ? (
<SessionEndedBanner
isStreaming={isStreaming || isWaitingForResponse}
/>
) : (
<ChatInputBar
inputValue={inputValue}
Expand Down
10 changes: 1 addition & 9 deletions cli/src/commands/ads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,7 @@ export const handleAdsDisable = (): {
export const getAdsEnabled = (): boolean => {
if (IS_FREEBUFF) return true

// 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
// Codebuff LITE is a paid mode now, so use the normal saved setting.
const settings = loadSettings()
return settings.adsEnabled ?? false
}
Loading
Loading