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
25 changes: 19 additions & 6 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"@types/pg": "^8.11.10",
"@types/readable-stream": "^4.0.18",
"@types/seedrandom": "^3.0.8",
"ai": "^5.0.0",
"ai": "5.0.0",
"ignore": "5.3.2",
"lodash": "4.17.21",
"next-auth": "^4.24.11",
Expand Down Expand Up @@ -215,9 +215,10 @@
"name": "@codebuff/sdk",
"version": "0.10.2",
"dependencies": {
"@ai-sdk/anthropic": "2.0.50",
"@jitl/quickjs-wasmfile-release-sync": "0.31.0",
"@vscode/tree-sitter-wasm": "0.1.4",
"ai": "^5.0.0",
"ai": "5.0.0",
"diff": "8.0.2",
"ignore": "7.0.5",
"micromatch": "^4.0.8",
Expand Down Expand Up @@ -340,7 +341,9 @@
"packages": {
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],

"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-qmX7afPRszUqG5hryHF3UN8ITPIRSGmDW6VYCmByzjoUkgm3MekzSx2hMV1wr0P+llDeuXb378SjqUfpvWJulg=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],

"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-VEm87DyRx1yIPywbTy8ntoyh4jEDv1rJ88m+2I7zOm08jJI5BhFtAWh0OF6YzZu1Vu4NxhOWO4ssGdsqydDQ3A=="],

"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.25", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.15" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VPylb5ytkOu9Bs1UnVmz4x0wr1VtS30Pw6ghh6GxpGH6lo4GOWqVnYuB+8M755dkof74c5LULZq5C1n/1J4Kvg=="],

Expand Down Expand Up @@ -1394,8 +1397,6 @@

"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],

"@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="],

"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="],

"@vscode/tree-sitter-wasm": ["@vscode/tree-sitter-wasm@0.1.4", "", {}, "sha512-kQVVg/CamCYDM+/XYCZuNTQyixjZd8ts/Gf84UzjEY0eRnbg6kiy5I9z2/2i3XdqwhI87iG07rkMR2KwhqcSbA=="],
Expand Down Expand Up @@ -1430,7 +1431,7 @@

"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],

