From 862d1a9beca89f4c7951d3590410a60a0dc5efbc Mon Sep 17 00:00:00 2001 From: brandon chen <9735006+brandonkachen@users.noreply.github.com> Date: Fri, 24 Apr 2026 22:26:57 -0700 Subject: [PATCH 1/4] [codex] show local freebuff model availability (#542) Co-authored-by: James Grugett --- .../components/freebuff-model-selector.tsx | 51 ++--- common/src/__tests__/freebuff-models.test.ts | 50 +++++ common/src/constants/freebuff-models.ts | 178 +++++++++++++++++- 3 files changed, 249 insertions(+), 30 deletions(-) create mode 100644 common/src/__tests__/freebuff-models.test.ts diff --git a/cli/src/components/freebuff-model-selector.tsx b/cli/src/components/freebuff-model-selector.tsx index 0850a0bd73..b6e46faef0 100644 --- a/cli/src/components/freebuff-model-selector.tsx +++ b/cli/src/components/freebuff-model-selector.tsx @@ -5,9 +5,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react' import { Button } from './button' import { FALLBACK_FREEBUFF_MODEL_ID, - FREEBUFF_DEPLOYMENT_HOURS_LABEL, FREEBUFF_GLM_MODEL_ID, FREEBUFF_MODELS, + getFreebuffDeploymentAvailabilityLabel, isFreebuffModelAvailable, } from '@codebuff/common/constants/freebuff-models' @@ -48,6 +48,10 @@ export const FreebuffModelSelector: React.FC = () => { const setSelectedModel = useFreebuffModelStore((s) => s.setSelectedModel) const session = useFreebuffSessionStore((s) => s.session) const now = useNow(60_000) + const deploymentAvailabilityLabel = useMemo( + () => getFreebuffDeploymentAvailabilityLabel(new Date(now)), + [now], + ) const [pending, setPending] = useState(null) const [hoveredId, setHoveredId] = useState(null) // Keyboard cursor — separate from the actually-selected model so that @@ -96,7 +100,7 @@ export const FreebuffModelSelector: React.FC = () => { out[id] = id === session.model ? Math.max(0, session.position - 1) - : depths[id] ?? 0 + : (depths[id] ?? 0) } return out } @@ -127,7 +131,7 @@ export const FreebuffModelSelector: React.FC = () => { 3 /* " · " */ + model.tagline.length + (model.availability === 'deployment_hours' - ? 3 + FREEBUFF_DEPLOYMENT_HOURS_LABEL.length + ? 3 + deploymentAvailabilityLabel.length : 0) + 2 /* " " */ + hintWidth @@ -135,13 +139,12 @@ export const FreebuffModelSelector: React.FC = () => { }, 0) // Leave a small margin for the surrounding padding on the waiting-room screen. return total > terminalWidth - 4 - }, [hintWidth, terminalWidth]) + }, [deploymentAvailabilityLabel, hintWidth, terminalWidth]) // "Already committed to this model" — only when the server has us queued // on it. On the landing screen (status 'none'), nothing is committed yet, // so picking the focused model is always a real action (first join). - const committedModelId = - session?.status === 'queued' ? session.model : null + const committedModelId = session?.status === 'queued' ? session.model : null const pick = useCallback( (modelId: string) => { @@ -166,7 +169,8 @@ export const FreebuffModelSelector: React.FC = () => { name === 'right' || name === 'down' || (name === 'tab' && !key.shift) const isBackward = name === 'left' || name === 'up' || (name === 'tab' && key.shift) - const isCommit = name === 'return' || name === 'enter' || name === 'space' + const isCommit = + name === 'return' || name === 'enter' || name === 'space' if (!isForward && !isBackward && !isCommit) return if (isCommit) { if ( @@ -222,19 +226,20 @@ export const FreebuffModelSelector: React.FC = () => { const isAvailable = isFreebuffModelAvailable(model.id, new Date(now)) const indicator = isSelected ? '●' : '○' const indicatorColor = isSelected ? theme.primary : theme.muted - const labelColor = isSelected && isAvailable ? theme.foreground : theme.muted + const labelColor = + isSelected && isAvailable ? theme.foreground : theme.muted // Clickable whenever picking would actually do something — i.e. // anything except re-picking the queue we're already in. - const interactable = !pending && isAvailable && model.id !== committedModelId + const interactable = + !pending && isAvailable && model.id !== committedModelId const ahead = aheadByModel?.[model.id] - const hint = - !isAvailable - ? 'Closed' - : ahead === undefined - ? '' - : ahead === 0 - ? 'No wait' - : `${ahead} ahead` + const hint = !isAvailable + ? 'Closed' + : ahead === undefined + ? '' + : ahead === 0 + ? 'No wait' + : `${ahead} ahead` const borderColor = isSelected ? theme.primary @@ -250,7 +255,9 @@ export const FreebuffModelSelector: React.FC = () => { if (isAvailable) pick(model.id) }} onMouseOver={() => interactable && setHoveredId(model.id)} - onMouseOut={() => setHoveredId((curr) => (curr === model.id ? null : curr))} + onMouseOut={() => + setHoveredId((curr) => (curr === model.id ? null : curr)) + } style={{ borderStyle: 'single', borderColor, @@ -263,15 +270,17 @@ export const FreebuffModelSelector: React.FC = () => { {indicator} {model.displayName} · {model.tagline} {model.availability === 'deployment_hours' && ( - · {FREEBUFF_DEPLOYMENT_HOURS_LABEL} + · {deploymentAvailabilityLabel} )} - {hint.padEnd(hintWidth)} + {hint.padEnd(hintWidth)} ) diff --git a/common/src/__tests__/freebuff-models.test.ts b/common/src/__tests__/freebuff-models.test.ts new file mode 100644 index 0000000000..c4ff0bb3e9 --- /dev/null +++ b/common/src/__tests__/freebuff-models.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from 'bun:test' + +import { + getFreebuffDeploymentAvailabilityLabel, + isFreebuffDeploymentHours, +} from '../constants/freebuff-models' + +describe('freebuff model availability', () => { + test('formats the close time in the user local timezone while deployment is open', () => { + expect( + getFreebuffDeploymentAvailabilityLabel(new Date('2026-01-05T18:00:00Z'), { + locale: 'en-US', + timeZone: 'America/Los_Angeles', + }), + ).toBe('until 5:00 PM local') + }) + + test('formats the next open time in the user local timezone while deployment is closed', () => { + expect( + getFreebuffDeploymentAvailabilityLabel(new Date('2026-01-05T12:00:00Z'), { + locale: 'en-US', + timeZone: 'America/Los_Angeles', + }), + ).toBe('opens 6:00 AM local') + }) + + test('includes the weekday when the next opening is on a later local day', () => { + expect( + getFreebuffDeploymentAvailabilityLabel(new Date('2026-01-10T20:00:00Z'), { + locale: 'en-US', + timeZone: 'America/Los_Angeles', + }), + ).toBe('opens Mon 6:00 AM local') + }) + + test('tracks deployment hours correctly across the open and close boundaries', () => { + expect(isFreebuffDeploymentHours(new Date('2026-01-05T13:59:00Z'))).toBe( + false, + ) + expect(isFreebuffDeploymentHours(new Date('2026-01-05T14:00:00Z'))).toBe( + true, + ) + expect(isFreebuffDeploymentHours(new Date('2026-01-06T00:59:00Z'))).toBe( + true, + ) + expect(isFreebuffDeploymentHours(new Date('2026-01-06T01:00:00Z'))).toBe( + false, + ) + }) +}) diff --git a/common/src/constants/freebuff-models.ts b/common/src/constants/freebuff-models.ts index 2e1ef8d8ea..a4ddd6f412 100644 --- a/common/src/constants/freebuff-models.ts +++ b/common/src/constants/freebuff-models.ts @@ -17,9 +17,28 @@ export interface FreebuffModelOption { availability: 'always' | 'deployment_hours' } +/** Server-facing fallback copy for APIs and provider errors that can't know + * the caller's local timezone. The CLI should render + * `getFreebuffDeploymentAvailabilityLabel()` instead. */ export const FREEBUFF_DEPLOYMENT_HOURS_LABEL = '9am ET-5pm PT' export const FREEBUFF_GLM_MODEL_ID = 'z-ai/glm-5.1' export const FREEBUFF_MINIMAX_MODEL_ID = 'minimax/minimax-m2.7' +const FREEBUFF_EASTERN_TIMEZONE = 'America/New_York' +const FREEBUFF_PACIFIC_TIMEZONE = 'America/Los_Angeles' + +interface ZonedDateParts { + year: number + month: number + day: number + weekday: string + hour: number + minute: number +} + +interface LocalTimeFormatOptions { + locale?: string + timeZone?: string +} export const FREEBUFF_MODELS = [ { @@ -71,31 +90,172 @@ export function getFreebuffModel(id: string): FreebuffModelOption { ) } -function getZonedParts( - date: Date, - timeZone: string, -): { weekday: string; minutes: number } { +function getZonedParts(date: Date, timeZone: string): ZonedDateParts { const parts = new Intl.DateTimeFormat('en-US', { timeZone, + year: 'numeric', + month: '2-digit', + day: '2-digit', weekday: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23', }).formatToParts(date) - const value = (type: string) => parts.find((part) => part.type === type)?.value + const value = (type: string) => + parts.find((part) => part.type === type)?.value + const year = Number(value('year') ?? 0) + const month = Number(value('month') ?? 1) + const day = Number(value('day') ?? 1) const hour = Number(value('hour') ?? 0) const minute = Number(value('minute') ?? 0) return { + year, + month, + day, weekday: value('weekday') ?? '', - minutes: hour * 60 + minute, + hour, + minute, + } +} + +function addDaysToYmd( + year: number, + month: number, + day: number, + days: number, +): Pick { + const next = new Date(Date.UTC(year, month - 1, day)) + next.setUTCDate(next.getUTCDate() + days) + return { + year: next.getUTCFullYear(), + month: next.getUTCMonth() + 1, + day: next.getUTCDate(), + } +} + +function getUtcForZonedTime( + parts: Pick, + timeZone: string, + hour: number, + minute: number, +): Date { + let guess = new Date( + Date.UTC(parts.year, parts.month - 1, parts.day, hour, minute), + ) + + for (let i = 0; i < 3; i++) { + const actual = getZonedParts(guess, timeZone) + const desiredUtc = Date.UTC( + parts.year, + parts.month - 1, + parts.day, + hour, + minute, + ) + const actualUtc = Date.UTC( + actual.year, + actual.month - 1, + actual.day, + actual.hour, + actual.minute, + ) + guess = new Date(guess.getTime() + (desiredUtc - actualUtc)) + } + + return guess +} + +function isWeekend( + parts: Pick, +): boolean { + const weekday = getWeekdayIndex(parts) + return weekday === 0 || weekday === 6 +} + +function getWeekdayIndex( + parts: Pick, +): number { + return new Date(Date.UTC(parts.year, parts.month - 1, parts.day)).getUTCDay() +} + +function getNextFreebuffDeploymentStart(now: Date): Date { + const easternNow = getZonedParts(now, FREEBUFF_EASTERN_TIMEZONE) + const weekday = getWeekdayIndex(easternNow) + const isBeforeTodayOpen = easternNow.hour < 9 + + const offset = + weekday === 6 + ? 2 + : weekday === 0 + ? 1 + : isBeforeTodayOpen + ? 0 + : weekday === 5 + ? 3 + : 1 + + return getUtcForZonedTime( + addDaysToYmd(easternNow.year, easternNow.month, easternNow.day, offset), + FREEBUFF_EASTERN_TIMEZONE, + 9, + 0, + ) +} + +function getCurrentFreebuffDeploymentEnd(now: Date): Date { + const pacificNow = getZonedParts(now, FREEBUFF_PACIFIC_TIMEZONE) + return getUtcForZonedTime(pacificNow, FREEBUFF_PACIFIC_TIMEZONE, 17, 0) +} + +function isSameLocalDay(left: Date, right: Date, timeZone?: string): boolean { + const formatter = new Intl.DateTimeFormat('en-CA', { + timeZone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + return formatter.format(left) === formatter.format(right) +} + +function formatLocalTime( + date: Date, + referenceNow: Date, + options: LocalTimeFormatOptions = {}, +): string { + const shouldShowWeekday = !isSameLocalDay( + date, + referenceNow, + options.timeZone, + ) + return new Intl.DateTimeFormat(options.locale, { + timeZone: options.timeZone, + weekday: shouldShowWeekday ? 'short' : undefined, + hour: 'numeric', + minute: '2-digit', + }).format(date) +} + +export function getFreebuffDeploymentAvailabilityLabel( + now: Date = new Date(), + options: LocalTimeFormatOptions = {}, +): string { + if (isFreebuffDeploymentHours(now)) { + const closesAt = getCurrentFreebuffDeploymentEnd(now) + return `until ${formatLocalTime(closesAt, now, options)} local` } + + const opensAt = getNextFreebuffDeploymentStart(now) + return `opens ${formatLocalTime(opensAt, now, options)} local` } export function isFreebuffDeploymentHours(now: Date = new Date()): boolean { - const eastern = getZonedParts(now, 'America/New_York') - const pacific = getZonedParts(now, 'America/Los_Angeles') + const eastern = getZonedParts(now, FREEBUFF_EASTERN_TIMEZONE) + const pacific = getZonedParts(now, FREEBUFF_PACIFIC_TIMEZONE) if (eastern.weekday === 'Sat' || eastern.weekday === 'Sun') return false - return eastern.minutes >= 9 * 60 && pacific.minutes < 17 * 60 + return ( + eastern.hour * 60 + eastern.minute >= 9 * 60 && + pacific.hour * 60 + pacific.minute < 17 * 60 + ) } export function isFreebuffModelAvailable( From 2bd8f2a6775cb4401afadd0f04dae3087ff63938 Mon Sep 17 00:00:00 2001 From: brandon chen <9735006+brandonkachen@users.noreply.github.com> Date: Fri, 24 Apr 2026 22:51:00 -0700 Subject: [PATCH 2/4] [codex] Raise MiniMax instant admit threshold (#544) Co-authored-by: James Grugett --- web/src/server/free-session/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/server/free-session/config.ts b/web/src/server/free-session/config.ts index 85bba7fa6f..10071b35fc 100644 --- a/web/src/server/free-session/config.ts +++ b/web/src/server/free-session/config.ts @@ -49,7 +49,7 @@ export function getSessionGraceMs(): number { */ const INSTANT_ADMIT_CAPACITY: Record = { 'z-ai/glm-5.1': 50, - 'minimax/minimax-m2.7': 200, + 'minimax/minimax-m2.7': 1000, } export function getInstantAdmitCapacity(id: string): number { From e1529ba83afdd56a6ddd6293efc9c46b0a460bca Mon Sep 17 00:00:00 2001 From: brandon chen <9735006+brandonkachen@users.noreply.github.com> Date: Fri, 24 Apr 2026 23:44:20 -0700 Subject: [PATCH 3/4] [codex] Fix freebuff model picker enter (#545) Co-authored-by: James Grugett --- .../components/freebuff-model-selector.tsx | 43 +++++---- cli/src/components/waiting-room-screen.tsx | 2 +- cli/src/hooks/use-freebuff-session.ts | 11 ++- .../freebuff-model-navigation.test.ts | 93 +++++++++++++++++++ cli/src/utils/freebuff-model-navigation.ts | 37 ++++++++ 5 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 cli/src/utils/__tests__/freebuff-model-navigation.test.ts create mode 100644 cli/src/utils/freebuff-model-navigation.ts diff --git a/cli/src/components/freebuff-model-selector.tsx b/cli/src/components/freebuff-model-selector.tsx index b6e46faef0..a453a15389 100644 --- a/cli/src/components/freebuff-model-selector.tsx +++ b/cli/src/components/freebuff-model-selector.tsx @@ -17,6 +17,10 @@ import { useFreebuffModelStore } from '../state/freebuff-model-store' import { useFreebuffSessionStore } from '../state/freebuff-session-store' import { useTerminalDimensions } from '../hooks/use-terminal-dimensions' import { useTheme } from '../hooks/use-theme' +import { + nextSelectableFreebuffModelId, + resolveFreebuffModelCommitTarget, +} from '../utils/freebuff-model-navigation' import type { KeyEvent } from '@opentui/core' @@ -173,30 +177,32 @@ export const FreebuffModelSelector: React.FC = () => { name === 'return' || name === 'enter' || name === 'space' if (!isForward && !isBackward && !isCommit) return if (isCommit) { - if ( - focusedId !== committedModelId && - isFreebuffModelAvailable(focusedId, new Date(now)) - ) { + const targetId = resolveFreebuffModelCommitTarget({ + focusedId, + selectedId: selectedModel, + committedId: committedModelId, + isSelectable: (modelId) => + isFreebuffModelAvailable(modelId, new Date(now)), + }) + if (targetId) { key.preventDefault?.() - pick(focusedId) + pick(targetId) } return } - const currentIdx = FREEBUFF_MODEL_SELECTOR_MODELS.findIndex( - (m) => m.id === focusedId, - ) - if (currentIdx === -1) return - const len = FREEBUFF_MODEL_SELECTOR_MODELS.length - const nextIdx = isForward - ? (currentIdx + 1) % len - : (currentIdx - 1 + len) % len - const target = FREEBUFF_MODEL_SELECTOR_MODELS[nextIdx] - if (target) { + const targetId = nextSelectableFreebuffModelId({ + modelIds: FREEBUFF_MODEL_SELECTOR_MODELS.map((model) => model.id), + focusedId, + direction: isForward ? 'forward' : 'backward', + isSelectable: (modelId) => + isFreebuffModelAvailable(modelId, new Date(now)), + }) + if (targetId) { key.preventDefault?.() - setFocusedId(target.id) + setFocusedId(targetId) } }, - [pending, pick, focusedId, committedModelId, now], + [pending, pick, focusedId, selectedModel, committedModelId, now], ), ) @@ -219,7 +225,8 @@ export const FreebuffModelSelector: React.FC = () => { // 'Selected' means the dot is filled and the label is bold. On the // landing screen ('none') this tracks the pre-focused pick; on the // queued screen it tracks the model the server has us on. Either - // way, selectedModel reflects the intent of "what Enter commits to." + // way, selectedModel is the safe fallback if focus ever lands on a + // closed row (for example when deployment hours change). const isSelected = model.id === selectedModel const isHovered = hoveredId === model.id const isFocused = focusedId === model.id && !isSelected diff --git a/cli/src/components/waiting-room-screen.tsx b/cli/src/components/waiting-room-screen.tsx index f2a09022eb..2bbee6c719 100644 --- a/cli/src/components/waiting-room-screen.tsx +++ b/cli/src/components/waiting-room-screen.tsx @@ -173,7 +173,7 @@ export const WaitingRoomScreen: React.FC = ({ maxWidth: contentMaxWidth, }} > - {error && !session && ( + {error && (!session || session.status === 'none') && ( ⚠ {error} diff --git a/cli/src/hooks/use-freebuff-session.ts b/cli/src/hooks/use-freebuff-session.ts index b7a91eb1ee..19f21ecaa2 100644 --- a/cli/src/hooks/use-freebuff-session.ts +++ b/cli/src/hooks/use-freebuff-session.ts @@ -376,6 +376,7 @@ export function useFreebuffSession(): UseFreebuffSessionResult { let abortController = new AbortController() let timer: ReturnType | null = null let previousStatus: FreebuffSessionResponse['status'] | null = null + let restartGeneration = 0 // Method for the NEXT tick. GET is read-only; POST claims/rotates a seat. // Startup is GET (probe before committing). After any POST completes we // flip back to GET. refresh() sets it to 'POST' for explicit join/rejoin; @@ -489,6 +490,7 @@ export function useFreebuffSession(): UseFreebuffSessionResult { controller = { restart: async (mode) => { + const generation = ++restartGeneration clearTimer() // Abort any in-flight fetch so it can't race us and overwrite state. abortController.abort() @@ -498,6 +500,7 @@ export function useFreebuffSession(): UseFreebuffSessionResult { // doesn't bounce a 'landing' restart straight back to 'ended'. previousStatus = null if (mode === 'landing') { + nextMethod = 'GET' // Land on the picker immediately. We can't go through the normal // tick/apply path because a server-side row that hasn't been // swept yet would trip the startup-takeover branch into an @@ -511,7 +514,13 @@ export function useFreebuffSession(): UseFreebuffSessionResult { const fetchController = abortController callSession('GET', token, { signal: fetchController.signal }) .then((response) => { - if (cancelled || fetchController.signal.aborted) return + if ( + cancelled || + fetchController.signal.aborted || + generation !== restartGeneration + ) { + return + } const depths = response.status === 'none' || response.status === 'queued' ? response.queueDepthByModel diff --git a/cli/src/utils/__tests__/freebuff-model-navigation.test.ts b/cli/src/utils/__tests__/freebuff-model-navigation.test.ts new file mode 100644 index 0000000000..4723245bad --- /dev/null +++ b/cli/src/utils/__tests__/freebuff-model-navigation.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, test } from 'bun:test' + +import { + nextSelectableFreebuffModelId, + resolveFreebuffModelCommitTarget, +} from '../freebuff-model-navigation' + +describe('nextSelectableFreebuffModelId', () => { + test('skips unavailable models when moving forward', () => { + const modelIds = ['glm', 'minimax'] + + expect( + nextSelectableFreebuffModelId({ + modelIds, + focusedId: 'minimax', + direction: 'forward', + isSelectable: (id) => id !== 'glm', + }), + ).toBe('minimax') + }) + + test('skips unavailable models when moving backward', () => { + const modelIds = ['glm', 'minimax'] + + expect( + nextSelectableFreebuffModelId({ + modelIds, + focusedId: 'minimax', + direction: 'backward', + isSelectable: (id) => id !== 'glm', + }), + ).toBe('minimax') + }) + + test('moves to the next available model when more than one is selectable', () => { + const modelIds = ['glm', 'minimax', 'other'] + + expect( + nextSelectableFreebuffModelId({ + modelIds, + focusedId: 'minimax', + direction: 'forward', + isSelectable: (id) => id !== 'glm', + }), + ).toBe('other') + }) + + test('returns null when no selectable model exists', () => { + expect( + nextSelectableFreebuffModelId({ + modelIds: ['glm'], + focusedId: 'glm', + direction: 'forward', + isSelectable: () => false, + }), + ).toBeNull() + }) +}) + +describe('resolveFreebuffModelCommitTarget', () => { + test('falls back to the selected model when focus is on a closed model', () => { + expect( + resolveFreebuffModelCommitTarget({ + focusedId: 'glm', + selectedId: 'minimax', + committedId: null, + isSelectable: (id) => id !== 'glm', + }), + ).toBe('minimax') + }) + + test('commits the focused model when it is selectable', () => { + expect( + resolveFreebuffModelCommitTarget({ + focusedId: 'minimax', + selectedId: 'glm', + committedId: null, + isSelectable: (id) => id === 'minimax', + }), + ).toBe('minimax') + }) + + test('returns null when the target is already committed', () => { + expect( + resolveFreebuffModelCommitTarget({ + focusedId: 'minimax', + selectedId: 'minimax', + committedId: 'minimax', + isSelectable: () => true, + }), + ).toBeNull() + }) +}) diff --git a/cli/src/utils/freebuff-model-navigation.ts b/cli/src/utils/freebuff-model-navigation.ts new file mode 100644 index 0000000000..eef067d5cf --- /dev/null +++ b/cli/src/utils/freebuff-model-navigation.ts @@ -0,0 +1,37 @@ +export function nextSelectableFreebuffModelId(params: { + modelIds: readonly string[] + focusedId: string + direction: 'forward' | 'backward' + isSelectable: (modelId: string) => boolean +}): string | null { + const { modelIds, focusedId, direction, isSelectable } = params + if (modelIds.length === 0) return null + + const currentIdx = modelIds.indexOf(focusedId) + if (currentIdx === -1) return null + + const step = direction === 'forward' ? 1 : -1 + // Include a full wrap back to the current item so arrows stay on the same + // selectable model when every peer is unavailable. + for (let offset = 1; offset <= modelIds.length; offset++) { + const idx = + (currentIdx + step * offset + modelIds.length) % modelIds.length + const candidate = modelIds[idx] + if (isSelectable(candidate)) return candidate + } + + return null +} + +export function resolveFreebuffModelCommitTarget(params: { + focusedId: string + selectedId: string + committedId: string | null + isSelectable: (modelId: string) => boolean +}): string | null { + const { focusedId, selectedId, committedId, isSelectable } = params + const targetId = isSelectable(focusedId) ? focusedId : selectedId + + if (!isSelectable(targetId) || targetId === committedId) return null + return targetId +} From bf6e29cdb343176412ff8c6820bc134bffb4d816 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Apr 2026 06:53:56 +0000 Subject: [PATCH 4/4] Bump Freebuff version to 0.0.49 --- freebuff/cli/release/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freebuff/cli/release/package.json b/freebuff/cli/release/package.json index a597e0852c..26eae19860 100644 --- a/freebuff/cli/release/package.json +++ b/freebuff/cli/release/package.json @@ -1,6 +1,6 @@ { "name": "freebuff", - "version": "0.0.48", + "version": "0.0.49", "description": "The world's strongest free coding agent", "license": "MIT", "bin": {