Skip to content

Commit a71a7b9

Browse files
committed
Merge remote-tracking branch 'origin/main' into jahooma/remove-referrals
2 parents 8c44fcb + 711f40c commit a71a7b9

17 files changed

Lines changed: 488 additions & 47 deletions

File tree

.claude/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"permissions": {
3+
"defaultMode": "auto"
4+
}
5+
}

cli/src/app.tsx

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -285,17 +285,6 @@ export const App = ({
285285
)
286286
}
287287

288-
// Render chat history screen when requested
289-
if (showChatHistory) {
290-
return (
291-
<ChatHistoryScreen
292-
onSelectChat={handleResumeChat}
293-
onCancel={closeChatHistory}
294-
onNewChat={handleNewChat}
295-
/>
296-
)
297-
}
298-
299288
// Use key to force remount when resuming a different chat from history
300289
const chatKey = resumeChatId ?? 'current'
301290

@@ -316,6 +305,10 @@ export const App = ({
316305
initialMode={initialMode}
317306
gitRoot={gitRoot}
318307
onSwitchToGitRoot={handleSwitchToGitRoot}
308+
showChatHistory={showChatHistory}
309+
onSelectChat={handleResumeChat}
310+
onCancelChatHistory={closeChatHistory}
311+
onNewChat={handleNewChat}
319312
/>
320313
)
321314
}
@@ -336,6 +329,10 @@ interface AuthedSurfaceProps {
336329
initialMode: AgentMode | undefined
337330
gitRoot: string | null | undefined
338331
onSwitchToGitRoot: () => void
332+
showChatHistory: boolean
333+
onSelectChat: (chatId: string) => void
334+
onCancelChatHistory: () => void
335+
onNewChat: () => void
339336
}
340337

