From 762d9b5bc3bcb84ba39f7040e4536c0d84c84380 Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Wed, 28 Oct 2026 12:21:52 +0100 Subject: [PATCH 1/9] Support Ultrafast mode for Codex chats --- client/features/chat/chat-send.test.ts | 25 ++++++ client/features/chat/chat-send.ts | 13 +-- .../chat/composer/model-order.test.ts | 11 +++ client/features/chat/composer/model-order.ts | 9 +- .../features/chat/composer/model-picker.tsx | 83 ++++++++++++++----- client/features/views/api.ts | 3 +- lib/fast-mode.ts | 5 ++ lib/types.ts | 12 ++- server/api/index.ts | 10 ++- server/api/session.test.ts | 4 + server/harness/claude-code/session.ts | 11 ++- server/harness/codex/NOTES.md | 17 +++- server/harness/codex/adapter.test.ts | 33 ++++++++ server/harness/codex/adapter.ts | 21 +++-- server/harness/codex/session.test.ts | 3 + server/harness/codex/session.ts | 3 +- server/harness/types.ts | 3 +- server/sessions/config.test.ts | 9 ++ server/sessions/config.ts | 5 +- server/web.ts | 3 +- server/workspace-state/file.ts | 3 +- 21 files changed, 230 insertions(+), 56 deletions(-) create mode 100644 lib/fast-mode.ts diff --git a/client/features/chat/chat-send.test.ts b/client/features/chat/chat-send.test.ts index f40f8d1e..709d67e4 100644 --- a/client/features/chat/chat-send.test.ts +++ b/client/features/chat/chat-send.test.ts @@ -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) diff --git a/client/features/chat/chat-send.ts b/client/features/chat/chat-send.ts index d976ed0c..d11b2e30 100644 --- a/client/features/chat/chat-send.ts +++ b/client/features/chat/chat-send.ts @@ -1,3 +1,4 @@ +import type { FastMode } from '@/lib/types' import type { AttachmentInput, AttachmentOrigin, @@ -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' @@ -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) @@ -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, diff --git a/client/features/chat/composer/model-order.test.ts b/client/features/chat/composer/model-order.test.ts index c119ff7f..c33baed9 100644 --- a/client/features/chat/composer/model-order.test.ts +++ b/client/features/chat/composer/model-order.test.ts @@ -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 = [ diff --git a/client/features/chat/composer/model-order.ts b/client/features/chat/composer/model-order.ts index 3deb5677..30edc60a 100644 --- a/client/features/chat/composer/model-order.ts +++ b/client/features/chat/composer/model-order.ts @@ -1,4 +1,4 @@ -import type { Model, WorkspaceType } from '@/lib/types' +import type { FastMode, Model, WorkspaceType } from '@/lib/types' export const DEFAULT_EFFORT = 'high' @@ -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[] { diff --git a/client/features/chat/composer/model-picker.tsx b/client/features/chat/composer/model-picker.tsx index 196c91db..c6e7f769 100644 --- a/client/features/chat/composer/model-picker.tsx +++ b/client/features/chat/composer/model-picker.tsx @@ -35,7 +35,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) @@ -113,9 +113,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 ( onChange(!fastMode)} - className="-my-1 -mr-1" + aria-label={`Speed: ${label}. Switch to ${nextLabel}`} + aria-pressed={fastMode !== false} + onClick={() => onChange(nextMode)} + className={cn('-my-1 -mr-1', fastMode === 'ultrafast' && 'ring-2 ring-primary/30')} > {fastMode ? ( @@ -138,8 +160,10 @@ function FastModeToggle({ fastMode, onChange }: FastModeToggleProps) { } /> - Fast mode - More usage + {label} mode + + {fastMode ? 'More usage · ' : ''}Switch to {nextLabel} + ) @@ -148,9 +172,10 @@ 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 } @@ -158,7 +183,8 @@ function EffortPicker({ currentEffort, effortLevels, fastMode, - showFastMode, + supportsFastMode, + supportsUltrafastMode, onFastModeChange, onValueChange }: EffortPickerProps) { @@ -187,7 +213,13 @@ function EffortPicker({ render={ : undefined} + leadingIcon={ + fastMode === 'ultrafast' ? ( + + ) : fastMode ? ( + + ) : undefined + } aria-label={`Effort: ${displayedLabel}`} /> } @@ -223,7 +255,14 @@ function EffortPicker({ Effort {displayedLabel} - {showFastMode && } + {(supportsFastMode || supportsUltrafastMode) && ( + + )} )} @@ -313,7 +352,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 }) } @@ -333,8 +372,13 @@ export const ModelPicker = memo(function ModelPicker({ sessionId }: ModelPickerP models={models} onValueChange={setSelectedModel} /> - {!showEffort && model.supportsFastMode && ( - + {!showEffort && (model.supportsFastMode || model.supportsUltrafastMode) && ( + )} {showEffort && ( diff --git a/client/features/views/api.ts b/client/features/views/api.ts index a63218d8..32b6d03e 100644 --- a/client/features/views/api.ts +++ b/client/features/views/api.ts @@ -1,3 +1,4 @@ +import type { FastMode } from '@/lib/types' import type { CompiledView, MessageAttachment, @@ -123,7 +124,7 @@ export type SubmitViewInput = { attachments?: MessageAttachment[] model?: string effort?: string - fastMode?: boolean + fastMode?: FastMode stream?: boolean } export function useSubmitView(workspaceId: string) { diff --git a/lib/fast-mode.ts b/lib/fast-mode.ts new file mode 100644 index 00000000..e55cd398 --- /dev/null +++ b/lib/fast-mode.ts @@ -0,0 +1,5 @@ +import type { FastMode } from './types' + +export function isFastMode(value: unknown): value is FastMode { + return typeof value === 'boolean' || value === 'ultrafast' +} diff --git a/lib/types.ts b/lib/types.ts index 70c1ef92..c4b067f1 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -260,7 +260,7 @@ export type ClientMessage = effort?: string // Opt into the selected model's faster processing mode. Omitted inherits // the provider configuration; false explicitly disables a stored default. - fastMode?: boolean + fastMode?: FastMode // Opt into live token-by-token streaming for this turn. Omitted/false runs // the current whole-block behavior. Only honored for providers that report // `supportsStreaming` (Claude Code); ignored otherwise. Like effort, a @@ -291,13 +291,16 @@ export type SessionSummary = { flavor?: 'chat' | 'cron' | 'subagent' } +// Keep existing boolean preferences: false is regular, true is fast. +export type FastMode = boolean | 'ultrafast' + // Per-chat agent settings, overriding the workspace defaults in // `.moi/.workspace.json`. A chat reopens with the model/effort/Fast mode it // last ran with; a brand-new chat is seeded from the workspace defaults. export type SessionConfig = { model?: string effort?: string - fastMode?: boolean + fastMode?: FastMode } export type SessionFork = { @@ -651,7 +654,7 @@ export type WorkspaceLayout = { // Reasoning-effort default for new sessions (a `supportedEffortLevels` value). selectedEffort?: string // Fast-mode default for new sessions. Undefined inherits the provider setting. - selectedFastMode?: boolean + selectedFastMode?: FastMode theme?: WorkspaceTheme } @@ -755,8 +758,9 @@ export type Model = { defaultEffort?: string supportsAdaptiveThinking?: boolean supportsFastMode?: boolean + supportsUltrafastMode?: boolean // Provider-resolved default when moi has no stored Fast-mode preference. - defaultFastMode?: boolean + defaultFastMode?: FastMode supportsAutoMode?: boolean } diff --git a/server/api/index.ts b/server/api/index.ts index b657cf2a..4e5d53db 100644 --- a/server/api/index.ts +++ b/server/api/index.ts @@ -1,3 +1,5 @@ +import type { FastMode } from '@/lib/types' +import { isFastMode } from '@/lib/fast-mode' import { detectInstalledHarnesses } from '../harness/executable' import { isMessageAttachments } from '@/lib/message-attachments' import { Hono } from 'hono' @@ -264,7 +266,7 @@ one.post('/views/:viewId/submit', async c => { selectedSessionScope?: SelectedSessionScope model?: string effort?: string - fastMode?: boolean + fastMode?: FastMode stream?: boolean attachments?: unknown }>() @@ -300,7 +302,7 @@ one.post('/views/:viewId/submit', async c => { optimisticId: body.optimisticId, model: typeof body.model === 'string' ? body.model : undefined, effort: typeof body.effort === 'string' ? body.effort : undefined, - fastMode: typeof body.fastMode === 'boolean' ? body.fastMode : undefined, + fastMode: isFastMode(body.fastMode) ? body.fastMode : undefined, stream: body.stream === true ? true : undefined }) return c.json(result) @@ -548,8 +550,8 @@ one.patch('/sessions/:sessionId/config', async c => { } if ('fastMode' in record) { const value = record.fastMode - if (value !== null && typeof value !== 'boolean') { - return c.text('fastMode must be a boolean or null', 400) + if (value !== null && !isFastMode(value)) { + return c.text('fastMode must be a boolean, "ultrafast", or null', 400) } patch.fastMode = value } diff --git a/server/api/session.test.ts b/server/api/session.test.ts index 929dec0c..bf18bce8 100644 --- a/server/api/session.test.ts +++ b/server/api/session.test.ts @@ -90,6 +90,10 @@ test('changing chat settings keeps the chat’s tab and fork history', async () ...entry, config: { model: 'sonnet', fastMode: false } }) + const ultrafast = await patch({ fastMode: 'ultrafast' }) + expect(ultrafast.status).toBe(200) + expect((await ultrafast.json()).sessions.child.config.fastMode).toBe('ultrafast') + expect((await patch({ fastMode: 'turbo' })).status).toBe(400) expect((await (await patch({ model: null, fastMode: null })).json()).sessions.child).toEqual( entry ) diff --git a/server/harness/claude-code/session.ts b/server/harness/claude-code/session.ts index 4fec89a4..77db84b5 100644 --- a/server/harness/claude-code/session.ts +++ b/server/harness/claude-code/session.ts @@ -37,6 +37,9 @@ import { resolveWorkspaceEnv } from '../../workspaces/env' import { requireHarnessExecutable } from '../executable' import type { SendMessageInput } from '../types' +// Claude supports only the original two speed modes. +type ClaudeSendMessageInput = Omit & { fastMode?: boolean } + // Media types Claude vision accepts; uploads.ts guarantees every image upload is // normalized to one of these, so the cast on `media_type` below is sound. type ImageMediaType = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' @@ -80,7 +83,7 @@ type InputQueue = { } type PendingMessage = { - input: SendMessageInput + input: ClaudeSendMessageInput content: MessageContent titleSource: string turnId: string @@ -671,7 +674,11 @@ function createLiveSession(input: { // Accept immediately, but keep the prompt and its settings in Moi until its // turn starts. The returned promise settles when the message reaches the SDK. -export async function sendCCMessage(input: SendMessageInput): Promise { +export async function sendCCMessage(incoming: SendMessageInput): Promise { + const input: ClaudeSendMessageInput = { + ...incoming, + fastMode: incoming.fastMode === 'ultrafast' ? false : incoming.fastMode + } const prepared = prepareAttachmentMessage(input.workspaceId, input.content, input.attachments) if (!prepared.text) return const { content: userContent, parts } = buildUserMessage(prepared) diff --git a/server/harness/codex/NOTES.md b/server/harness/codex/NOTES.md index bf6c175c..86df81ad 100644 --- a/server/harness/codex/NOTES.md +++ b/server/harness/codex/NOTES.md @@ -102,7 +102,7 @@ does not spawn one per card. Workspace import discovery separately reads up to 400 recent rollout heads under the server's `$CODEX_HOME/sessions`, without requiring a CLI executable. -## Models, effort, and fast mode +## Models, effort, and speed modes `model/list` is the source of picker capabilities. The catalog is paginated, filters hidden rows, and is cached per client for one minute. Login/account @@ -120,15 +120,24 @@ Changing a selection does not retry an earlier failed message. Configured reasoning effort applies when supported by the selected model; otherwise the adapter uses its advertised default. Fast mode is available when `serviceTiers` includes `priority`. Only catalogs missing that field fall back -to `additionalSpeedTiers: ["fast"]`. A non-null configured tier takes precedence +to `additionalSpeedTiers: ["fast"]`. Ultrafast uses the same capability check for +`ultrafast`, including the legacy fallback only when `serviceTiers` is missing. +The picker cycles regular → fast → ultrafast → regular, skipping unavailable +tiers. Existing boolean preferences remain valid; the third value is +`"ultrafast"`. A non-null configured tier takes precedence over the catalog default. | Moi fast-mode setting | Native `serviceTier` | | --------------------- | ---------------------------------------- | -| On | `"priority"` | -| Off | `null` | +| Regular (`false`) | `null` | +| Fast (`true`) | `"priority"` | +| Ultrafast | `"ultrafast"` | | Unset | Omitted; inherits the config/thread tier | +When the catalog is available, UI sends resolve the speed just like the model: +they send the displayed default explicitly, clearing an inherited tier when the +selected model does not support it. + Model, effort, and tier changes apply on the next **new turn**. `turn/steer` has no fields for changing them during an active turn. Moi uses the persistent `serviceTier` override for chat settings; it does not use the newer diff --git a/server/harness/codex/adapter.test.ts b/server/harness/codex/adapter.test.ts index d4666701..531b50fe 100644 --- a/server/harness/codex/adapter.test.ts +++ b/server/harness/codex/adapter.test.ts @@ -18,6 +18,39 @@ import { const THREAD = 'thread-1' +test('Ultrafast follows advertised capabilities and configured defaults', () => { + const model = { + id: 'sol', + model: 'sol', + displayName: 'Sol', + serviceTiers: [ + { id: 'priority', name: 'Fast', description: '' }, + { id: 'ultrafast', name: 'Ultrafast', description: '' } + ] + } + expect(codexModelToModel(model, 'ultrafast')).toMatchObject({ + supportsFastMode: true, + supportsUltrafastMode: true, + defaultFastMode: 'ultrafast' + }) + expect( + codexModelToModel({ + ...model, + serviceTiers: undefined, + additionalSpeedTiers: ['fast', 'ultrafast'] + }).supportsUltrafastMode + ).toBe(true) + expect( + codexModelToModel({ ...model, serviceTiers: [], additionalSpeedTiers: ['ultrafast'] }) + .supportsUltrafastMode + ).toBeUndefined() + expect( + codexModelToModel({ ...model, serviceTiers: [model.serviceTiers[0]] }, 'ultrafast') + .defaultFastMode + ).toBe(false) + expect(codexServiceTierForFastMode('ultrafast')).toBe('ultrafast') +}) + test('current service tiers take precedence over deprecated speed tiers', () => { const model = { id: 'm', model: 'm', displayName: 'M', additionalSpeedTiers: ['fast'] } expect(codexModelToModel(model).supportsFastMode).toBe(true) diff --git a/server/harness/codex/adapter.ts b/server/harness/codex/adapter.ts index 52fd32f5..5e61b943 100644 --- a/server/harness/codex/adapter.ts +++ b/server/harness/codex/adapter.ts @@ -13,7 +13,7 @@ import type { Turn } from '@/lib/format' import { stripMoiContextLoose } from '@/lib/moi-context' -import type { McpServer, Model, SessionSummary } from '@/lib/types' +import type { FastMode, McpServer, Model, SessionSummary } from '@/lib/types' import type { WorkspaceActivityPreview } from '../types' @@ -134,9 +134,10 @@ export function withCodexTurnDuration(turn: Turn, durationMs: number | null | un const CODEX_FAST_SERVICE_TIER = 'priority' export function codexServiceTierForFastMode( - fastMode: boolean | undefined -): typeof CODEX_FAST_SERVICE_TIER | null | undefined { + fastMode: FastMode | undefined +): typeof CODEX_FAST_SERVICE_TIER | 'ultrafast' | null | undefined { if (fastMode === undefined) return undefined + if (fastMode === 'ultrafast') return 'ultrafast' return fastMode ? CODEX_FAST_SERVICE_TIER : null } @@ -192,6 +193,10 @@ export function codexModelToModel(m: CodexModel, configuredServiceTier?: string m.serviceTiers !== undefined ? m.serviceTiers.some(tier => tier.id === CODEX_FAST_SERVICE_TIER) : (m.additionalSpeedTiers ?? []).includes('fast') + const supportsUltrafastMode = + m.serviceTiers !== undefined + ? m.serviceTiers.some(tier => tier.id === 'ultrafast') + : (m.additionalSpeedTiers ?? []).includes('ultrafast') // The effective user/project config governs omitted wire values. A null // config value leaves the model catalog's default in charge. const defaultServiceTier = configuredServiceTier ?? m.defaultServiceTier @@ -208,10 +213,14 @@ export function codexModelToModel(m: CodexModel, configuredServiceTier?: string ...(m.defaultReasoningEffort && efforts.includes(m.defaultReasoningEffort) ? { defaultEffort: m.defaultReasoningEffort } : {}), - ...(supportsFastMode + ...(supportsUltrafastMode ? { supportsUltrafastMode: true } : {}), + ...(supportsFastMode || supportsUltrafastMode ? { - supportsFastMode: true, - defaultFastMode: defaultServiceTier === CODEX_FAST_SERVICE_TIER + ...(supportsFastMode ? { supportsFastMode: true } : {}), + defaultFastMode: + supportsUltrafastMode && defaultServiceTier === 'ultrafast' + ? 'ultrafast' + : supportsFastMode && defaultServiceTier === CODEX_FAST_SERVICE_TIER } : {}) } diff --git a/server/harness/codex/session.test.ts b/server/harness/codex/session.test.ts index 9c3588a1..90955cf4 100644 --- a/server/harness/codex/session.test.ts +++ b/server/harness/codex/session.test.ts @@ -522,6 +522,9 @@ describe('Codex live session lifecycle', () => { await f.send('fast', { fastMode: true, effort: 'low' }) expect(f.calls.at(-1)?.params).toMatchObject({ serviceTier: 'priority', effort: 'low' }) f.emit('turn/completed', { turn: { id: 'turn-1', status: 'completed', items: [] } }) + await f.send('ultrafast', { fastMode: 'ultrafast' }) + expect(f.calls.at(-1)?.params.serviceTier).toBe('ultrafast') + f.emit('turn/completed', { turn: { id: 'turn-2', status: 'completed', items: [] } }) await f.send('normal', { fastMode: false }) expect(f.calls.at(-1)?.params.serviceTier).toBeNull() }) diff --git a/server/harness/codex/session.ts b/server/harness/codex/session.ts index c12c09af..84683322 100644 --- a/server/harness/codex/session.ts +++ b/server/harness/codex/session.ts @@ -1,3 +1,4 @@ +import type { FastMode } from '@/lib/types' import type { MessageAttachment } from '@/lib/types' import { attachmentLabel } from '@/lib/moi-attachments' // Live state per (workspaceId, sessionId): display history, native lifecycle, @@ -905,7 +906,7 @@ type CodexSendInput = { optimisticId?: string model?: string effort?: string - fastMode?: boolean + fastMode?: FastMode stream?: boolean // Rendered through native additionalContext or the legacy text envelope. context?: MoiContext diff --git a/server/harness/types.ts b/server/harness/types.ts index 425794b8..63e31636 100644 --- a/server/harness/types.ts +++ b/server/harness/types.ts @@ -1,3 +1,4 @@ +import type { FastMode } from '@/lib/types' // The harness contract: what an agent backend must implement to be drivable // by moi. See README.md in this folder for the full checklist and the // message-type layers; each harness's index.ts exports one Harness object @@ -39,7 +40,7 @@ export type SendMessageInput = { optimisticId?: string model?: string effort?: string - fastMode?: boolean + fastMode?: FastMode stream?: boolean // Structured moi context (lib/moi-context.ts). Each harness renders and // injects it its own way: Claude Code as a leading system-reminder text diff --git a/server/sessions/config.test.ts b/server/sessions/config.test.ts index 483cd857..aa3071ba 100644 --- a/server/sessions/config.test.ts +++ b/server/sessions/config.test.ts @@ -25,6 +25,15 @@ afterEach(async () => { }) describe('session Fast mode config', () => { + test('Ultrafast survives a disk reload and a session rename', async () => { + await saveSessionConfig(ws, 'temporary', { fastMode: 'ultrafast' }) + setWorkspaceStateRoot(scratchDir) + expect(await getSessionConfig(ws, 'temporary')).toEqual({ fastMode: 'ultrafast' }) + await updateWorkspaceState(ws, state => renameSessionEntry(state, 'temporary', 'real')) + expect(await getSessionConfig(ws, 'real')).toEqual({ fastMode: 'ultrafast' }) + await saveSessionConfig(ws, 'real', { fastMode: false }) + expect(await getSessionConfig(ws, 'real')).toEqual({ fastMode: false }) + }) test('preserves explicit false and clears only when asked', async () => { await saveSessionConfig(ws, 'session-1', { model: 'sonnet', diff --git a/server/sessions/config.ts b/server/sessions/config.ts index a210c210..576421c5 100644 --- a/server/sessions/config.ts +++ b/server/sessions/config.ts @@ -1,3 +1,4 @@ +import { isFastMode } from '@/lib/fast-mode' import type { SessionConfig } from '@/lib/types' import { updateWorkspaceState, type WorkspaceRef } from '../workspace-state' import { getSessionEntry } from './store' @@ -6,7 +7,7 @@ import { getSessionEntry } from './store' export type SessionConfigPatch = { model?: string | null effort?: string | null - fastMode?: boolean | null + fastMode?: SessionConfig['fastMode'] | null } export async function getSessionConfig( @@ -33,7 +34,7 @@ export async function saveSessionConfig( } if (typeof patch.model === 'string') config.model = patch.model if (typeof patch.effort === 'string') config.effort = patch.effort - if (typeof patch.fastMode === 'boolean') config.fastMode = patch.fastMode + if (isFastMode(patch.fastMode)) config.fastMode = patch.fastMode if (Object.keys(config).length) session.config = config else delete session.config return config diff --git a/server/web.ts b/server/web.ts index 8a739368..0d9d0324 100644 --- a/server/web.ts +++ b/server/web.ts @@ -1,3 +1,4 @@ +import { isFastMode } from '@/lib/fast-mode' import type { UserProfile } from '@/lib/collab/types' import { isMessageAttachments } from '@/lib/message-attachments' import type { ClientMessage, StatusSnapshotMessage } from '@/lib/types' @@ -79,7 +80,7 @@ function isClientMessage(value: unknown): value is ClientMessage { (v.optimisticId === undefined || typeof v.optimisticId === 'string') && (v.model === undefined || typeof v.model === 'string') && (v.effort === undefined || typeof v.effort === 'string') && - (v.fastMode === undefined || typeof v.fastMode === 'boolean') && + (v.fastMode === undefined || isFastMode(v.fastMode)) && (v.stream === undefined || typeof v.stream === 'boolean') && (v.context === undefined || isMoiContext(v.context)) && (v.attachments === undefined || isMessageAttachments(v.attachments)) diff --git a/server/workspace-state/file.ts b/server/workspace-state/file.ts index 8a541c55..c498dbd3 100644 --- a/server/workspace-state/file.ts +++ b/server/workspace-state/file.ts @@ -1,3 +1,4 @@ +import { isFastMode } from '@/lib/fast-mode' import { mkdir, rename, rm } from 'node:fs/promises' import { dirname } from 'node:path' @@ -65,7 +66,7 @@ function normalizeSession(value: unknown): WorkspaceSessionState { const config = { ...(typeof model === 'string' ? { model } : {}), ...(typeof effort === 'string' ? { effort } : {}), - ...(typeof fastMode === 'boolean' ? { fastMode } : {}) + ...(isFastMode(fastMode) ? { fastMode } : {}) } if (Object.keys(config).length) session.config = config } From c925b44dd1076deae24177a4dbafaed9e7caa1fb Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Wed, 28 Oct 2026 12:26:15 +0100 Subject: [PATCH 2/9] Preserve Codex fast-tier aliases across client versions --- server/harness/codex/NOTES.md | 3 +- server/harness/codex/adapter.ts | 3 +- server/harness/codex/speed-mode.test.ts | 71 +++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 server/harness/codex/speed-mode.test.ts diff --git a/server/harness/codex/NOTES.md b/server/harness/codex/NOTES.md index 86df81ad..18021f8c 100644 --- a/server/harness/codex/NOTES.md +++ b/server/harness/codex/NOTES.md @@ -125,7 +125,8 @@ to `additionalSpeedTiers: ["fast"]`. Ultrafast uses the same capability check fo The picker cycles regular → fast → ultrafast → regular, skipping unavailable tiers. Existing boolean preferences remain valid; the third value is `"ultrafast"`. A non-null configured tier takes precedence -over the catalog default. +over the catalog default. Both `fast` and `priority` config values select Fast; +outgoing requests use the canonical `priority` tier. | Moi fast-mode setting | Native `serviceTier` | | --------------------- | ---------------------------------------- | diff --git a/server/harness/codex/adapter.ts b/server/harness/codex/adapter.ts index 5e61b943..c254fffd 100644 --- a/server/harness/codex/adapter.ts +++ b/server/harness/codex/adapter.ts @@ -220,7 +220,8 @@ export function codexModelToModel(m: CodexModel, configuredServiceTier?: string defaultFastMode: supportsUltrafastMode && defaultServiceTier === 'ultrafast' ? 'ultrafast' - : supportsFastMode && defaultServiceTier === CODEX_FAST_SERVICE_TIER + : supportsFastMode && + (defaultServiceTier === CODEX_FAST_SERVICE_TIER || defaultServiceTier === 'fast') } : {}) } diff --git a/server/harness/codex/speed-mode.test.ts b/server/harness/codex/speed-mode.test.ts new file mode 100644 index 00000000..a4eb0c1c --- /dev/null +++ b/server/harness/codex/speed-mode.test.ts @@ -0,0 +1,71 @@ +import { expect, test } from 'bun:test' + +import { resolveChatRunOptions } from '@/client/features/chat/chat-send' +import { resolveFastMode } from '@/client/features/chat/composer/model-order' +import type { FastMode } from '@/lib/types' +import { type CodexModel, codexModelsToModels, codexServiceTierForFastMode } from './adapter' + +const base: CodexModel = { id: 'model', model: 'model', displayName: 'Model' } +const fast = { id: 'priority', name: 'Fast', description: '' } +const ultrafast = { id: 'ultrafast', name: 'Ultrafast', description: '' } + +const catalogs: { name: string; model: CodexModel; expected: FastMode }[] = [ + { name: 'before speed metadata', model: base, expected: false }, + { name: 'legacy fast only', model: { ...base, additionalSpeedTiers: ['fast'] }, expected: true }, + { + name: 'legacy ultrafast', + model: { ...base, additionalSpeedTiers: ['fast', 'ultrafast'] }, + expected: 'ultrafast' + }, + { name: 'current fast only', model: { ...base, serviceTiers: [fast] }, expected: true }, + { + name: 'current ultrafast', + model: { ...base, serviceTiers: [fast, ultrafast] }, + expected: 'ultrafast' + }, + { + name: 'current empty overrides legacy', + model: { ...base, serviceTiers: [], additionalSpeedTiers: ['fast', 'ultrafast'] }, + expected: false + }, + { + name: 'unknown future tier', + model: { ...base, serviceTiers: [{ id: 'future', name: 'Future', description: '' }] }, + expected: false + } +] + +test.each(catalogs)('$name safely resolves a saved Ultrafast preference', ({ model, expected }) => { + const models = codexModelsToModels([model], {}) + const run = resolveChatRunOptions( + { provider: 'codex', availability: { status: 'available' }, models }, + 'model', + undefined, + 'ultrafast' + ) + expect(resolveFastMode(models[1], 'ultrafast')).toBe(expected) + expect(run.fastMode).toBe(expected) + expect(codexServiceTierForFastMode(run.fastMode)).toBe( + expected === 'ultrafast' ? 'ultrafast' : expected ? 'priority' : null + ) +}) + +test.each(['fast', 'priority'])( + 'preserves the configured %s alias with legacy and current catalogs', + alias => { + for (const model of [ + { ...base, additionalSpeedTiers: ['fast'] }, + { ...base, serviceTiers: [fast, ultrafast] } + ]) { + const models = codexModelsToModels([model], { service_tier: alias }) + const agent = { + provider: 'codex' as const, + availability: { status: 'available' as const }, + models + } + expect(resolveFastMode(models[1], undefined)).toBe(true) + expect(resolveChatRunOptions(agent, 'model', undefined).fastMode).toBe(true) + expect(resolveChatRunOptions(agent, 'model', undefined, false).fastMode).toBe(false) + } + } +) From 82ff8bd9050a163889611e9f2c02c5a53eba4abe Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Wed, 28 Oct 2026 12:27:16 +0100 Subject: [PATCH 3/9] Use a double bolt for Ultrafast mode --- .../features/chat/composer/model-picker.tsx | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/client/features/chat/composer/model-picker.tsx b/client/features/chat/composer/model-picker.tsx index c6e7f769..bc46e6d4 100644 --- a/client/features/chat/composer/model-picker.tsx +++ b/client/features/chat/composer/model-picker.tsx @@ -113,6 +113,24 @@ function ModelDropdown({ current, model, models, onValueChange }: ModelDropdownP ) } +type UltrafastIconProps = { size: 16 | 20 } + +function UltrafastIcon({ size }: UltrafastIconProps) { + const stroke = size === 16 ? 1.75 : 1.5 + return ( + + ) +} + type FastModeToggleProps = { fastMode: FastMode supportsFastMode: boolean @@ -149,9 +167,11 @@ function FastModeToggle({ aria-label={`Speed: ${label}. Switch to ${nextLabel}`} aria-pressed={fastMode !== false} onClick={() => onChange(nextMode)} - className={cn('-my-1 -mr-1', fastMode === 'ultrafast' && 'ring-2 ring-primary/30')} + className="-my-1 -mr-1" > - {fastMode ? ( + {fastMode === 'ultrafast' ? ( + + ) : fastMode ? ( ) : ( @@ -215,7 +235,7 @@ function EffortPicker({ label={displayedLabel} leadingIcon={ fastMode === 'ultrafast' ? ( - + ) : fastMode ? ( ) : undefined From 87865504ba1590d2c1a3847a41f7bc9dc89af55e Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Wed, 28 Oct 2026 12:30:24 +0100 Subject: [PATCH 4/9] Use an existing Ultrafast icon asset --- client/components/shared/ultrafast-icon.tsx | 46 +++++++++++++++++++ .../features/chat/composer/model-picker.tsx | 23 ++-------- 2 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 client/components/shared/ultrafast-icon.tsx diff --git a/client/components/shared/ultrafast-icon.tsx b/client/components/shared/ultrafast-icon.tsx new file mode 100644 index 00000000..3268195c --- /dev/null +++ b/client/components/shared/ultrafast-icon.tsx @@ -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' + } + ] +]) diff --git a/client/features/chat/composer/model-picker.tsx b/client/features/chat/composer/model-picker.tsx index bc46e6d4..d0c9a880 100644 --- a/client/features/chat/composer/model-picker.tsx +++ b/client/features/chat/composer/model-picker.tsx @@ -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' @@ -113,24 +114,6 @@ function ModelDropdown({ current, model, models, onValueChange }: ModelDropdownP ) } -type UltrafastIconProps = { size: 16 | 20 } - -function UltrafastIcon({ size }: UltrafastIconProps) { - const stroke = size === 16 ? 1.75 : 1.5 - return ( - - ) -} - type FastModeToggleProps = { fastMode: FastMode supportsFastMode: boolean @@ -170,7 +153,7 @@ function FastModeToggle({ className="-my-1 -mr-1" > {fastMode === 'ultrafast' ? ( - + ) : fastMode ? ( ) : ( @@ -235,7 +218,7 @@ function EffortPicker({ label={displayedLabel} leadingIcon={ fastMode === 'ultrafast' ? ( - + ) : fastMode ? ( ) : undefined From e7ac694f9df5814b4f9be0e2b7fe227089844726 Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Wed, 28 Oct 2026 12:34:38 +0100 Subject: [PATCH 5/9] Run CI on Bun 1.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 45bcee1a..83036a9b 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "workspace", "cli" ], - "packageManager": "bun@1.3.14", + "packageManager": "bun@1.4.0", "engines": { "bun": ">=1.3" }, From d2c51ac009605495cdf1743fb28eb3554f47a5b6 Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Wed, 28 Oct 2026 12:37:49 +0100 Subject: [PATCH 6/9] Use the current Bun patch with native addon fixes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 83036a9b..100701a1 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "workspace", "cli" ], - "packageManager": "bun@1.4.0", + "packageManager": "bun@1.4.4", "engines": { "bun": ">=1.3" }, From 986dfc35bfbaddf6e58ea6df9c1f7304c49ffd8e Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Wed, 28 Oct 2026 12:40:29 +0100 Subject: [PATCH 7/9] Align the Bun type lock entries with package.json --- bun.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index 81baf0b2..476609d6 100644 --- a/bun.lock +++ b/bun.lock @@ -660,7 +660,7 @@ "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], - "@types/bun": ["@types/bun@1.4.1", "", { "dependencies": { "bun-types": "1.4.1" } }, "sha512-0AVGiTXGajf1rgKom3N+c5L7CBxuoyyv1i44M0nX4UDK0G/fnRAMiri93nHuVPIb429KKtAgj7HatVmmOjeQLA=="], + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], "@types/culori": ["@types/culori@4.0.1", "", {}, "sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ=="], @@ -772,7 +772,7 @@ "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], - "bun-types": ["bun-types@1.4.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-loKuVrAFZKfEv+JvWkHRS9GW5IqLuLRjVXN9p+vZvBN86O5hf/pBZQ5hSoyipsrMmWObZBDvWnlmKvjKTM0PdA=="], + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], From 4a92cad1f502d9c73e4f5dc87a2cfa95df9157b2 Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Wed, 28 Oct 2026 12:42:43 +0100 Subject: [PATCH 8/9] Pin Linux CI to Ubuntu 24.04 for native addon compatibility --- .github/workflows/ci.yml | 3 ++- bun.lock | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e65b1bbd..56237ce0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/bun.lock b/bun.lock index 476609d6..81baf0b2 100644 --- a/bun.lock +++ b/bun.lock @@ -660,7 +660,7 @@ "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], - "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + "@types/bun": ["@types/bun@1.4.1", "", { "dependencies": { "bun-types": "1.4.1" } }, "sha512-0AVGiTXGajf1rgKom3N+c5L7CBxuoyyv1i44M0nX4UDK0G/fnRAMiri93nHuVPIb429KKtAgj7HatVmmOjeQLA=="], "@types/culori": ["@types/culori@4.0.1", "", {}, "sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ=="], @@ -772,7 +772,7 @@ "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], - "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + "bun-types": ["bun-types@1.4.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-loKuVrAFZKfEv+JvWkHRS9GW5IqLuLRjVXN9p+vZvBN86O5hf/pBZQ5hSoyipsrMmWObZBDvWnlmKvjKTM0PdA=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], diff --git a/package.json b/package.json index 100701a1..45bcee1a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "workspace", "cli" ], - "packageManager": "bun@1.4.4", + "packageManager": "bun@1.3.14", "engines": { "bun": ">=1.3" }, From e0d7f6a614bbd571c962f9087846e18f25fcef3f Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Wed, 28 Oct 2026 12:47:22 +0100 Subject: [PATCH 9/9] Use Bun 1.4.4 for CI --- bun.lock | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 81baf0b2..476609d6 100644 --- a/bun.lock +++ b/bun.lock @@ -660,7 +660,7 @@ "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], - "@types/bun": ["@types/bun@1.4.1", "", { "dependencies": { "bun-types": "1.4.1" } }, "sha512-0AVGiTXGajf1rgKom3N+c5L7CBxuoyyv1i44M0nX4UDK0G/fnRAMiri93nHuVPIb429KKtAgj7HatVmmOjeQLA=="], + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], "@types/culori": ["@types/culori@4.0.1", "", {}, "sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ=="], @@ -772,7 +772,7 @@ "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], - "bun-types": ["bun-types@1.4.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-loKuVrAFZKfEv+JvWkHRS9GW5IqLuLRjVXN9p+vZvBN86O5hf/pBZQ5hSoyipsrMmWObZBDvWnlmKvjKTM0PdA=="], + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], diff --git a/package.json b/package.json index 45bcee1a..100701a1 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "workspace", "cli" ], - "packageManager": "bun@1.3.14", + "packageManager": "bun@1.4.4", "engines": { "bun": ">=1.3" },