"ai": ["ai@5.0.116", "", { "dependencies": { "@ai-sdk/gateway": "2.0.23", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+2hYJ80/NcDWuv9K2/MLP3cTCFgwWHmHlS1tOpFUKKcmLbErAAlE/S2knsKboc3PNAu8pQkDr2N3K/Vle7ENgQ=="],
"ai": ["ai@5.0.0", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-F4jOhOSeiZD8lXpF4l1hRqyM1jbqoLKGVZNxAP467wmQCsWUtElMa3Ki5PrDMq6qvUNC3deUKfERDAsfj7IDlg=="],

"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],

Expand Down Expand Up @@ -3570,6 +3571,10 @@

"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],

"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],

"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],

"@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.15", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kOc6Pxb7CsRlNt+sLZKL7/VGQUd7ccl3/tIK+Bqf5/QhHR0Qm3qRBMz1IwU1RmjJEZA73x+KB5cUckbDl2WF7Q=="],

"@auth/core/jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
Expand Down Expand Up @@ -3762,6 +3767,8 @@

"accepts/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],

"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],

"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],

"app-path/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
Expand Down Expand Up @@ -4120,6 +4127,10 @@

"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],

"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],

"@ai-sdk/gateway/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],

"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],

"@codebuff/web/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2" } }, "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA=="],
Expand Down Expand Up @@ -4292,6 +4303,8 @@

"accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],

"ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],

"app-path/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],

"app-path/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
Expand Down
21 changes: 21 additions & 0 deletions cli/src/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getAdsEnabled } from './commands/ads'
import { routeUserPrompt, addBashMessageToHistory } from './commands/router'
import { AdBanner } from './components/ad-banner'
import { ChatInputBar } from './components/chat-input-bar'
import { BottomStatusLine } from './components/bottom-status-line'
import { areCreditsRestored } from './components/out-of-credits-banner'
import { LoadPreviousButton } from './components/load-previous-button'
import { MessageWithAgents } from './components/message-with-agents'
Expand All @@ -26,6 +27,7 @@ import { useAgentValidation } from './hooks/use-agent-validation'
import { useAskUserBridge } from './hooks/use-ask-user-bridge'
import { authQueryKeys } from './hooks/use-auth-query'
import { useChatInput } from './hooks/use-chat-input'
import { useClaudeQuotaQuery } from './hooks/use-claude-quota-query'
import {
useChatKeyboard,
type ChatKeyboardHandlers,
Expand Down Expand Up @@ -73,6 +75,7 @@ import {
getStatusIndicatorState,
type AuthStatus,
} from './utils/status-indicator-state'
import { getClaudeOAuthStatus } from './utils/claude-oauth'
import { createPasteHandler } from './utils/strings'
import { computeInputLayoutMetrics } from './utils/text-layout'
import { createMarkdownPalette } from './utils/theme-system'
Expand Down Expand Up @@ -1360,6 +1363,15 @@ export const Chat = ({
isAskUserActive: askUserState !== null,
})
const hasStatusIndicatorContent = statusIndicatorState.kind !== 'idle'

const isClaudeOAuthActive = getClaudeOAuthStatus().connected

// Fetch Claude quota when OAuth is active
const { data: claudeQuota } = useClaudeQuotaQuery({
enabled: isClaudeOAuthActive,
refetchInterval: 60 * 1000, // Refetch every 60 seconds
})

const inputBoxTitle = useMemo(() => {
const segments: string[] = []

Expand All @@ -1380,6 +1392,9 @@ export const Chat = ({
!feedbackMode &&
(hasStatusIndicatorContent || shouldShowQueuePreview || !isAtBottom)

// Determine if Claude is actively streaming/waiting
const isClaudeActive = isStreaming || isWaitingForResponse

// Track mouse movement for ad activity (throttled)
const lastMouseActivityRef = useRef<number>(0)
const handleMouseActivity = useCallback(() => {
Expand Down Expand Up @@ -1541,6 +1556,12 @@ export const Chat = ({
cwd: getProjectRoot() ?? process.cwd(),
})}
/>

<BottomStatusLine
isClaudeConnected={isClaudeOAuthActive}
isClaudeActive={isClaudeActive}
claudeQuota={claudeQuota}
/>
</box>
</box>
)
Expand Down
10 changes: 10 additions & 0 deletions cli/src/commands/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,16 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
return { openPublishMode: true }
},
}),
defineCommand({
name: 'connect:claude',
aliases: ['claude'],
handler: (params) => {
// Enter connect:claude mode to show the OAuth banner
useChatStore.getState().setInputMode('connect:claude')
params.saveToHistory(params.inputValue.trim())
clearInput(params)
},
}),
]

export function findCommand(cmd: string): CommandDefinition | undefined {
Expand Down
18 changes: 18 additions & 0 deletions cli/src/commands/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
extractReferralCode,
normalizeReferralCode,
} from './router-utils'
import { handleClaudeAuthCode } from '../components/claude-connect-banner'
import { getProjectRoot } from '../project-files'
import { useChatStore } from '../state/chat-store'
import {
Expand Down Expand Up @@ -284,6 +285,23 @@ export async function routeUserPrompt(
return
}

// Handle connect:claude mode input (authorization code)
if (inputMode === 'connect:claude') {
const code = trimmed
if (code) {
const result = await handleClaudeAuthCode(code)
setMessages((prev) => [
...prev,
getUserMessage(trimmed),
getSystemMessage(result.message),
])
}
saveToHistory(trimmed)
setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
setInputMode('default')
return
}

// Handle referral mode input
if (inputMode === 'referral') {
// Validate the referral code (3-50 alphanumeric chars with optional dashes)
Expand Down
82 changes: 82 additions & 0 deletions cli/src/components/bottom-status-line.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react'

import { useTheme } from '../hooks/use-theme'

import { formatResetTime } from '../utils/time-format'

import type { ClaudeQuotaData } from '../hooks/use-claude-quota-query'

interface BottomStatusLineProps {
/** Whether Claude OAuth is connected */
isClaudeConnected: boolean
/** Whether Claude is actively being used (streaming/waiting) */
isClaudeActive: boolean
/** Quota data from Anthropic API */
claudeQuota?: ClaudeQuotaData | null
}

/**
* Bottom status line component - shows below the input box
* Currently displays Claude subscription status when connected
*/
export const BottomStatusLine: React.FC<BottomStatusLineProps> = ({
isClaudeConnected,
isClaudeActive,
claudeQuota,
}) => {
const theme = useTheme()

// Don't render if there's nothing to show
if (!isClaudeConnected) {
return null
}

// Use the more restrictive of the two quotas (5-hour window is usually the limiting factor)
const displayRemaining = claudeQuota
? Math.min(claudeQuota.fiveHourRemaining, claudeQuota.sevenDayRemaining)
: null

// Check if quota is exhausted (0%)
const isExhausted = displayRemaining !== null && displayRemaining <= 0

// Get the reset time for the limiting quota window
const resetTime = claudeQuota
? claudeQuota.fiveHourRemaining <= claudeQuota.sevenDayRemaining
? claudeQuota.fiveHourResetsAt
: claudeQuota.sevenDayResetsAt
: null

// Determine dot color: red if exhausted, green if active, muted otherwise
const dotColor = isExhausted
? theme.error
: isClaudeActive
? theme.success
: theme.muted

return (
<box
style={{
width: '100%',
flexDirection: 'row',
justifyContent: 'flex-end',
paddingRight: 1,
}}
>
<box
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 0,
}}
>
<text style={{ fg: dotColor }}>●</text>
<text style={{ fg: theme.muted }}> Claude subscription</text>
{isExhausted && resetTime ? (
<text style={{ fg: theme.muted }}>{` · resets in ${formatResetTime(resetTime)}`}</text>
) : displayRemaining !== null ? (
<text style={{ fg: theme.foreground }}>{` ${Math.round(displayRemaining)}%`}</text>
) : null}
</box>
</box>
)
}
Loading
Loading