341338
/**
@@ -359,6 +356,10 @@ const AuthedSurface = ({
359356
initialMode,
360357
gitRoot,
361358
onSwitchToGitRoot,
359+
showChatHistory,
360+
onSelectChat,
361+
onCancelChatHistory,
362+
onNewChat,
362363
}: AuthedSurfaceProps) => {
363364
const { session, error: sessionError } = useFreebuffSession()
364365

@@ -388,6 +389,20 @@ const AuthedSurface = ({
388389
return <WaitingRoomScreen session={session} error={sessionError} />
389390
}
390391

392+
// Chat history renders inside AuthedSurface so the freebuff session stays
393+
// mounted while the user browses history. Unmounting this surface would
394+
// DELETE the session row and drop the user back into the waiting room on
395+
// return.
396+
if (showChatHistory) {
397+
return (
398+
<ChatHistoryScreen
399+
onSelectChat={onSelectChat}
400+
onCancel={onCancelChatHistory}
401+
onNewChat={onNewChat}
402+
/>
403+
)
404+
}
405+
391406
return (
392407
<Chat
393408
key={chatKey}

cli/src/chat.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,15 +1473,17 @@ export const Chat = ({
14731473
)}
14741474

14751475
{reviewMode ? (
1476-
// Review takes precedence over the session-ended banner: during the
1477-
// grace window the agent may still be asking to run tools, and
1478-
// those approvals must be reachable for the run to finish.
1476+
// Review and ask_user take precedence over the session-ended banner:
1477+
// during the grace window the agent may still be asking to run tools
1478+
// or asking the user a question, and those approvals/answers must be
1479+
// reachable for the run to finish — otherwise the agent hangs
1480+
// waiting for input that can never be given.
14791481
<ReviewScreen
14801482
onSelectOption={handleReviewOptionSelect}
14811483
onCustom={handleReviewCustom}
14821484
onCancel={handleCloseReviewScreen}
14831485
/>
1484-
) : isFreebuffSessionOver ? (
1486+
) : isFreebuffSessionOver && !askUserState ? (
14851487
<SessionEndedBanner
14861488
isStreaming={isStreaming || isWaitingForResponse}
14871489
/>

cli/src/hooks/helpers/send-message.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -510,10 +510,16 @@ function handleFreebuffGateError(
510510
switch (kind) {
511511
case 'session_expired':
512512
case 'waiting_room_required':
513-
// Our seat is gone mid-chat. Flip to `ended` instead of auto re-queuing:
514-
// the Chat surface stays mounted so any in-flight agent work can finish
515-
// under the server-side grace period, and the session-ended banner
516-
// prompts the user to press Enter when they're ready to rejoin.
513+
// Our seat is gone mid-chat. Finalize the AI message so its streaming
514+
// indicator stops — otherwise `isComplete` stays false and the message
515+
// keeps rendering a blinking cursor forever, making the user think the
516+
// agent is still working even though the SessionEndedBanner is visible
517+
// and actionable. Also disposes the batched-updater flush interval.
518+
updater.markComplete()
519+
// Flip to `ended` instead of auto re-queuing: the Chat surface stays
520+
// mounted so any in-flight agent work can finish under the server-side
521+
// grace period, and the session-ended banner prompts the user to press
522+
// Enter when they're ready to rejoin.
517523
markFreebuffSessionEnded()
518524
return
519525
case 'waiting_room_queued':

docs/error-schema.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Used for errors that the client needs to identify programmatically:
3434

3535
| Status | `error` code | Example `message` |
3636
|--------|-------------|-------------------|
37-
| 403 | `account_suspended` | `"Your account has been suspended due to billing issues. Please contact support@codebuff.com to resolve this."` |
37+
| 403 | `account_suspended` | `"Your account has been suspended. Please contact support@codebuff.com if you did not expect this."` |
3838
| 403 | `free_mode_unavailable` | `"Free mode is not available in your country."` (Freebuff: `"Freebuff is not available in your country."`) |
3939
| 429 | `rate_limit_exceeded` | `"Subscription weekly limit reached. Your limit resets in 2 hours. Enable 'Continue with credits' in the CLI to use a-la-carte credits."` |
4040

packages/agent-runtime/src/__tests__/main-prompt.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ describe('mainPrompt', () => {
375375
it('should update consecutiveAssistantMessages when new prompt is received', async () => {
376376
const sessionState = getInitialSessionState(mockFileContext)
377377
sessionState.mainAgentState.stepsRemaining = 12
378+
const initialStepsRemaining = sessionState.mainAgentState.stepsRemaining
378379

379380
const action = {
380381
type: 'prompt' as const,
@@ -394,7 +395,7 @@ describe('mainPrompt', () => {
394395

395396
// When there's a new prompt, consecutiveAssistantMessages should be set to 1
396397
expect(newSessionState.mainAgentState.stepsRemaining).toBe(
397-
sessionState.mainAgentState.stepsRemaining - 1,
398+
initialStepsRemaining - 1,
398399
)
399400
})
400401

packages/agent-runtime/src/run-agent-step.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,17 @@ export const runAgentStep = async (
536536
}
537537
}
538538

539+
/**
540+
* Runs the agent loop.
541+
*
542+
* IMPORTANT: This function mutates `params.agentState` in place throughout the
543+
* run (not just at return time). Fields like `messageHistory`, `systemPrompt`,
544+
* `toolDefinitions`, `creditsUsed`, and `output` are updated as work progresses
545+
* so that callers holding a reference to the same object (e.g. the SDK's
546+
* `sessionState.mainAgentState`) see in-progress work immediately — which
547+
* matters when an error is thrown mid-run and the normal return path is
548+
* skipped.
549+
*/
539550
export async function loopAgentSteps(
540551
params: {
541552
addAgentStep: AddAgentStepFn
@@ -800,12 +811,13 @@ export async function loopAgentSteps(
800811
return cachedAdditionalToolDefinitions
801812
}
802813

803-
let currentAgentState: AgentState = {
804-
...initialAgentState,
805-
messageHistory: initialMessages,
806-
systemPrompt: system,
807-
toolDefinitions,
808-
}
814+
// Mutate initialAgentState so that in-progress work propagates back to the
815+
// caller's shared reference (e.g. SDK's sessionState.mainAgentState) even if
816+
// an error is thrown before we return.
817+
initialAgentState.messageHistory = initialMessages
818+
initialAgentState.systemPrompt = system
819+
initialAgentState.toolDefinitions = toolDefinitions
820+
let currentAgentState: AgentState = initialAgentState
809821

810822
// Convert tool definitions to Anthropic format for accurate token counting
811823
// Tool definitions are stored as { [name]: { description, inputSchema } }
@@ -908,7 +920,8 @@ export async function loopAgentSteps(
908920
} = programmaticResult
909921
n = generateN
910922

911-
currentAgentState = programmaticAgentState
923+
Object.assign(initialAgentState, programmaticAgentState)
924+
currentAgentState = initialAgentState
912925
totalSteps = stepNumber
913926

914927
shouldEndTurn = endTurn
@@ -989,7 +1002,8 @@ export async function loopAgentSteps(
9891002
logger.error('No runId found for agent state after finishing agent run')
9901003
}
9911004

992-
currentAgentState = newAgentState
1005+
Object.assign(initialAgentState, newAgentState)
1006+
currentAgentState = initialAgentState
9931007
shouldEndTurn = llmShouldEndTurn
9941008
nResponses = generatedResponses
9951009

0 commit comments

Comments
 (0)