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
38 changes: 38 additions & 0 deletions .github/workflows/bot-sweep.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Freebuff Bot Sweep

# Hourly dry-run sweep over active freebuff sessions. Calls the
# /api/admin/bot-sweep endpoint, which emails james@codebuff.com with a
# ranked list of suspects. No bans are issued — review and run
# scripts/ban-freebuff-bots.ts manually.

on:
schedule:
- cron: '0 * * * *'
workflow_dispatch:

jobs:
sweep:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Trigger bot-sweep
env:
BOT_SWEEP_SECRET: ${{ secrets.BOT_SWEEP_SECRET }}
BOT_SWEEP_URL: ${{ vars.BOT_SWEEP_URL || 'https://www.codebuff.com/api/admin/bot-sweep' }}
run: |
set -euo pipefail
if [ -z "$BOT_SWEEP_SECRET" ]; then
echo "BOT_SWEEP_SECRET is not set — skipping."
exit 0
fi
status=$(curl -sS -o /tmp/resp.json -w '%{http_code}' \
-X POST "$BOT_SWEEP_URL" \
-H "Authorization: Bearer $BOT_SWEEP_SECRET" \
-H "Content-Type: application/json" \
--max-time 120)
echo "HTTP $status"
cat /tmp/resp.json
echo
if [ "$status" != "200" ]; then
exit 1
fi
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"commander": "^14.0.1",
"immer": "^10.1.3",
"jimp": "^1.6.0",
"node-machine-id": "^1.1.12",
"open": "^10.1.0",
"pino": "9.4.0",
"posthog-node": "^5.8.0",
Expand Down
4 changes: 2 additions & 2 deletions cli/src/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { reportActivity } from './utils/activity-tracker'
import { trackEvent } from './utils/analytics'
import { showClipboardMessage } from './utils/clipboard'
import { readClipboardImage } from './utils/clipboard-image'
import { endAndRejoinFreebuffSession } from './hooks/use-freebuff-session'
import { returnToFreebuffLanding } from './hooks/use-freebuff-session'
import { END_SESSION_MESSAGE, IS_FREEBUFF } from './utils/constants'
import { getSystemMessage } from './utils/message-history'
import { getInputModeConfig } from './utils/input-modes'
Expand Down Expand Up @@ -1460,7 +1460,7 @@ export const Chat = ({
...prev,
getSystemMessage(END_SESSION_MESSAGE),
])
endAndRejoinFreebuffSession().catch(() => {})
returnToFreebuffLanding({ resetChat: true }).catch(() => {})
}}
freebuffSession={freebuffSession}
/>
Expand Down
11 changes: 6 additions & 5 deletions cli/src/commands/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { handleInitializationFlowLocally } from './init'
import { buildInterviewPrompt, buildPlanPrompt, buildReviewPromptFromArgs } from './prompt-builders'
import { runBashCommand } from './router'
import { handleUsageCommand } from './usage'
import { endAndRejoinFreebuffSession } from '../hooks/use-freebuff-session'
import { returnToFreebuffLanding } from '../hooks/use-freebuff-session'
import { useThemeStore } from '../hooks/use-theme'
import { WEBSITE_URL } from '../login/constants'
import { useChatStore } from '../state/chat-store'
Expand Down Expand Up @@ -613,9 +613,10 @@ const ALL_COMMANDS: CommandDefinition[] = [
clearInput(params)
},
}),
// /end-session (freebuff-only) — end the active session early and re-queue. The
// hook flips status from 'active' → 'queued', which unmounts <Chat> and
// mounts <WaitingRoomScreen>, where the user can pick a different model.
// /end-session (freebuff-only) — end the active session early and drop back
// to the model picker. The hook flips status to 'none', which unmounts
// <Chat> and mounts <WaitingRoomScreen> on the landing view, where the
// user picks a model and hits Enter to rejoin the queue.
defineCommand({
name: 'end-session',
handler: (params) => {
Expand All @@ -626,7 +627,7 @@ const ALL_COMMANDS: CommandDefinition[] = [
])
params.saveToHistory(params.inputValue.trim())
clearInput(params)
endAndRejoinFreebuffSession().catch(() => {
returnToFreebuffLanding({ resetChat: true }).catch(() => {
// The hook surfaces poll errors via the session store; nothing to do
// here beyond letting the chat history reflect the attempt.
})
Expand Down
8 changes: 6 additions & 2 deletions cli/src/components/freebuff-model-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,14 @@ export const FreebuffModelSelector: React.FC = () => {
// subtract. In-queue ('queued'): for the user's queue, "ahead" is
// `position - 1` (themselves don't count); for every other queue, switching
// would land them at the back, so it's that queue's full depth. Null before
// any snapshot so the UI doesn't flash misleading zeros.
// any snapshot so the UI doesn't flash misleading zeros — in particular,
// landing mode after a session ends initially sets status='none' with no
// queueDepthByModel; returning null here keeps the hint blank until the
// fetch lands, instead of showing "No wait" on every row.
const aheadByModel = useMemo<Record<string, number> | null>(() => {
if (session?.status === 'none') {
const depths = session.queueDepthByModel ?? {}
if (!session.queueDepthByModel) return null
const depths = session.queueDepthByModel
const out: Record<string, number> = {}
for (const { id } of FREEBUFF_MODELS) out[id] = depths[id] ?? 0
return out
Expand Down
51 changes: 0 additions & 51 deletions cli/src/components/login-modal-utils.ts

This file was deleted.

21 changes: 11 additions & 10 deletions cli/src/components/login-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,11 @@ import { useLoginPolling } from '../hooks/use-login-polling'
import { useLogo } from '../hooks/use-logo'
import { useSheenAnimation } from '../hooks/use-sheen-animation'
import { useTheme } from '../hooks/use-theme'
import {
formatUrl,
generateFingerprintId,
calculateResponsiveLayout,
} from '../login/utils'
import { formatUrl, calculateResponsiveLayout } from '../login/utils'
import { useLoginStore } from '../state/login-store'
import { IS_FREEBUFF } from '../utils/constants'
import { copyTextToClipboard, isRemoteSession } from '../utils/clipboard'
import { getFingerprintId } from '../utils/fingerprint'
import { logger } from '../utils/logger'
import { getLogoBlockColor, getLogoAccentColor } from '../utils/theme-system'

Expand All @@ -40,6 +37,7 @@ export const LoginModal = ({
loginUrl,
loading,
error,
fingerprintId,
fingerprintHash,
expiresAt,
isWaitingForEnter,
Expand All @@ -49,6 +47,7 @@ export const LoginModal = ({
setLoginUrl,
setLoading,
setError,
setFingerprintId,
setFingerprintHash,
setExpiresAt,
setIsWaitingForEnter,
Expand All @@ -59,9 +58,6 @@ export const LoginModal = ({
setHasClickedLink,
} = useLoginStore()

// Generate fingerprint ID (only once on mount)
const [fingerprintId] = useState(() => generateFingerprintId())

// Track hover state for copy button
const [isCopyButtonHovered, setIsCopyButtonHovered] = useState(false)

Expand Down Expand Up @@ -111,17 +107,22 @@ export const LoginModal = ({
setLoading(true)
setError(null)

fetchLoginUrlMutation.mutate(fingerprintId, {
// Near-instant after the prefetch in initializeApp; falls back to the
// sync legacy fingerprint if hardware hashing fails.
const id = await getFingerprintId()
setFingerprintId(id)

fetchLoginUrlMutation.mutate(id, {
onSettled: () => {
setLoading(false)
},
})
}, [
fingerprintId,
loading,
hasOpenedBrowser,
setLoading,
setError,
setFingerprintId,
fetchLoginUrlMutation,
])

Expand Down
14 changes: 9 additions & 5 deletions cli/src/components/session-ended-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useKeyboard } from '@opentui/react'
import React, { useCallback, useState } from 'react'

import { Button } from './button'
import { refreshFreebuffSession } from '../hooks/use-freebuff-session'
import { returnToFreebuffLanding } from '../hooks/use-freebuff-session'
import { useTheme } from '../hooks/use-theme'
import { BORDER_CHARS } from '../utils/ui-constants'

Expand Down Expand Up @@ -35,10 +35,14 @@ export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({
const rejoin = useCallback(() => {
if (!canRejoin) return
setRejoining(true)
// Once the POST lands, the hook flips status to 'queued' and app.tsx
// swaps us into <WaitingRoomScreen>, unmounting this banner. No need to
// clear `rejoining` on success — the component will be gone.
refreshFreebuffSession({ resetChat: true }).catch(() => setRejoining(false))
// Drop back to the landing picker (status: 'none') so the user picks a
// model and hits Enter again to commit, instead of being silently
// re-queued. app.tsx swaps us into <WaitingRoomScreen> on the
// transition, unmounting this banner — no need to clear `rejoining` on
// success.
returnToFreebuffLanding({ resetChat: true }).catch(() =>
setRejoining(false),
)
}, [canRejoin])

useKeyboard(
Expand Down
7 changes: 6 additions & 1 deletion cli/src/components/tools/suggest-followups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { defineToolComponent } from './types'
import { useTerminalDimensions } from '../../hooks/use-terminal-dimensions'
import { useTheme } from '../../hooks/use-theme'
import { getLatestFollowupToolCallId, useChatStore } from '../../state/chat-store'
import { useFreebuffSessionStore } from '../../state/freebuff-session-store'
import { IS_FREEBUFF } from '../../utils/constants'
import { Button } from '../button'

import type { ToolRenderConfig } from './types'
Expand Down Expand Up @@ -223,6 +225,9 @@ const SuggestFollowupsItem = ({
}: SuggestFollowupsItemProps) => {
const theme = useTheme()
const inputFocused = useChatStore((state) => state.inputFocused)
const isFreebuffSessionOver = useFreebuffSessionStore(
(state) => IS_FREEBUFF && state.session?.status === 'ended',
)
const setSuggestedFollowups = useChatStore(
(state) => state.setSuggestedFollowups,
)
Expand Down Expand Up @@ -305,7 +310,7 @@ const SuggestFollowupsItem = ({
isHovered={hoveredIndex === index}
onSendFollowup={onSendFollowup}
onHover={setHoveredIndex}
disabled={!inputFocused}
disabled={!inputFocused || isFreebuffSessionOver}
labelColumnWidth={labelColumnWidth}
/>
))}
Expand Down
Loading
Loading