Skip to content
Open
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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ concurrency:
jobs:
check:
name: Checks (Linux)
runs-on: ubuntu-latest
# Ubuntu 26.04's native-addon runtime crashes Bun when sharp follows applet builds.
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout repository
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

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

46 changes: 46 additions & 0 deletions client/components/shared/ultrafast-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Existing Ultrafast glyph from bb's Codex provider, adapted to the Tabler component API.
* Source: https://github.com/get-bb/bb/blob/main/plugins/provider-codex/icons/ultrafast.svg
*
* MIT License
*
* Copyright (c) 2026 Michael Yong
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { createReactComponent } from '@tabler/icons-react'

export const IconUltrafast = createReactComponent('outline', 'ultrafast', 'Ultrafast', [
[
'path',
{
d: 'M11.983 12.593L11.579 12.593C10.273 12.593 9.62 12.593 9.342 12.162C9.064 11.732 9.329 11.132 9.859 9.933L11.453 6.327C11.936 5.236 12.177 4.691 12.644 4.386C13.112 4.08 13.706 4.08 14.893 4.08L16.731 4.08C18.174 4.08 18.895 4.08 19.167 4.551C19.439 5.022 19.081 5.652 18.365 6.91L17.422 8.57C17.066 9.195 16.889 9.508 16.891 9.764C16.894 10.097 17.071 10.404 17.357 10.572C17.577 10.701 17.936 10.701 18.653 10.701C19.559 10.701 20.012 10.701 20.248 10.858C20.555 11.062 20.715 11.426 20.659 11.792C20.616 12.073 20.311 12.41 19.701 13.084L14.83 18.47C13.873 19.528 13.395 20.057 13.074 19.89C12.753 19.722 12.907 19.024 13.215 17.629L13.82 14.895C14.055 13.832 14.172 13.301 13.89 12.947C13.607 12.593 13.066 12.593 11.983 12.593Z',
fill: 'currentColor',
key: 'bolt-0'
}
],
[
'path',
{
d: 'M6.115 12.593L6.04 11.471L6.31 10.166L8.96 4.08L7.171 4.171L6.644 4.386L6.281 4.722L5.728 5.714L3.355 11.151L3.244 11.584L3.247 11.931L3.379 12.214L3.625 12.411L4.371 12.57ZM7.788 15.039L6.919 19.124L6.932 19.714L7.158 19.917L7.459 19.831L7.86 19.505L10.051 17.121L10.348 15.774L9.117 15.609L8.359 15.351Z',
fill: 'currentColor',
key: 'bolt-1'
}
]
])
25 changes: 25 additions & 0 deletions client/features/chat/chat-send.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,31 @@ describe('Codex model selection', () => {
]
}

test('sends Ultrafast only to a model that advertises it', () => {
const data = {
...catalog,
models: catalog.models.map(model => ({ ...model, supportsUltrafastMode: true }))
}
expect(resolveChatRunOptions(data, 'gpt-5.6-sol', 'low', 'ultrafast').fastMode).toBe(
'ultrafast'
)
expect(resolveChatRunOptions(catalog, 'gpt-5.6-sol', 'low', 'ultrafast').fastMode).toBe(true)
expect(resolveChatRunOptions(catalog, 'gpt-5.6-luna', 'low', 'ultrafast').fastMode).toBe(false)
})

test('sends the displayed Codex default and clears inherited tiers on unsupported models', () => {
const data = {
...catalog,
models: catalog.models.map(model =>
model.value === 'gpt-5.6-sol'
? { ...model, supportsUltrafastMode: true, defaultFastMode: 'ultrafast' as const }
: model
)
}
expect(resolveChatRunOptions(data, 'gpt-5.6-sol', 'low').fastMode).toBe('ultrafast')
expect(resolveChatRunOptions(data, 'gpt-5.6-luna', 'low').fastMode).toBe(false)
})

