Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c7e5807
Freebuff waiting room backend
jahooma Apr 18, 2026
41ffbab
Freebuff waiting room client
jahooma Apr 18, 2026
fa1f8f8
Fix cli test
jahooma Apr 18, 2026
2d9f081
10 minutes before cache clears in free mode
jahooma Apr 18, 2026
90a9580
Remove thinker-with-files-gemini from freebuff
jahooma Apr 18, 2026
c76c5a3
Bump version to 1.0.642
github-actions[bot] Apr 18, 2026
35d0775
Bump Freebuff version to 0.0.34
github-actions[bot] Apr 18, 2026
0f261bf
Increase test timeout
jahooma Apr 18, 2026
9f8de8a
fix: avoid DNS lookup after proxied release CONNECT (#506)
YoungSx Apr 18, 2026
e411821
Remove evalbuff and expensivebuff (#493)
aether-agent[bot] Apr 18, 2026
45fe312
fix: correct code-map line counting (#508)
hiSandog Apr 18, 2026
5c51810
Revert restrictions on using paid codebuff
jahooma Apr 18, 2026
bb53e06
Merge branch 'main' into freebuff-waiting-room
jahooma Apr 18, 2026
282194a
Fixes
jahooma Apr 18, 2026
8ca704a
Admit one user per 30s
jahooma Apr 18, 2026
4a0efb8
Detect cold Fireworks deployments; tighten TTFT/queue thresholds
jahooma Apr 18, 2026
f5f2f60
Drop MAX_CONCURRENT_SESSIONS; drip admission is sole concurrency control
jahooma Apr 18, 2026
0a1bd36
Handle Ctrl+C on freebuff waiting-room / superseded screens
jahooma Apr 18, 2026
e25cde5
Tighten TTFT/queue degraded thresholds; add scrape-check script
jahooma Apr 18, 2026
8ee55ab
Improve waiting room screen
jahooma Apr 18, 2026
845bed1
Session countdown
jahooma Apr 19, 2026
5ddb102
Add freebuff session grace window
jahooma Apr 19, 2026
febb263
Add freebuff session-end banner and drain-window handling
jahooma Apr 19, 2026
0204a37
Replace Fireworks Prometheus monitor with reachability probe
jahooma Apr 19, 2026
5ca04d3
Show upgrade-required error to old freebuff clients
jahooma Apr 19, 2026
f99d28f
Simplify freebuff waiting-room implementation
jahooma Apr 19, 2026
ede8639
Collapse freebuff session states into the wire shape
jahooma Apr 19, 2026
0148514
Show session drain as status bar fill, with final-5m countdown
jahooma Apr 19, 2026
d575c88
Tweak text for session end banner
jahooma Apr 19, 2026
fdf60ae
More cleanup
jahooma Apr 19, 2026
59a0e48
Fix basher test
jahooma Apr 19, 2026
1aeab98
Handle old backend result
jahooma Apr 19, 2026
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
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
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"]
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}
/>
)
}
21 changes: 20 additions & 1 deletion 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 @@ -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 @@ -1461,11 +1473,18 @@ export const Chat = ({
)}

{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
62 changes: 62 additions & 0 deletions cli/src/components/freebuff-superseded-screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { TextAttributes } from '@opentui/core'
import React from 'react'

import { useFreebuffCtrlCExit } from '../hooks/use-freebuff-ctrl-c-exit'
import { useLogo } from '../hooks/use-logo'
import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
import { useTheme } from '../hooks/use-theme'
import { getLogoAccentColor, getLogoBlockColor } from '../utils/theme-system'

/**
* Terminal state shown after a 409 session_superseded response. Another CLI on
* the same account rotated our instance id and we've stopped polling — the
* user needs to close the other instance and restart.
*/
export const FreebuffSupersededScreen: React.FC = () => {
const theme = useTheme()
const { contentMaxWidth } = useTerminalDimensions()
const blockColor = getLogoBlockColor(theme.name)
const accentColor = getLogoAccentColor(theme.name)
const { component: logoComponent } = useLogo({
availableWidth: contentMaxWidth,
accentColor,
blockColor,
})

useFreebuffCtrlCExit()

return (
<box
style={{
width: '100%',
height: '100%',
flexDirection: 'column',
backgroundColor: theme.background,
alignItems: 'center',
justifyContent: 'center',
paddingLeft: 2,
paddingRight: 2,
gap: 1,
}}
>
<box style={{ marginBottom: 1 }}>{logoComponent}</box>
<text
style={{ fg: theme.foreground, marginBottom: 1 }}
attributes={TextAttributes.BOLD}
>
Another freebuff instance took over this account.
</text>
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
Only one CLI per account can be active at a time.
</text>
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
Close the other instance, then restart freebuff here.
</text>
<box style={{ marginTop: 1 }}>
<text style={{ fg: theme.muted }}>
Press <span fg={theme.primary}>Ctrl+C</span> to exit.
</text>
</box>
</box>
)
}
Loading
Loading