From 1f4ac057e4f63e34f5b440a9850c0fdeaaf04170 Mon Sep 17 00:00:00 2001 From: "@mrubens" <2600+mrubens@users.noreply.github.com> Date: Thu, 10 Sep 2026 04:07:28 +0000 Subject: [PATCH 1/2] feat: add experimental Code Mode setting --- .../settings/ExperimentalSettings.tsx | 69 +++++++++++++++ .../pages/ExperimentalSettingsPage.tsx | 3 +- .../settings/settings-navigation.ts | 3 +- .../experimental-settings/index.test.ts | 86 +++++++++++++++++++ .../commands/experimental-settings/index.ts | 63 ++++++++++++++ apps/web/src/trpc/routers/_app.ts | 15 ++++ packages/db/package.json | 1 + .../db/src/lib/model-runtime-config.test.ts | 40 +++++++++ packages/db/src/lib/model-runtime-config.ts | 16 +++- .../src/__tests__/config.test.ts | 11 ++- .../__tests__/metadata-descriptions.test.ts | 10 ++- packages/feature-flags/src/config.ts | 6 ++ packages/feature-flags/src/index.ts | 9 ++ packages/feature-flags/src/types.ts | 6 +- pnpm-lock.yaml | 3 + 15 files changed, 330 insertions(+), 11 deletions(-) create mode 100644 apps/web/src/components/settings/ExperimentalSettings.tsx create mode 100644 apps/web/src/trpc/commands/experimental-settings/index.test.ts create mode 100644 apps/web/src/trpc/commands/experimental-settings/index.ts diff --git a/apps/web/src/components/settings/ExperimentalSettings.tsx b/apps/web/src/components/settings/ExperimentalSettings.tsx new file mode 100644 index 0000000000..bf85a6c2fc --- /dev/null +++ b/apps/web/src/components/settings/ExperimentalSettings.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; + +import { FlaskConical, Skeleton, Switch } from '@/components/system'; +import { useTRPC } from '@/trpc/client'; + +import { Section } from './Section'; +import type { ExperimentalSettings as ExperimentalSettingsData } from '@/trpc/commands/experimental-settings'; + +export function ExperimentalSettings() { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const queryKey = trpc.experimentalSettings.get.queryKey(); + const settingsQuery = useQuery(trpc.experimentalSettings.get.queryOptions()); + const updateMutation = useMutation( + trpc.experimentalSettings.setOpenCodeCodeMode.mutationOptions(), + ); + + const handleToggle = async (enabled: boolean) => { + const previous = settingsQuery.data; + queryClient.setQueryData(queryKey, { + openCodeCodeModeEnabled: enabled, + }); + + try { + const updated = await updateMutation.mutateAsync({ enabled }); + queryClient.setQueryData(queryKey, updated); + toast.success(`Code Mode ${enabled ? 'enabled' : 'disabled'}`); + } catch (error) { + queryClient.setQueryData(queryKey, previous); + toast.error( + error instanceof Error ? error.message : 'Failed to update Code Mode.', + ); + } + }; + + if (settingsQuery.isPending) { + return ; + } + + if (settingsQuery.isError || !settingsQuery.data) { + return ( +
+ Failed to load experimental settings. +
+ ); + } + + return ( +
+
+ void handleToggle(checked === true)} + /> +
+

+ Defer eligible tools and discover them when needed, reducing the + tool definitions sent with each request. +

+
+
+
+ ); +} diff --git a/apps/web/src/components/settings/pages/ExperimentalSettingsPage.tsx b/apps/web/src/components/settings/pages/ExperimentalSettingsPage.tsx index cb1c77bade..f804970071 100644 --- a/apps/web/src/components/settings/pages/ExperimentalSettingsPage.tsx +++ b/apps/web/src/components/settings/pages/ExperimentalSettingsPage.tsx @@ -1,11 +1,12 @@ 'use client'; import { SettingsShell } from '@/components/settings/SettingsShell'; +import { ExperimentalSettings } from '@/components/settings/ExperimentalSettings'; export function ExperimentalSettingsPage() { return ( - {null} + ); } diff --git a/apps/web/src/components/settings/settings-navigation.ts b/apps/web/src/components/settings/settings-navigation.ts index a776c2946f..160419ff70 100644 --- a/apps/web/src/components/settings/settings-navigation.ts +++ b/apps/web/src/components/settings/settings-navigation.ts @@ -174,7 +174,8 @@ const SETTINGS_NAVIGATION_ITEMS: SettingsNavigationItem[] = [ id: 'experimental', label: 'Experimental', title: 'Experimental', - description: 'No experimental features at the moment. Check back soon.', + description: + 'Try opt-in features that may change while they are being evaluated.', href: SETTINGS_PATHS.experimental, icon: FlaskConical, adminOnly: true, diff --git a/apps/web/src/trpc/commands/experimental-settings/index.test.ts b/apps/web/src/trpc/commands/experimental-settings/index.test.ts new file mode 100644 index 0000000000..7aa4bebbb5 --- /dev/null +++ b/apps/web/src/trpc/commands/experimental-settings/index.test.ts @@ -0,0 +1,86 @@ +const { findFirst, insert, values, onConflictDoUpdate } = vi.hoisted(() => ({ + findFirst: vi.fn(), + insert: vi.fn(), + values: vi.fn(), + onConflictDoUpdate: vi.fn(), +})); + +vi.mock('@roomote/db/server', () => ({ + db: { + query: { deploymentSettings: { findFirst } }, + insert, + }, + deploymentSettings: { id: 'deployment_settings.id' }, + eq: vi.fn(), +})); + +vi.mock('../setup/shared', () => ({ + assertAdmin: (auth: { isAdmin: boolean }) => { + if (!auth.isAdmin) throw new Error('Unauthorized'); + }, +})); + +import type { UserAuthSuccess } from '@/types'; + +import { + getExperimentalSettingsCommand, + setOpenCodeCodeModeCommand, +} from './index'; + +const adminAuth = { userId: 'admin', isAdmin: true } as UserAuthSuccess; +const memberAuth = { userId: 'member', isAdmin: false } as UserAuthSuccess; + +describe('experimental settings commands', () => { + beforeEach(() => { + vi.clearAllMocks(); + insert.mockReturnValue({ values }); + values.mockReturnValue({ onConflictDoUpdate }); + onConflictDoUpdate.mockResolvedValue(undefined); + }); + + it('defaults Code Mode off and reads a persisted opt-in', async () => { + findFirst.mockResolvedValueOnce(undefined).mockResolvedValueOnce({ + metadata: { opencode_code_mode: true }, + }); + + await expect(getExperimentalSettingsCommand(adminAuth)).resolves.toEqual({ + openCodeCodeModeEnabled: false, + }); + await expect(getExperimentalSettingsCommand(adminAuth)).resolves.toEqual({ + openCodeCodeModeEnabled: true, + }); + }); + + it('persists the validated boolean without dropping other metadata', async () => { + findFirst.mockResolvedValue({ metadata: { keep_me: 'value' } }); + + await expect( + setOpenCodeCodeModeCommand(adminAuth, { enabled: true }), + ).resolves.toEqual({ openCodeCodeModeEnabled: true }); + + expect(values).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'default', + metadata: { keep_me: 'value', opencode_code_mode: true }, + }), + ); + expect(onConflictDoUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + target: 'deployment_settings.id', + set: expect.objectContaining({ + metadata: { keep_me: 'value', opencode_code_mode: true }, + }), + }), + ); + }); + + it('rejects non-admin reads and writes', async () => { + await expect(getExperimentalSettingsCommand(memberAuth)).rejects.toThrow( + 'Unauthorized', + ); + await expect( + setOpenCodeCodeModeCommand(memberAuth, { enabled: true }), + ).rejects.toThrow('Unauthorized'); + expect(findFirst).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/trpc/commands/experimental-settings/index.ts b/apps/web/src/trpc/commands/experimental-settings/index.ts new file mode 100644 index 0000000000..82b0f4072f --- /dev/null +++ b/apps/web/src/trpc/commands/experimental-settings/index.ts @@ -0,0 +1,63 @@ +import { db, deploymentSettings, eq } from '@roomote/db/server'; +import { + isOpenCodeCodeModeEnabledFromMetadata, + OPENCODE_CODE_MODE_METADATA_KEY, +} from '@roomote/feature-flags'; + +import type { UserAuthSuccess } from '@/types'; + +import { assertAdmin } from '../setup/shared'; + +const DEFAULT_DEPLOYMENT_ID = 'default'; + +export type ExperimentalSettings = { + openCodeCodeModeEnabled: boolean; +}; + +function normalizeMetadata(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) + ? { ...(value as Record) } + : {}; +} + +export async function getExperimentalSettingsCommand( + auth: UserAuthSuccess, +): Promise { + assertAdmin(auth); + const settings = await db.query.deploymentSettings.findFirst({ + where: eq(deploymentSettings.id, DEFAULT_DEPLOYMENT_ID), + columns: { metadata: true }, + }); + + return { + openCodeCodeModeEnabled: isOpenCodeCodeModeEnabledFromMetadata( + settings?.metadata, + ), + }; +} + +export async function setOpenCodeCodeModeCommand( + auth: UserAuthSuccess, + input: { enabled: boolean }, +): Promise { + assertAdmin(auth); + const existing = await db.query.deploymentSettings.findFirst({ + where: eq(deploymentSettings.id, DEFAULT_DEPLOYMENT_ID), + columns: { metadata: true }, + }); + const metadata = { + ...normalizeMetadata(existing?.metadata), + [OPENCODE_CODE_MODE_METADATA_KEY]: input.enabled, + }; + const now = new Date(); + + await db + .insert(deploymentSettings) + .values({ id: DEFAULT_DEPLOYMENT_ID, metadata, updatedAt: now }) + .onConflictDoUpdate({ + target: deploymentSettings.id, + set: { metadata, updatedAt: now }, + }); + + return { openCodeCodeModeEnabled: input.enabled }; +} diff --git a/apps/web/src/trpc/routers/_app.ts b/apps/web/src/trpc/routers/_app.ts index 451bdd9683..e37fd29f4a 100644 --- a/apps/web/src/trpc/routers/_app.ts +++ b/apps/web/src/trpc/routers/_app.ts @@ -468,6 +468,10 @@ import { setDeploymentTimeZoneCommand, setAnonymousAnalyticsCommand, } from '../commands/misc-settings'; +import { + getExperimentalSettingsCommand, + setOpenCodeCodeModeCommand, +} from '../commands/experimental-settings'; import { backfillBrainTaskMemoriesCommand, getBrainPageCommand, @@ -3258,6 +3262,17 @@ export const appRouter = createRouter({ ), }), + experimentalSettings: createRouter({ + get: protectedProcedure.query(({ ctx: { auth } }) => + getExperimentalSettingsCommand(auth), + ), + setOpenCodeCodeMode: protectedProcedure + .input(z.object({ enabled: z.boolean() })) + .mutation(({ ctx: { auth }, input }) => + setOpenCodeCodeModeCommand(auth, input), + ), + }), + releases: createRouter({ status: protectedProcedure.query(({ ctx: { auth } }) => getReleaseStatusCommand(auth), diff --git a/packages/db/package.json b/packages/db/package.json index 9948b8807c..f8d1c5c91d 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -40,6 +40,7 @@ }, "dependencies": { "@roomote/env": "workspace:^", + "@roomote/feature-flags": "workspace:^", "@roomote/types": "workspace:^", "drizzle-orm": "^0.45.2", "nanoid": "^5.1.16", diff --git a/packages/db/src/lib/model-runtime-config.test.ts b/packages/db/src/lib/model-runtime-config.test.ts index d6b2263551..c42da2e30a 100644 --- a/packages/db/src/lib/model-runtime-config.test.ts +++ b/packages/db/src/lib/model-runtime-config.test.ts @@ -98,6 +98,46 @@ describe('resolveEffectiveModelRuntimeEnv', () => { mockGetFreshXaiAccessToken.mockResolvedValue(null); }); + it('enables only the direct OpenCode Code Mode experiment when configured', async () => { + mockDeploymentSettingsFindFirst.mockResolvedValue({ + metadata: { opencode_code_mode: true }, + runtimeModelConfig: {}, + taskModelSettings: {}, + }); + + const [controlPlaneEnv, sandboxEnv] = await Promise.all([ + resolveEffectiveModelRuntimeEnv({ + runtimeEnv: {}, + deploymentEnvVars: {}, + }), + resolveSandboxModelRuntimeEnv({ + runtimeEnv: {}, + deploymentEnvVars: {}, + }), + ]); + + for (const env of [controlPlaneEnv, sandboxEnv]) { + expect(env.OPENCODE_EXPERIMENTAL_CODE_MODE).toBe('1'); + expect(env.OPENCODE_EXPERIMENTAL).toBeUndefined(); + } + }); + + it('leaves OpenCode Code Mode disabled by default', async () => { + mockDeploymentSettingsFindFirst.mockResolvedValue({ + metadata: {}, + runtimeModelConfig: {}, + taskModelSettings: {}, + }); + + const env = await resolveSandboxModelRuntimeEnv({ + runtimeEnv: {}, + deploymentEnvVars: {}, + }); + + expect(env.OPENCODE_EXPERIMENTAL_CODE_MODE).toBeUndefined(); + expect(env.OPENCODE_EXPERIMENTAL).toBeUndefined(); + }); + it('prefers real runtime env values over persisted deployment config', async () => { mockDeploymentSettingsFindFirst.mockResolvedValue({ runtimeModelConfig: { diff --git a/packages/db/src/lib/model-runtime-config.ts b/packages/db/src/lib/model-runtime-config.ts index 1a5177cc10..5aa6be76e2 100644 --- a/packages/db/src/lib/model-runtime-config.ts +++ b/packages/db/src/lib/model-runtime-config.ts @@ -1,4 +1,5 @@ import { eq } from 'drizzle-orm'; +import { isOpenCodeCodeModeEnabledFromMetadata } from '@roomote/feature-flags'; import { applyImplicitLiteLlmModelPrefix, CHATGPT_FAST_MODE_ENV_VAR_NAME, @@ -112,6 +113,7 @@ async function loadPersistedRuntimeModelConfig( const deployment = await executor.query.deploymentSettings.findFirst({ where: eq(deploymentSettings.id, DEFAULT_DEPLOYMENT_ID), columns: { + metadata: true, runtimeModelConfig: true, taskModelSettings: true, }, @@ -124,6 +126,9 @@ async function loadPersistedRuntimeModelConfig( catalogModels: getTaskModelCatalog(deployment?.taskModelSettings), enabledCatalogModels: getEnabledTaskModels(deployment?.taskModelSettings), defaultModelId: getDefaultTaskModelId(deployment?.taskModelSettings), + openCodeCodeModeEnabled: isOpenCodeCodeModeEnabledFromMetadata( + deployment?.metadata, + ), }; } @@ -408,7 +413,13 @@ async function resolveModelRuntimeEnv( const executor = options.executor ?? db; const [ persistedEnvVars, - { runtimeModelConfig, catalogModels, enabledCatalogModels, defaultModelId }, + { + runtimeModelConfig, + catalogModels, + enabledCatalogModels, + defaultModelId, + openCodeCodeModeEnabled, + }, ] = await Promise.all([ resolveEffectiveDeploymentEnvVars({ deploymentEnvVars: options.deploymentEnvVars, @@ -703,6 +714,9 @@ async function resolveModelRuntimeEnv( return { ...resolvedRoleEnv, + ...(openCodeCodeModeEnabled + ? { OPENCODE_EXPERIMENTAL_CODE_MODE: '1' } + : {}), ...(providerKeyNames.length > 0 && { R_MODEL_ENV_KEYS: providerKeyNames.join(','), }), diff --git a/packages/feature-flags/src/__tests__/config.test.ts b/packages/feature-flags/src/__tests__/config.test.ts index 3e6733b0d8..70eacd5ea6 100644 --- a/packages/feature-flags/src/__tests__/config.test.ts +++ b/packages/feature-flags/src/__tests__/config.test.ts @@ -3,15 +3,14 @@ import { describe, expect, it } from 'vitest'; import { DEPLOYMENT_METADATA_BOOLEAN_CONFIG } from '../config'; describe('deployment metadata config', () => { - it('retains the deployment-control metadata descriptors', () => { + it('retains active metadata descriptors', () => { expect(Object.keys(DEPLOYMENT_METADATA_BOOLEAN_CONFIG).sort()).toEqual([ 'anonymous_analytics_enabled', 'deployment_disabled', + 'opencode_code_mode', ]); - for (const descriptor of Object.values( - DEPLOYMENT_METADATA_BOOLEAN_CONFIG, - )) { - expect(descriptor.kind).toBe('deployment-control'); - } + expect(DEPLOYMENT_METADATA_BOOLEAN_CONFIG.opencode_code_mode?.kind).toBe( + 'experimental', + ); }); }); diff --git a/packages/feature-flags/src/__tests__/metadata-descriptions.test.ts b/packages/feature-flags/src/__tests__/metadata-descriptions.test.ts index f2569fb1b3..e83f44b114 100644 --- a/packages/feature-flags/src/__tests__/metadata-descriptions.test.ts +++ b/packages/feature-flags/src/__tests__/metadata-descriptions.test.ts @@ -11,7 +11,6 @@ describe('metadata descriptions', () => { 'visual_proof_auto_screencast', 'background_subagents', 'opencode_background_subagents', - 'opencode_code_mode', 'composerSuggestions', ])('classifies removed experiment metadata %s as legacy', (key) => { expect(getBooleanMetadataDescriptorByKey(key)).toEqual({ @@ -21,6 +20,15 @@ describe('metadata descriptions', () => { }); }); + it('classifies Code Mode as an active experiment', () => { + expect(getBooleanMetadataDescriptorByKey('opencode_code_mode')).toEqual({ + kind: 'experimental', + description: + 'Defer eligible tools and discover them when needed, reducing the tool definitions sent with each request.', + group: 'Code Mode', + }); + }); + it('retains active deployment-control descriptors', () => { expect(getBooleanMetadataDescriptorByKey('deployment_disabled').kind).toBe( 'deployment-control', diff --git a/packages/feature-flags/src/config.ts b/packages/feature-flags/src/config.ts index c573741edb..30155cefb0 100644 --- a/packages/feature-flags/src/config.ts +++ b/packages/feature-flags/src/config.ts @@ -20,4 +20,10 @@ export const DEPLOYMENT_METADATA_BOOLEAN_CONFIG: Record< description: 'Share anonymous usage analytics (instance and user activity identified only by random IDs) with the Roomote team. Enabled by default; absent means enabled.', }, + opencode_code_mode: { + kind: 'experimental', + group: 'Code Mode', + description: + 'Defer eligible tools and discover them when needed, reducing the tool definitions sent with each request.', + }, }; diff --git a/packages/feature-flags/src/index.ts b/packages/feature-flags/src/index.ts index 80179ad6da..41b3ddbdcb 100644 --- a/packages/feature-flags/src/index.ts +++ b/packages/feature-flags/src/index.ts @@ -32,6 +32,8 @@ export function getBooleanMetadataDescriptorByKey( export const ANONYMOUS_ANALYTICS_METADATA_KEY = 'anonymous_analytics_enabled' as const; +export const OPENCODE_CODE_MODE_METADATA_KEY = 'opencode_code_mode' as const; + export function isAnonymousAnalyticsEnabledFromMetadata( metadata: unknown, cloudEnabled = false, @@ -43,3 +45,10 @@ export function isAnonymousAnalyticsEnabledFromMetadata( return coerceToBoolean(normalizedMetadata[ANONYMOUS_ANALYTICS_METADATA_KEY]); } + +export function isOpenCodeCodeModeEnabledFromMetadata( + metadata: unknown, +): boolean { + const normalizedMetadata = normalizeMetadataRecord(metadata); + return coerceToBoolean(normalizedMetadata[OPENCODE_CODE_MODE_METADATA_KEY]); +} diff --git a/packages/feature-flags/src/types.ts b/packages/feature-flags/src/types.ts index d3101c3850..221040fa75 100644 --- a/packages/feature-flags/src/types.ts +++ b/packages/feature-flags/src/types.ts @@ -1,4 +1,7 @@ -export type MetadataBooleanKind = 'deployment-control' | 'legacy'; +export type MetadataBooleanKind = + | 'deployment-control' + | 'experimental' + | 'legacy'; export interface MetadataBooleanDescriptor { description: string | null; @@ -10,5 +13,6 @@ export interface MetadataRecord { queue_parallel_task_limit?: boolean | number | string; deployment_disabled?: boolean; anonymous_analytics_enabled?: boolean; + opencode_code_mode?: boolean; [key: string]: unknown; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c75c0bef06..34e98fd283 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1340,6 +1340,9 @@ importers: '@roomote/env': specifier: workspace:^ version: link:../env + '@roomote/feature-flags': + specifier: workspace:^ + version: link:../feature-flags '@roomote/types': specifier: workspace:^ version: link:../types From 7f31f1e81240218c0e387096726063b16ee717e1 Mon Sep 17 00:00:00 2001 From: "@mrubens" <2600+mrubens@users.noreply.github.com> Date: Thu, 10 Sep 2026 04:18:55 +0000 Subject: [PATCH 2/2] test: update experimental settings description --- apps/web/src/components/settings/settings-navigation.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/settings/settings-navigation.test.ts b/apps/web/src/components/settings/settings-navigation.test.ts index 9c8e9d9006..080d797a00 100644 --- a/apps/web/src/components/settings/settings-navigation.test.ts +++ b/apps/web/src/components/settings/settings-navigation.test.ts @@ -3,9 +3,9 @@ import { describe, expect, it } from 'vitest'; import { getSettingsNavigationItem } from './settings-navigation'; describe('settings navigation', () => { - it('shows the empty state for Experimental settings', () => { + it('describes Experimental settings with active opt-in features', () => { expect(getSettingsNavigationItem('experimental')?.description).toBe( - 'No experimental features at the moment. Check back soon.', + 'Try opt-in features that may change while they are being evaluated.', ); }); });