for (const pick of [undefined, 'default', 'gpt-6-astra', 'gpt-5.6-sol']) {
test(`sends the displayed Sol model for ${pick ?? 'an implicit default'}`, () => {
const displayed = resolveSelectedModel(catalog.models, pick)
Expand Down
13 changes: 7 additions & 6 deletions client/features/chat/chat-send.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FastMode } from '@/lib/types'
import type {
AttachmentInput,
AttachmentOrigin,
Expand All @@ -16,7 +17,7 @@ import { changeWorkspaceStateCache } from '@/client/features/workspace/workspace
import { attachmentKey, liveStore } from '@/client/features/chat/chat-store'
import { useUiStore } from '@/client/store/ui'
import type { StagedAttachment } from '@/client/features/chat/composer/attachments/types'
import { resolveSelectedModel } from '@/client/features/chat/composer/model-order'
import { resolveFastMode, resolveSelectedModel } from '@/client/features/chat/composer/model-order'
import type { MoiUserMessageOptions } from '@/client/features/workspace/moi-context'
import { STREAM_RESPONSES } from '@/client/lib/flags'
import { formatChatTitle } from '@/lib/chat-title'
Expand Down Expand Up @@ -197,8 +198,8 @@ export function resolveChatRunOptions(
modelsData: WorkspaceAgent | undefined,
pickedModel: string | undefined,
pickedEffort: string | undefined,
pickedFastMode?: boolean
): { model?: string; effort?: string; fastMode?: boolean; stream?: true } {
pickedFastMode?: FastMode
): { model?: string; effort?: string; fastMode?: FastMode; stream?: true } {
const models = modelsData?.models
const savedModel =
!pickedModel || !models || models.some(candidate => candidate.value === pickedModel)
Expand All @@ -220,9 +221,9 @@ export function resolveChatRunOptions(
? pickedEffort
: undefined
const fastMode =
pickedFastMode === undefined || !modelInfo || modelInfo.supportsFastMode
? pickedFastMode
: false
modelInfo && (pickedFastMode !== undefined || modelsData?.provider === 'codex')
? resolveFastMode(modelInfo, pickedFastMode)
: pickedFastMode
const stream = STREAM_RESPONSES && modelsData?.supportsStreaming ? true : undefined
return {
model,
Expand Down
11 changes: 11 additions & 0 deletions client/features/chat/composer/model-order.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ function model(value: string, resolvedModel?: string): Model {
return { value, resolvedModel, displayName: value }
}

test('Ultrafast selection and defaults follow the selected model capabilities', () => {
const supported = { ...model('sol'), supportsFastMode: true, supportsUltrafastMode: true }
expect(resolveFastMode(supported, 'ultrafast')).toBe('ultrafast')
expect(resolveFastMode({ ...supported, defaultFastMode: 'ultrafast' }, undefined)).toBe(
'ultrafast'
)
expect(resolveFastMode({ ...supported, defaultFastMode: 'ultrafast' }, false)).toBe(false)
expect(resolveFastMode({ ...supported, supportsUltrafastMode: false }, 'ultrafast')).toBe(true)
expect(resolveFastMode(model('no-speed-modes'), 'ultrafast')).toBe(false)
})

describe('sortModelsByProviderOrder', () => {
test('orders Anthropic model families while preserving SDK order within each family', () => {
const models = [
Expand Down
9 changes: 5 additions & 4 deletions client/features/chat/composer/model-order.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Model, WorkspaceType } from '@/lib/types'
import type { FastMode, Model, WorkspaceType } from '@/lib/types'

export const DEFAULT_EFFORT = 'high'

Expand Down Expand Up @@ -79,9 +79,10 @@ export function hasEffortChoice(levels: readonly string[]): boolean {
return levels.length > 1
}

export function resolveFastMode(model: Model, selectedFastMode: boolean | undefined): boolean {
if (model.supportsFastMode !== true) return false
return selectedFastMode ?? model.defaultFastMode ?? false
export function resolveFastMode(model: Model, selectedFastMode: FastMode | undefined): FastMode {
const mode = selectedFastMode ?? model.defaultFastMode ?? false
if (mode === 'ultrafast' && model.supportsUltrafastMode) return mode
return Boolean(mode && model.supportsFastMode)
}

export function sortModelsByProviderOrder(models: Model[], provider: WorkspaceType): Model[] {
Expand Down
86 changes: 67 additions & 19 deletions client/features/chat/composer/model-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type ComponentProps, type ReactNode, memo, useState } from 'react'
import { AnimatePresence, motion } from 'motion/react'

import { IconBolt, IconBoltFilled } from '@tabler/icons-react'
import { IconUltrafast } from '@/client/components/shared/ultrafast-icon'

import { useSaveSessionConfig, useSessionState } from '../sessions/api'
import { useWorkspaceAgent } from '@/client/features/workspace/api'
Expand Down Expand Up @@ -35,7 +36,7 @@ import { Slider } from '@/client/components/ui/slider'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/client/components/ui/tooltip'
import { useWorkspaceLayoutCtx } from '@/client/features/workspace/workspace-layout-context'
import { cn } from '@/client/lib/cn'
import type { Model } from '@/lib/types'
import type { FastMode, Model } from '@/lib/types'

function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1)
Expand Down Expand Up @@ -113,9 +114,31 @@ function ModelDropdown({ current, model, models, onValueChange }: ModelDropdownP
)
}

type FastModeToggleProps = { fastMode: boolean; onChange: (value: boolean) => void }
type FastModeToggleProps = {
fastMode: FastMode
supportsFastMode: boolean
supportsUltrafastMode: boolean
onChange: (value: FastMode) => void
}

function FastModeToggle({ fastMode, onChange }: FastModeToggleProps) {
function FastModeToggle({
fastMode,
supportsFastMode,
supportsUltrafastMode,
onChange
}: FastModeToggleProps) {
const nextMode =
fastMode === 'ultrafast'
? false
: fastMode
? supportsUltrafastMode
? 'ultrafast'
: false
: supportsFastMode
? true
: 'ultrafast'
const label = fastMode === 'ultrafast' ? 'Ultrafast' : fastMode ? 'Fast' : 'Regular'
const nextLabel = nextMode === 'ultrafast' ? 'ultrafast' : nextMode ? 'fast' : 'regular'
return (
<Tooltip>
<TooltipTrigger
Expand All @@ -124,12 +147,14 @@ function FastModeToggle({ fastMode, onChange }: FastModeToggleProps) {
type="button"
variant="ghost"
size="icon-sm"
aria-label={fastMode ? 'Turn off fast mode' : 'Turn on fast mode'}
aria-pressed={fastMode}
onClick={() => onChange(!fastMode)}
aria-label={`Speed: ${label}. Switch to ${nextLabel}`}
aria-pressed={fastMode !== false}
onClick={() => onChange(nextMode)}
className="-my-1 -mr-1"
>
{fastMode ? (
{fastMode === 'ultrafast' ? (
<IconUltrafast className="text-primary" stroke={1.75} />
) : fastMode ? (
<IconBoltFilled className="text-primary" stroke={1.75} />
) : (
<IconBolt className="text-muted-foreground" stroke={1.75} />
Expand All @@ -138,8 +163,10 @@ function FastModeToggle({ fastMode, onChange }: FastModeToggleProps) {
}
/>
<TooltipContent align="center" className="flex-col gap-0">
<span>Fast mode</span>
<span className="font-normal text-muted-foreground">More usage</span>
<span>{label} mode</span>
<span className="font-normal text-muted-foreground">
{fastMode ? 'More usage · ' : ''}Switch to {nextLabel}
</span>
</TooltipContent>
</Tooltip>
)
Expand All @@ -148,17 +175,19 @@ function FastModeToggle({ fastMode, onChange }: FastModeToggleProps) {
type EffortPickerProps = {
currentEffort: string
effortLevels: readonly string[]
fastMode: boolean
showFastMode: boolean
onFastModeChange: (value: boolean) => void
fastMode: FastMode
supportsFastMode: boolean
supportsUltrafastMode: boolean
onFastModeChange: (value: FastMode) => void
onValueChange: (value: string) => void
}

function EffortPicker({
currentEffort,
effortLevels,
fastMode,
showFastMode,
supportsFastMode,
supportsUltrafastMode,
onFastModeChange,
onValueChange
}: EffortPickerProps) {
Expand Down Expand Up @@ -187,7 +216,13 @@ function EffortPicker({
render={
<PickerTrigger
label={displayedLabel}
leadingIcon={fastMode ? <IconBolt stroke={1.5} /> : undefined}
leadingIcon={
fastMode === 'ultrafast' ? (
<IconUltrafast className="text-primary" stroke={1.5} />
) : fastMode ? (
<IconBolt stroke={1.5} />
) : undefined
}
aria-label={`Effort: ${displayedLabel}`}
/>
}
Expand Down Expand Up @@ -223,7 +258,14 @@ function EffortPicker({
<span className="text-muted-foreground">Effort</span>
<output aria-live="polite">{displayedLabel}</output>
</div>
{showFastMode && <FastModeToggle fastMode={fastMode} onChange={onFastModeChange} />}
{(supportsFastMode || supportsUltrafastMode) && (
<FastModeToggle
fastMode={fastMode}
supportsFastMode={supportsFastMode}
supportsUltrafastMode={supportsUltrafastMode}
onChange={onFastModeChange}
/>
)}
</motion.div>
)}
</AnimatePresence>
Expand Down Expand Up @@ -313,7 +355,7 @@ export const ModelPicker = memo(function ModelPicker({ sessionId }: ModelPickerP
else setLayout({ selectedEffort: value })
}

const setSelectedFastMode = (value: boolean) => {
const setSelectedFastMode = (value: FastMode) => {
if (sessionId) saveSessionConfig.mutate({ sessionId, patch: { fastMode: value } })
else setLayout({ selectedFastMode: value })
}
Expand All @@ -333,16 +375,22 @@ export const ModelPicker = memo(function ModelPicker({ sessionId }: ModelPickerP
models={models}
onValueChange={setSelectedModel}
/>
{!showEffort && model.supportsFastMode && (
<FastModeToggle fastMode={fastMode} onChange={setSelectedFastMode} />
{!showEffort && (model.supportsFastMode || model.supportsUltrafastMode) && (
<FastModeToggle
fastMode={fastMode}
supportsFastMode={model.supportsFastMode === true}
supportsUltrafastMode={model.supportsUltrafastMode === true}
onChange={setSelectedFastMode}
/>
)}
{showEffort && (
<EffortPicker
key={model.value}
currentEffort={currentEffort}
effortLevels={effortLevels}
fastMode={fastMode}
showFastMode={model.supportsFastMode === true}
supportsFastMode={model.supportsFastMode === true}
supportsUltrafastMode={model.supportsUltrafastMode === true}
onFastModeChange={setSelectedFastMode}
onValueChange={setSelectedEffort}
/>
Expand Down
3 changes: 2 additions & 1 deletion client/features/views/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FastMode } from '@/lib/types'
import type {
CompiledView,
MessageAttachment,
Expand Down Expand Up @@ -123,7 +124,7 @@ export type SubmitViewInput = {
attachments?: MessageAttachment[]
model?: string
effort?: string
fastMode?: boolean
fastMode?: FastMode
stream?: boolean
}
export function useSubmitView(workspaceId: string) {
Expand Down
5 changes: 5 additions & 0 deletions lib/fast-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { FastMode } from './types'

export function isFastMode(value: unknown): value is FastMode {
return typeof value === 'boolean' || value === 'ultrafast'
}
Loading
Loading